自己動手 打造Swift全功能 JSON 解析、生成庫
準備工作
起因
在我動手搞這個 JSON 解析庫之前,我一直在用 SwiftyJSON 這個庫,這個庫是國人開源的***的 Swift 項目,沒有之一,也是全球***的 Swift 庫第二名,***名是網絡庫 Alamofire。由于要實現 ["key"]["key1"] 這樣的遞歸查找,我一直覺得 JSON 解析庫非常復雜難搞。
過程
最近比較閑,我打算把之前用過的開源庫都自己實現一下,提升一下自己。而且我在實際使用 SwiftyJSON 的過程中,遇到過非合法長字符串導致奔潰的情況,我打算先從 JSON 解析庫下手,于是中秋節的前一天,吃完午飯我就開搞了,到了下午六七點,解析的功能就全部搞定了,十分出乎預料。中秋節這天我又把生成的功能做了,整理下代碼,收拾收拾就給開源了。
API 統計
言歸正傳,我們的準備工作還是要做的:統計 SwiftyJSON 的主要 API。經過簡單統計,我找到了所有我在項目中使用過的 SwiftyJSON 的 API,主要分為四類:
通過特定路徑取出特定類型的值,如:json["key"]["key1"].stringValue
取出某個數組類型的子 JSON,循環拿到里面的值
將某個 JSON 對象格式化成字符串
使用 Dictionary 生成 JSON 對象
遞歸取值
設計基本結構
既然要兼容 SwiftyJSON 的主要 API,那調用方式跟它一樣就行了:先使用 NSData、Array 或者 Dictionary 生成 JSON 對象,再對這個對象進行操作,拿到我們想要的值、數組、完整的 JSON 字符串等。
為了對比 API 的執行結果,我們仍然引入 SwiftyJSON 庫,所以我們需要一個其他的類名,在這里我們就暫定為 JSONND,是 JSON Never Die 的縮寫,含義是永不奔潰的 JSON 解析庫。
我們先從網絡數據下手。網絡數據的來源一般為 NSData,經過簡單查詢我們知道系統提供了一個 JSON 解析方法,可以把 NSData 格式的解析為 AnyObject,構造出 JSONND 類:
- public struct JSONND {
- public var jsonObject: AnyObject!
- public static func initWithData(data: NSData) -> JSONND! {
- do {
- return JSONND(jsonObject: try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments))
- } catch let error as NSError {
- let e = NSError(domain: "JSONNeverDie.JSONParseError", code: error.code, userInfo: error.userInfo)
- NSLog(e.localizedDescription)
- return JSONND()
- }
- }
- }
需要注意的是,我們給 JSON 類使用的是 struct 結構體,為了它能夠具備自動初始化函數,值類型等優良特性。JSON 直觀上感覺是 String 的衍生,故使用值類型也起到降低學習成本的作用。
我們使用下面的代碼來檢驗成果:
- let data = NSData(contentsOfURL: NSURL(string: "http://httpbin.org/get?hello=world")!)!
- let json = JSONND.initWithData(data)
運行,正常,初始化代碼完成。
支持 ["key"]["key1"] 形式的遞歸取值
為了支持遞歸取值,同時不讓我們的 JSONND 結構體變的過于臃腫,我們考慮將遞歸取值的任務交給第二個結構體:
- public struct JSONNDElement {
- public var data: AnyObject!
- public init(data: AnyObject!) {
- self.data = data
- }
- public subscript (index: String) -> JSONNDElement {
- if let jsonDictionary = self.data as? Dictionary {
- if let value = jsonDictionary[index] {
- return JSONNDElement(data: value)
- } else {
- NSLog("JSONNeverDie: No such key '\(index)'")
- }
- }
- return JSONNDElement(data: nil)
- }
- }
同時,我們需要在 JSONND 結構體中觸發遞歸取值的***次:
- public subscript (index: String) -> JSONNDElement {
- let jsonNDElement = JSONNDElement(data: self.jsonObject)
- return jsonNDElement[index]
- }
檢驗成果:
- let data = NSData(contentsOfURL: NSURL(string: "http://httpbin.org/get?hello=world")!)!
- let json = JSONND.initWithData(data)
- let args = json["args"]
- let hello = args["hello"]
運行,正常,遞歸取值完成。
取出 Int、Float、String、Array、Bool 類型的值
在我們通過 ["key"]["key1"] 的形式拿到最終的 JSONNDElement 對象之后,我們就需要把他的 data 轉換成我們想要的類型輸出了。介紹 JSON 數據類型的文檔:http://www.yiibai.com/json/json_data_types.html
SwiftyJSON 采用兩級函數來取值,即 .int 為 Int? 類型, .intValue 為 Int 類型,這顯然是為了適應不同的 API 設計作出的兼容,我們也要實現這樣的兩級取值。要實現取值,其實是非常簡單的,if let 轉換一下類型,基本就 OK 了:
- public var int: Int? {
- get {
- if let _ = self.data {
- return self.data.integerValue
- } else {
- return nil
- }
- }
- }
- public var intValue: Int {
- get {
- if let i = self.int {
- return i
- } else {
- return 0
- }
- }
- }
由于代碼比較繁瑣無趣,這里只用 Int 展示一下,更多代碼請見 Github。
Array 類型的處理要單獨拿出來處理,因為 Array 有子級,所以我們得到的將是 JSONNDElement 數組。Array 處理代碼如下:
- public var array: [JSONNDElement]? {
- get {
- if let _ = self.data {
- if let arr = self.data as? Array {
- var result = Array()
- for i in arr {
- result.append(JSONNDElement(data: i))
- }
- return result
- } else {
- return nil
- }
- } else {
- return nil
- }
- }
- }
- public var arrayValue: [JSONNDElement] {
- get {
- if let i = self.array {
- return i
- } else {
- return []
- }
- }
- }
將 JSONND 對象格式化成字符串
通過在 JSONND 和 JSONNDElement 中添加兩個函數,將成員變量 data 轉換成 String 就可以加上這個功能了:
JSONND 中:
- public var jsonString: String? {
- let jsonNDElement = JSONNDElement(data: self.jsonObject)
- return jsonNDElement.jsonString
- }
- public var jsonStringValue: String {
- let jsonNDElement = JSONNDElement(data: self.jsonObject)
- return jsonNDElement.jsonStringValue
- }
JSONNDElement 中:
- public var jsonString: String? {
- get {
- do {
- if let _ = self.data {
- return NSString(data: try NSJSONSerialization.dataWithJSONObject(self.data, options: .PrettyPrinted), encoding: NSUTF8StringEncoding) as? String
- } else {
- return nil
- }
- } catch {
- return nil
- }
- }
- }
- public var jsonStringValue: String {
- get {
- if let i = self.jsonString {
- return i
- } else {
- return ""
- }
- }
- }
使用 Array、Dictionary 生成 JSON 對象
這一步操作我們將使用從 SwiftyJSON 中偷來的函數,稍加改裝就可以利用了:
- // stolen from SwiftyJSON
- extension JSONND: DictionaryLiteralConvertible {
- public init(dictionaryLiteral elements: (String, AnyObject)...) {
- self.init(jsonObject: elements.reduce([String : AnyObject]()){(dictionary: [String : AnyObject], element:(String, AnyObject)) -> [String : AnyObject] in
- var d = dictionary
- d[element.0] = element.1
- return d
- })
- }
- }
- // stolen from SwiftyJSON
- extension JSONND: ArrayLiteralConvertible {
- public init(arrayLiteral elements: AnyObject...) {
- self.init(jsonObject: elements)
- }
- }
代碼的原理也很簡單,利用系統的自動轉換 protocol:DictionaryLiteralConvertible 和 ArrayLiteralConvertible,讓 Array 和 Dictionary 自動轉換為 JSONND 類型。現在我們可以采用這種方式定義 JSONND 對象了:
- let dictionaryJSON: JSONND = ["a": 1, "b": [1, 2, 3]]
- let arrayJSON: JSONND = [0, 1, 2]
搞定!
檢驗成果
我已經給 JSONNeverDie 項目寫了完整的單元測試來測試每一項功能,感興趣的同學可以去 Github 查看測試代碼。