JSON的第三方庫原始碼閱讀分享(ObjectMapper, SwiftyJSON, 以及Codable)

weixin_34166847發表於2018-05-28

更多文章

ObjectMapper原始碼分析

在看ObjectMapper原始碼的時候,我自己嘗試著寫了一個簡易的JSON解析器。程式碼在DJJSON的ObjMapper裡。

首先我們都知道,使用ObjectMapper的時候,我們一定要實現Mappable協議。這個協議裡又有兩個要實現的方法:

init?(map: Map)
mutating func mapping(map: Map)
複製程式碼

使用的時候,只用如下:

struct Ability: Mappable {
	var mathematics: String?
	var physics: String?
	var chemistry: String?

	init?(map: Map) {

	}

	mutating func mapping(map: Map) {
		mathematics <- map["mathematics"]
		physics <- map["physics"]
		chemistry <- map["chemistry"]
	}
}
複製程式碼

然後對應的json如下:

let json = """
{
"mathematics": "excellent",
"physics": "bad",
"chemistry": "fine"
}
"""
複製程式碼

然後將這段json解析為Ability的Model, 即:

let ability = Mapper<Ability>().map(JSONString: json)
複製程式碼

這樣就完成了JSON資料的解析成Model的過程,這也是我們專案中最頻繁出現的場景。那麼,我們有想過,為什麼這樣就能將JSON的資料轉化為對應的Model麼?為什麼會需要有‘<-’這麼奇怪的符號,它又是啥意思呢?

首先初看<-的符號,我們的第一感覺就是把右邊的值賦給左邊的變數,然後我們去看原始碼,發現這個符號是這個庫自定義的一個操作符。在Operators.swift裡。

定義如下:

infix operator <-

/// Object of Basic type
public func <- <T>(left: inout T, right: Map) {
	switch right.mappingType {
	case .fromJSON where right.isKeyPresent:
		FromJSON.basicType(&left, object: right.value())
	case .toJSON:
		left >>> right
	default: ()
	}
}
複製程式碼

然後根據不同的泛型型別,這個操作符會進行不同的處理。

接著,我們再看一下map方法。

map方法存在於Mapper類中, 定義如下:

 func map(JSONString: String) -> M? {
	if let JSON = Mapper.parseJSONString(JSONString: JSONString) as? [String: Any] {
  		return map(JSON: JSON)
	}
	return nil
}

func map(JSON: [String: Any]) -> M? {
	let map = Map(JSON: JSON)
	if let klass = M.self as? Mappable.Type {
  	if var obj = klass.init(map: map) as? M {
    	obj.mapping(map: map)
    	return obj
  		}
	}
	return nil
}
複製程式碼

可以看到,在map的方法中,我們最後會呼叫Mappable協議中定義的mapping方法,來對json資料做出轉化。

最後再看一下Map這個類,這個類主要用來處理找到key所對應的value。處理方式如下:

private func valueFor(_ keyPathComponents: ArraySlice<String>, dict: [String: Any]) -> (Bool, Any?) {
	guard !keyPathComponents.isEmpty else { return (false, nil) }

	if let keyPath = keyPathComponents.first {
  		let obj = dict[keyPath]
  		if obj is NSNull {
    		return (true, nil)
  		} else if keyPathComponents.count > 1, let d = obj as? [String: Any] {
    		let tail = keyPathComponents.dropFirst()
    		return valueFor(tail, dict: d)
  		} else if keyPathComponents.count > 1, let arr = obj as? [Any] {
    		let tail = keyPathComponents.dropFirst()
    		return valueFor(tail, array: arr)
  		} else {
    		return (obj != nil, obj)
  		}
	}

	return (false, nil)
}

private func valueFor(_ keyPathComponents: ArraySlice<String>, array: [Any]) -> (Bool, Any?) {
	guard !keyPathComponents.isEmpty else { return (false, nil) }

	if let keyPath = keyPathComponents.first, let index = Int(keyPath), index >= 0 && index < array.count {
  		let obj = array[index]
  		if obj is NSNull {
    		return (true, nil)
  		} else if keyPathComponents.count > 1, let dict = obj as? [String: Any] {
    		let tail = keyPathComponents.dropFirst()
    		return valueFor(tail, dict: dict)
  		} else if keyPathComponents.count > 1, let arr = obj as? [Any] {
    		let tail = keyPathComponents.dropFirst()
    		return valueFor(tail, array: arr)
  		} else {
    		return (true, obj)
  		}
	}

	return (false, nil)
}
複製程式碼

