前言
在OC階段使用模型轉換的框架有很多,代表有:JSONModel、 YYModel、MJExtension。
OC的原理主要是通過runtime 獲取類的屬性,在運行時獲取Model的字段名集合,遍歷該集合,拿Key去JSON中取值并完成賦值。而且Swift 的屬性默認并不是動態屬性,我們能在運行時獲取一個Model實例的所有字段、字段值,但卻無法給它賦值。事實上,我們拿到的value是原值的一個只讀拷貝,即使獲取到這個拷貝的地址寫入新值,也是無效的。
OC的轉換方式雖然在OC中完全適用,但是缺點也很嚴重,一方面只能只能繼承 NSObject
,并不支持Struct;還有一個更嚴重的問題,optional 的屬性不能正確解析,反正坑還是挺多的。
所以如果是項目中有Swift的Model,就需要找到一個更好的轉換方式。
為了解決這些問題,很多處理JSON的開源庫應運而生。在Swift中,這些開源庫主要朝著兩個方向努力:
- 保持JSON語義,直接解析JSON,但通過封裝使調用方式更優雅、更安全;
- 預定義Model類,將JSON反序列化為類實例,再使用這些實例。
先討論第一種方式,其實我在16年前用Swift的時候主要是用第一種方式,最初是原始的解析方式,茫茫多的guard
,很傻的方法。 然后我就開始用大名鼎鼎的SwiftyJSON,它本質上仍然是根據JSON結構去取值,使用起來順手、清晰。但是他有一個根本性的問題,如果key拼寫錯誤,或者其他的拼寫錯誤就會很崩潰。
第二種方式應該是最優化的,最合理的方式。每一個Model都會通過一個Mappable
協議來表明JSON
字典映射關系,然后實現JSON和對象的轉換。當然還有一個黑魔法 HandyJSON ,通過分析Swift數據結構在內存中的布局,自動分析出映射關系,進一步降低開發者使用的成本。
下面來介紹ObjectMapper 的用法,實現思路,以及源碼分析。
ObjectMapper 介紹
ObjectMapper 是一個使用 Swift 編寫的用于 model 對象(類和結構體)和 JSON 之間轉換的框架。
ObjectMapper特性
- 將JSON映射到對象
- 將對象映射到JSON
- 嵌套對象(獨立,在數組或字典中)
- 映射期間的自定義轉換
- 結構支持
- 不可改變的支持
ObjectMapper可以映射由以下類型組成的類:
Int
Bool
Double
Float
String
RawRepresentable (Enums)
Array<Any>
Dictionary<String, Any>
Object<T: Mappable>
Array<T: Mappable>
Array<Array<T: Mappable>>
Set<T: Mappable>
Dictionary<String, T: Mappable>
Dictionary<String, Array<T: Mappable>>
Optionals of all the above
Implicitly Unwrapped Optionals of the above
基本用法
ObjectMapper中定義了一個協議Mappable
Mappable協議中聲明了兩個方法
mutation func mapping(map: Map)
init?(map: Map)
ObjectMapper使用 <-
運算符來定義每個成員變量如何映射到JSON和從JSON映射。
class User: Mappable {
var username: String?
var age: Int?
var weight: Double!
var array: [Any]?
var dictionary: [String : Any] = [:]
var bestFriend: User? // Nested User object
var friends: [User]? // Array of Users
var birthday: Date?
required init?(map: Map) {
}
// Mappable
func mapping(map: Map) {
username <- map["username"]
age <- map["age"]
weight <- map["weight"]
array <- map["arr"]
dictionary <- map["dict"]
bestFriend <- map["best_friend"]
friends <- map["friends"]
birthday <- (map["birthday"], DateTransform())
}
}
struct Temperature: Mappable {
var celsius: Double?
var fahrenheit: Double?
init?(map: Map) {
}
mutating func mapping(map: Map) {
celsius <- map["celsius"]
fahrenheit <- map["fahrenheit"]
}
}
如果我們的類或結構體如上面的示例一樣實現了協議,我們就可以方便的進行JSON和模型之間的轉換
let JSONString = "{\"weight\": 180}"
let user = User(JSONString: JSONString)
user?.age = 10
user?.username = "ash"
user?.birthday = Date()
user?.weight = 180
if let jsonStr = user?.toJSONString(prettyPrint: true) {
debugPrint(jsonStr)
}
當然也可以通過Mapper類來進行轉換
let user = Mapper<User>().map(JSONString: JSONString)
let JSONString = Mapper().toJSONString(user, prettyPrint: true)
嵌套對象的映射
正如前面所列,ObjectMapper支持嵌套對象的映射
{
"distance" : {
"text" : "102",
"value" : 31
}
}
我們想要直接取出distance對象中的value值,可以設置如下mapping
func mapping(map: Map) {
distance <- map["distance.value"]
}
自定義轉換規則
ObjectMapper允許開發者在數據映射過程中指定轉換規則
class People: Mappable {
var birthday: NSDate?
required init?(_ map: Map) {
}
func mapping(map: Map) {
birthday <- (map["birthday"], DateTransform())
}
let JSON = "\"birthday\":1458117795332"
let result = Mapper<People>().map(JSON)
}
由于我們指定了birthday
的轉換規則,所以上述代碼在解析JSON數據的時候會將long類型轉換成Date類型
除了使用ObjectMapper給我們提供的轉換規則外,我們還可以通過實現TransformType協議來自定義我們的轉換規則
ObjectMapper為我們提供了一個TransformOf類來實現轉換結果,TransformOf實際就是實現了TransformType協議的,TransformOf有兩個類型的參數和兩個閉包參數,類型表示參與轉換的數據的類型,閉包表示轉換的規則
public protocol TransformType {
typealias Object
typealias JSON
func transformFromJSON(value: AnyObject?) -> Object?
func transformToJSON(value: Object?) -> JSON?
}
let transform = TransformOf<Int, String>(fromJSON: { (value: String?) -> Int? in
}, toJSON: { (value: Int?) -> String? in
// transform value from Int? to String?
if let value = value {
return String(value)
}
return nil
})
func mapping(map: Map) {
id <- (map["id"], transform)
}
泛型對象
ObjectMapper同樣可以處理泛型類型的參數,不過這個泛型類型需要在實現了Mappable協議的基礎上才可以正常使用
class User: Mappable {
var name: String?
required init?(_ map: Map) {
}
func mapping(_ map: Map) {
name <- map["name"]
}
}
class Result<T: Mappable>: Mappable {
var result: T?
required init?(_ map: Map) {
}
func mapping(map: Map) {
result <- map["result"]
}
}
let JSON = "{\"result\": {\"name\": \"anenn\"}}"
let result = Mapper<Result<User>>().map(JSON)
基本上的大部分常用用法都介紹完了,滿足日常的開發需求應該是沒問題的,下面我們要研究一下源碼部分
源碼解析
功能分類
根據實現的思路來分類應該可以分成三類:
- Core 部分
- Operators 部分
- Transforms 部分
其實 core 和 Operators 也可以歸為一類,但是拆開來看更加容易理解,還是拆開來吧。
因為源代碼比較多,這篇文章先介紹 Core 部分,了解這部分基本上的實現思路就已經很明確了,然后在最后會介紹一下 Sourcery 的自動代碼生成,不然 mapping
方法中的代碼寫的讓人很絕望。
Mappable
跟Mappable
相關的協議有StaticMappable
、ImmutableMappable
,我們先將 StaticMappable
和 ImmutableMappable
這兩種協議的處理邏輯放一放,直接關注最重要的 Mappable
協議的實現,了解了 Mappable
另外兩個很好理解。
/// BaseMappable should not be implemented directly. Mappable or StaticMappable should be used instead
public protocol BaseMappable {
/// This function is where all variable mappings should occur. It is executed by Mapper during the mapping (serialization and deserialization) process.
mutating func mapping(map: Map)
}
public protocol Mappable: BaseMappable {
/// This function can be used to validate JSON prior to mapping. Return nil to cancel mapping at this point
init?(map: Map)
}
public extension BaseMappable {
/// Initializes object from a JSON String
public init?(JSONString: String, context: MapContext? = nil) {
if let obj: Self = Mapper(context: context).map(JSONString: JSONString) {
self = obj
} else {
return nil
}
}
/// Initializes object from a JSON Dictionary
public init?(JSON: [String: Any], context: MapContext? = nil) {
if let obj: Self = Mapper(context: context).map(JSON: JSON) {
self = obj
} else {
return nil
}
}
/// Returns the JSON Dictionary for the object
public func toJSON() -> [String: Any] {
return Mapper().toJSON(self)
}
/// Returns the JSON String for the object
public func toJSONString(prettyPrint: Bool = false) -> String? {
return Mapper().toJSONString(self, prettyPrint: prettyPrint)
}
}
BaseMappable
為實現 Mappable
的 Model 提供了四種實例方法,有兩個是初始化方法,當然你也可以自己新建一個 Mapper
來初始化;還有兩個是 Model 轉 JSON 的方法。
Mapper
繼續看 Mapper
的代碼,Mapper中核心代碼為下面的方法
/// Maps a JSON dictionary to an object that conforms to Mappable
public func map(JSON: [String: Any]) -> N? {
let map = Map(mappingType: .fromJSON, JSON: JSON, context: context, shouldIncludeNilValues: shouldIncludeNilValues)
if let klass = N.self as? StaticMappable.Type { // Check if object is StaticMappable
if var object = klass.objectForMapping(map: map) as? N {
object.mapping(map: map)
return object
}
} else if let klass = N.self as? Mappable.Type { // Check if object is Mappable
if var object = klass.init(map: map) as? N {
object.mapping(map: map)
return object
}
} else if let klass = N.self as? ImmutableMappable.Type { // Check if object is ImmutableMappable
do {
return try klass.init(map: map) as? N
} catch let error {
#if DEBUG
let exception: NSException
if let mapError = error as? MapError {
exception = NSException(name: .init(rawValue: "MapError"), reason: mapError.description, userInfo: nil)
} else {
exception = NSException(name: .init(rawValue: "ImmutableMappableError"), reason: error.localizedDescription, userInfo: nil)
}
exception.raise()
#endif
}
} else {
// Ensure BaseMappable is not implemented directly
assert(false, "BaseMappable should not be implemented directly. Please implement Mappable, StaticMappable or ImmutableMappable")
}
return nil
}
根據N的協議類型走不同的協議方法,最終得到 object
。
讓我們用 Mappable
來舉例,先回到之前協議中的方法
mutation func mapping(map: Map)
init?(map: Map)
這樣對著看就很好理解了,init?(map: Map)
沒有 return nil
的時候,就會調用 func mapping(map: Map)
方法來指定映射關系,那這個映射關系有什么作用呢,后面會慢慢介紹。
extension Mapper {
// MARK: Functions that create JSON from objects
///Maps an object that conforms to Mappable to a JSON dictionary <String, Any>
public func toJSON(_ object: N) -> [String: Any] {
var mutableObject = object
let map = Map(mappingType: .toJSON, JSON: [:], context: context, shouldIncludeNilValues: shouldIncludeNilValues)
mutableObject.mapping(map: map)
return map.JSON
}
///Maps an array of Objects to an array of JSON dictionaries [[String: Any]]
public func toJSONArray(_ array: [N]) -> [[String: Any]] {
return array.map {
// convert every element in array to JSON dictionary equivalent
self.toJSON($0)
}
}
///Maps a dictionary of Objects that conform to Mappable to a JSON dictionary of dictionaries.
public func toJSONDictionary(_ dictionary: [String: N]) -> [String: [String: Any]] {
return dictionary.map { (arg: (key: String, value: N)) in
// convert every value in dictionary to its JSON dictionary equivalent
return (arg.key, self.toJSON(arg.value))
}
}
///Maps a dictionary of Objects that conform to Mappable to a JSON dictionary of dictionaries.
public func toJSONDictionaryOfArrays(_ dictionary: [String: [N]]) -> [String: [[String: Any]]] {
return dictionary.map { (arg: (key: String, value: [N])) in
// convert every value (array) in dictionary to its JSON dictionary equivalent
return (arg.key, self.toJSONArray(arg.value))
}
}
/// Maps an Object to a JSON string with option of pretty formatting
public func toJSONString(_ object: N, prettyPrint: Bool = false) -> String? {
let JSONDict = toJSON(object)
return Mapper.toJSONString(JSONDict as Any, prettyPrint: prettyPrint)
}
/// Maps an array of Objects to a JSON string with option of pretty formatting
public func toJSONString(_ array: [N], prettyPrint: Bool = false) -> String? {
let JSONDict = toJSONArray(array)
return Mapper.toJSONString(JSONDict as Any, prettyPrint: prettyPrint)
}
/// Converts an Object to a JSON string with option of pretty formatting
public static func toJSONString(_ JSONObject: Any, prettyPrint: Bool) -> String? {
let options: JSONSerialization.WritingOptions = prettyPrint ? .prettyPrinted : []
if let JSON = Mapper.toJSONData(JSONObject, options: options) {
return String(data: JSON, encoding: String.Encoding.utf8)
}
return nil
}
/// Converts an Object to JSON data with options
public static func toJSONData(_ JSONObject: Any, options: JSONSerialization.WritingOptions) -> Data? {
if JSONSerialization.isValidJSONObject(JSONObject) {
let JSONData: Data?
do {
JSONData = try JSONSerialization.data(withJSONObject: JSONObject, options: options)
} catch let error {
print(error)
JSONData = nil
}
return JSONData
}
return nil
}
}
Mapper
還有一些 toJSON
的方法,這邊的方法也很好理解,具體的實現都是在 Map
的一些方法,要知道這些方法具體實現就需要繼續往下看。
Map
Map 中有兩個核心的方法,先看自定義下標的方法,分析一下最重要的那個自定義下標的方法
/// Sets the current mapper value and key.
/// The Key paramater can be a period separated string (ex. "distance.value") to access sub objects.
public subscript(key: String) -> Map {
// save key and value associated to it
return self.subscript(key: key)
}
public subscript(key: String, delimiter delimiter: String) -> Map {
return self.subscript(key: key, delimiter: delimiter)
}
public subscript(key: String, nested nested: Bool) -> Map {
return self.subscript(key: key, nested: nested)
}
public subscript(key: String, nested nested: Bool, delimiter delimiter: String) -> Map {
return self.subscript(key: key, nested: nested, delimiter: delimiter)
}
public subscript(key: String, ignoreNil ignoreNil: Bool) -> Map {
return self.subscript(key: key, ignoreNil: ignoreNil)
}
public subscript(key: String, delimiter delimiter: String, ignoreNil ignoreNil: Bool) -> Map {
return self.subscript(key: key, delimiter: delimiter, ignoreNil: ignoreNil)
}
public subscript(key: String, nested nested: Bool, ignoreNil ignoreNil: Bool) -> Map {
return self.subscript(key: key, nested: nested, ignoreNil: ignoreNil)
}
public subscript(key: String, nested nested: Bool?, delimiter delimiter: String, ignoreNil ignoreNil: Bool) -> Map {
return self.subscript(key: key, nested: nested, delimiter: delimiter, ignoreNil: ignoreNil)
}
private func `subscript`(key: String, nested: Bool? = nil, delimiter: String = ".", ignoreNil: Bool = false) -> Map {
// save key and value associated to it
currentKey = key
keyIsNested = nested ?? key.contains(delimiter)
nestedKeyDelimiter = delimiter
if mappingType == .fromJSON {
// check if a value exists for the current key
// do this pre-check for performance reasons
if keyIsNested {
// break down the components of the key that are separated by delimiter
(isKeyPresent, currentValue) = valueFor(ArraySlice(key.components(separatedBy: delimiter)), dictionary: JSON)
} else {
let object = JSON[key]
let isNSNull = object is NSNull
isKeyPresent = isNSNull ? true : object != nil
currentValue = isNSNull ? nil : object
}
// update isKeyPresent if ignoreNil is true
if ignoreNil && currentValue == nil {
isKeyPresent = false
}
}
return self
}
另一個核心的方法就是通過自定義下標的值,從JSON字典中根據key獲取了value。
/// Fetch value from JSON dictionary, loop through keyPathComponents until we reach the desired object
private func valueFor(_ keyPathComponents: ArraySlice<String>, dictionary: [String: Any]) -> (Bool, Any?) {
// Implement it as a tail recursive function.
if keyPathComponents.isEmpty {
return (false, nil)
}
if let keyPath = keyPathComponents.first {
let isTail = keyPathComponents.count == 1
let object = dictionary[keyPath]
if object is NSNull {
return (isTail, nil)
} else if keyPathComponents.count > 1, let dict = object as? [String: Any] {
let tail = keyPathComponents.dropFirst()
return valueFor(tail, dictionary: dict)
} else if keyPathComponents.count > 1, let array = object as? [Any] {
let tail = keyPathComponents.dropFirst()
return valueFor(tail, array: array)
} else {
return (isTail && object != nil, object)
}
}
return (false, nil)
}
/// Fetch value from JSON Array, loop through keyPathComponents them until we reach the desired object
private func valueFor(_ keyPathComponents: ArraySlice<String>, array: [Any]) -> (Bool, Any?) {
// Implement it as a tail recursive function.
if keyPathComponents.isEmpty {
return (false, nil)
}
//Try to convert keypath to Int as index
if let keyPath = keyPathComponents.first,
let index = Int(keyPath) , index >= 0 && index < array.count {
let isTail = keyPathComponents.count == 1
let object = array[index]
if object is NSNull {
return (isTail, nil)
} else if keyPathComponents.count > 1, let array = object as? [Any] {
let tail = keyPathComponents.dropFirst()
return valueFor(tail, array: array)
} else if keyPathComponents.count > 1, let dict = object as? [String: Any] {
let tail = keyPathComponents.dropFirst()
return valueFor(tail, dictionary: dict)
} else {
return (isTail, object)
}
}
return (false, nil)
}
看到這里其實 Core 部分的代碼基本上就看完了,還有一些toJSON的方法,其他的類同的方法,那些對于理解 ObjectMapper 沒有影響。
寫在最后
Sourcery
簡單介紹一些 Sourcery 這個自動生成代碼的工具。
Sourcery 是一個 Swift 代碼生成的開源命令行工具,它 (通過 SourceKitten 使用 Apple 的 SourceKit 框架,來分析你的源碼中的各種聲明和標注,然后套用你預先定義的 Stencil 模板 (一種語法和 Mustache 很相似的 Swift 模板語言) 進行代碼生成。我們下面會先看一個使用 SourceKitten 最簡單的例子,來說明如何使用這個工具。然后再針對我們的字典轉換問題進行實現。
安裝 SourceKitten 非常簡單,brew install sourcery
即可。不過,如果你想要在實際項目中使用這個工具的話,我建議直接從發布頁面下載二進制文件,放到 Xcode 項目目錄中,然后添加 Run Script 的 Build Phase 來在每次編譯的時候自動生成。
之前說過了 mapping
函數實現起來過于臃腫耗時,你可以用插件來生成 mapping
函數
用于生成Mappable
和ImmutableMappable
代碼的Xcode插件
但是Xcode 8之后不讓用插件了,除非用野路子重簽名的方式安裝插件,而且安裝了還不一定能用,反正那個很坑,還要復制一個Xcode用來打包上傳,本弱雞電腦根本沒那么多空間。
兩個方法我都試過了, 個人覺得 SourceKitten 更加適合,那個插件的確實不好用,還有一種方式,可以在網站上自動生成,然后復制進來。
接下來就可以嘗試以下書寫模板代碼了??梢詤⒄?Sourcery 文檔 關于單個 Type 和 Variable 的部分的內容來實現。另外,可以考慮使用 --watch
模式來在文件改變時自動生成代碼,來實時觀察結果。
如果聲明一個struct
protocol AutoMappable {}
struct Person {
var firstName: String
var lastName: String
var birthDate: Date
var friend: [String]
var lalala: Dictionary<String, Any>
var age: Int {
return Calendar.current.dateComponents([.year],
from: birthDate,
to: Date()).year ?? -1
}
}
extension Person: AutoMappable {}
下面是我的模版代碼
import ObjectMapper
{% for type in types.implementing.AutoMappable|struct %}
// MARK: {{ type.name }} Mappable
extension {{type.name}}: Mappable {
init?(map: Map) {
return nil
}
mutating func mapping(map: Map) {
{% for variable in type.storedVariables %}
{% if variable.isArray %}
{{variable.name}} <- map["{{variable.name}}.0.value"]
{% elif variable.isDictionary %}
{{variable.name}} <- map["{{variable.name}}.value"]
{% else %}
{{variable.name}} <- map["{{variable.name}}"]
{% endif %}
{% endfor %}
}
}
{% endfor %}
自動生成的代碼顯示如下:
import ObjectMapper
// MARK: Person Mappable
extension Person: Mappable {
init?(map: Map) {
return nil
}
mutating func mapping(map: Map) {
firstName <- map["firstName"]
lastName <- map["lastName"]
birthDate <- map["birthDate"]
friend <- map["friend.0.value"]
lalala <- map["lalala.value"]
}
}
上面的這種方式顯然是運行時最高效的方式,所以強烈推薦是這個方法來使用ObjectMapper。
后面會繼續介紹 ObjectMapper 其他源碼的實現思路。