其中在處理分隔符上,採用的是遞迴呼叫的方式,不過就我們目前專案中,還沒有用到過。

上述這幾個步驟,就是ObjectMapper的核心方法。我也根據這些步驟,自己實現了一個解析的庫。

但是這個只能解析一些最簡單的型別,其他的像enum之類的,還需要做一些自定義的轉化。主要的資料轉化都在Operators資料夾中。

SwiftyJSON 原始碼解析

構造器

SwiftyJSON對外暴露的主要的構造器:

public init(data: Data, options opt: JSONSerialization.ReadingOptions = []) throws
public init(_ object: Any)
public init(parseJSON jsonString: String)
複製程式碼

最終呼叫的構造器為:

fileprivate init(jsonObject: Any)
複製程式碼
型別

自定義了幾個型別:

public enum Type: Int {
	case number
	case string
	case bool
	case array
	case dictionary
	case null
	case unknown
}
複製程式碼
儲存物件已經何時對JSON進行的解析
  • 儲存物件:

       /// Private object
      fileprivate var rawArray: [Any] = []
      fileprivate var rawDictionary: [String: Any] = [:]
      fileprivate var rawString: String = ""
      fileprivate var rawNumber: NSNumber = 0
      fileprivate var rawNull: NSNull = NSNull()
      fileprivate var rawBool: Bool = false
      
      /// JSON type, fileprivate setter
      public fileprivate(set) var type: Type = .null
    
      /// Error in JSON, fileprivate setter
      public fileprivate(set) var error: SwiftyJSONError?
    複製程式碼
  • 解析過程

    主要是在object屬性的get & set方法中進行。然後將解析後的值儲存到上述的屬性中去。解析過程中,有個unwrap方法值得我們關注。

    unwrap:

      /// Private method to unwarp an object recursively
      private func unwrap(_ object: Any) -> Any {
          switch object {
          case let json as JSON:
              return unwrap(json.object)
          case let array as [Any]:
              return array.map(unwrap)
          case let dictionary as [String: Any]:
              var unwrappedDic = dictionary
              for (k, v) in dictionary {
                  unwrappedDic[k] = unwrap(v)
              }
              return unwrappedDic
          default:
              return object
          }
      }
    複製程式碼

    這個方法根據object的型別,對其進行遞迴的解析。

JSON的語法糖

為了統一Array和Dictionary的下標訪問的型別,自定義了一個enum型別,JSONKey:

public enum JSONKey {
    case index(Int)
    case key(String)
}

// To mark both String and Int can be used in subscript.

extension Int: JSONSubscriptType {
    public var jsonKey: JSONKey {
        return JSONKey.index(self)
    }
}

extension String: JSONSubscriptType {
    public var jsonKey: JSONKey {
        return JSONKey.key(self)
    }
}
複製程式碼

然後就是喜聞樂見的subscript語法糖:

```
 let json = JSON[data]
 let path = [9,"list","person","name"]
 let name = json[path]
 ```
public subscript(path: [JSONSubscriptType]) -> JSON


// let name = json[9]["list"]["person"]["name"]
public subscript(path: JSONSubscriptType...) -> JSON
複製程式碼
資料的轉化

最後就是暴露欄位,給開發者使用。比如:

 public var int: Int?
 public var intValue: Int
複製程式碼

每個型別都有optional和unoptional。

Swift 4.0及以後的JSON解析

首先我們知道,在Swift4.0以前,JSON資料解析成Model是多麼的繁瑣。舉個例子:

/// Swift 3.0
if let data = json.data(using: .utf8, allowLossyConversion: true),
  let dict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
  print("name: \(dict["name"])")
}
/// 我們只能獲取到json對應的dict,至於轉換成model的話,還是需要採用上面的方式,本質都是遞迴轉化,即key與property的對應轉化。
複製程式碼

那麼,在swift4.0後,我們可以怎麼做呢。如下:

struct DJJSON<T: Decodable> {

  fileprivate let data: Data
  
  init(data: Data?) {
    if let data = data {
      self.data = data
    } else {
      self.data = Data()
    }
  }
  
  init(jsonString: String) {
    let data = jsonString.data(using: .utf8, allowLossyConversion: true)
    self.init(data: data)
  }
  
  func decode() -> T? {
    do {
      let decoder = JSONDecoder()
      decoder.keyDecodingStrategy = .convertFromSnakeCase
      let result = try decoder.decode(T.self, from: data)
      return result
    } catch let error {
      print("error: \(error)")
    }
    return nil
  }
  
}

if let r = DJJSON<GrocerProduct>(jsonString: json).decode() {
  print("result: \(r.ability?.mathematics)")
  print("imageUrl: \(r.imageUrl)")
}
複製程式碼

我們只要保證轉化的model是遵守Codable協議的即可。至於Key跟Property的轉化,蘋果預設就幫我做了。那麼有的朋友就要問了,那怎麼自定義Key呢,蘋果給我們提供了一個enum叫CodingKeys, 我們只要在這個裡面做自定義就行了,預設的話就是key與property是對應的。如:

private enum CodingKeys: String, CodingKey {
    case mathematics = "math"
    case physics, chemistry
}
複製程式碼

那麼問題又來了,有些欄位是蛇形的,像什麼image_url,有沒有辦法不自己做自定義就能搞定呢,誒,還真有,那就是在swift4.1中提供的這個convertFromSnakeCase。

// 完成image_url與imageUrl的轉化
decoder.keyDecodingStrategy = .convertFromSnakeCase
複製程式碼

那麼,這個是怎麼實現的呢,我們很好奇,因為感覺自己也可以做這個轉化啊,是不是easy game。我們去看swift的原始碼:

fileprivate static func _convertFromSnakeCase(_ stringKey: String) -> String {
        guard !stringKey.isEmpty else { return stringKey }
    
        // Find the first non-underscore character
        guard let firstNonUnderscore = stringKey.index(where: { $0 != "_" }) else {
            // Reached the end without finding an _
            return stringKey
        }
    
        // Find the last non-underscore character
        var lastNonUnderscore = stringKey.index(before: stringKey.endIndex)
        while lastNonUnderscore > firstNonUnderscore && stringKey[lastNonUnderscore] == "_" {
            stringKey.formIndex(before: &lastNonUnderscore)
        }
    
        let keyRange = firstNonUnderscore...lastNonUnderscore
        let leadingUnderscoreRange = stringKey.startIndex..<firstNonUnderscore
        let trailingUnderscoreRange = stringKey.index(after: lastNonUnderscore)..<stringKey.endIndex
    
        var components = stringKey[keyRange].split(separator: "_")
        let joinedString : String
        if components.count == 1 {
            // No underscores in key, leave the word as is - maybe already camel cased
            joinedString = String(stringKey[keyRange])
        } else {
            joinedString = ([components[0].lowercased()] + components[1...].map { $0.capitalized }).joined()
        }
    
        // Do a cheap isEmpty check before creating and appending potentially empty strings
        let result : String
        if (leadingUnderscoreRange.isEmpty && trailingUnderscoreRange.isEmpty) {
            result = joinedString
        } else if (!leadingUnderscoreRange.isEmpty && !trailingUnderscoreRange.isEmpty) {
            // Both leading and trailing underscores
            result = String(stringKey[leadingUnderscoreRange]) + joinedString + String(stringKey[trailingUnderscoreRange])
        } else if (!leadingUnderscoreRange.isEmpty) {
            // Just leading
            result = String(stringKey[leadingUnderscoreRange]) + joinedString
        } else {
            // Just trailing
            result = joinedString + String(stringKey[trailingUnderscoreRange])
        }
        return result
    }
複製程式碼

真的寫的特別精煉跟嚴謹好吧,學習一下這個。

到這裡,就結束了,謝謝大家。

相關文章