前言
客戶端開發項目中,不可避免地需要解析網絡數據---將服務端下發的JSON數據解析成客戶端可閱讀友好的Model。Objective-C下使用最多的是JSONModel,它能在OC Runtime基礎下很好地完成解析工作。那么在純Swift代碼中,這個功能是如何實現的?下面開始我們的探索~
- 手動解析
- 原生:Swift4.0 JSONDecoder
- JSONDecoder 問題 及 解決方案
手動解析
假設一個User類要解析,Json如下:
1
2
3
4
5
|
{ "userId" : 1 , "name" : "Jack" , "height" : 1.7 , } |
對應的創建一個User結構體(也可以是類):
1
2
3
4
5
|
struct User { var userId: Int? var name: String? var height: CGFloat? } |
把JSON轉成User
在Swift4.0前,我們以手動解析的方式將JSON model化。給User加一個以JSON為參數的初始化方法,代碼如下:
1
2
3
4
5
6
7
8
9
10
11
|
struct User { ... init?(json: [String: Any]) { guard let userId = json[ "userId" ] as? Int, let name = json[ "name" ] as? String, let height = json[ "height" ] as? CGFloat else { return nil } self.userId = userId self.name = name self.height = height } } |
依次從json中取出model所需的具體類型的數據,填充到具體對應屬性中。如果其中一個轉換失敗或者沒有值,初始化會失敗返回nil。
如果某個值不需要強校驗,直接取值再賦值,把guard let內的語句去掉。例如,若height不用校驗,可看如下代碼:
1
2
3
4
5
6
7
8
9
10
|
struct User { ... init?(json: [String: Any]) { guard let userId = json[ "userId" ] as? Int, let name = json[ "name" ] as? String else { return nil } self.userId = userId self.name = name self.height = json[ "height" ] as? CGFloat } } |
原生:Swift4.0 JSONDecoder
2017年6月份左右Swift4.0發布,其中一個重大更新就是JSON的加解密。擺脫手工解析字段的繁瑣,聊聊幾行代碼就可將JSON轉換成Model。與Objective-C下的JSONModel極為相似。同樣解析上述例子中的User,Swift4.0可以這么寫:
1
2
3
4
5
6
7
8
9
10
|
struct User: Decodable { var userId: Int? var name: String? var height: CGFloat? } let decoder = JSONDecoder() if let data = jsonString.data(using: String.Encoding.utf8) { let user = try ? decoder.decode(User.self, from: data) } |
so easy~ 與手動解析不同點在于:
1.移除了手寫init?方法。不需要手動解了
2.User實現Decodable協議,協議的定義如下:
1
2
3
4
5
6
7
8
9
10
|
/// A type that can decode itself from an external representation. public protocol Decodable { /// Creates a new instance by decoding from the given decoder. /// /// This initializer throws an error if reading from the decoder fails, or /// if the data read is corrupted or otherwise invalid. /// /// - Parameter decoder: The decoder to read data from. public init(from decoder: Decoder) throws } |
Decodable協議只有一個方法public init(from decoder: Decoder) throws
---以Decoder實例進行初始化,初始化失敗可能拋出異常。慶幸的是,只要繼承Decodable協議,系統會自動檢測類中的屬性進行初始化工作,省去了人工解析的麻煩~
3.使用了JSONDecoder。它是真正的解析工具,主導整個解析過程
讀到這里,是不是覺得人生從黑暗邁向了光明~~
可是,它并不完美...
JSONDecoder問題及方案
解析JSON經常遇到這樣兩種不一致問題:
- 服務端下發的key跟端上不一致。比如,服務端下發key="order_id",端上定義key="orderId"
- 服務端下發的日期表達是yyyy-MM-dd HH:mm或者時間戳,但端上是Date類型
- 服務端下發的基本類型和端上定義的不一致。服務端下發的是String,端上定義的Int,等
前兩個問題JSONDecoder都能很好地解決。
第一個key不一致問題,JSONDecoder有現成的方案。以上面介紹的例子來說,假設服務端返回的key是user_id而不是userId,那么我們可以使用JSONDecoder的CodingKeys像JSONModel一樣對屬性名稱在加解密時的名稱做轉換。User修改如下:
1
2
3
4
5
6
7
8
9
10
|
struct User: Decodable { var userId: Int? var name: String? var height: CGFloat? enum CodingKeys: String, CodingKey { case userId = "user_id" case name case height } } |
第二個,Date轉換問題。JSONDecoder也為我們提供了單獨的API:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
open class JSONDecoder { /// The strategy to use for decoding `Date` values. public enum DateDecodingStrategy { /// Defer to `Date` for decoding. This is the default strategy. case deferredToDate /// Decode the `Date` as a UNIX timestamp from a JSON number. case secondsSince1970 /// Decode the `Date` as UNIX millisecond timestamp from a JSON number. case millisecondsSince1970 /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). case iso8601 /// Decode the `Date` as a string parsed by the given formatter. case formatted(DateFormatter) /// Decode the `Date` as a custom value decoded by the given closure. case custom((Decoder) throws -> Date) } ...... /// The strategy to use in decoding dates. Defaults to `.deferredToDate`. open var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy } |
設置好了JSONDecoder屬性dateDecodingStrategy后,解析Date類型就會按照指定的策略進行解析。
類型不一致
至此,JSONDecoder為我們提供了
- 解析不同key值對象
- Date類型可自定義轉換
- Float在一些正負無窮及無值得特殊表示。(出現的概率很少,不作具體說明了)
但遇到基本類型端上與服務端不一致時(比如一個數字1,端上的Code是Int型,服務端下發String:"1"),JSONDecoder會拋出typeMismatch異常而終結整個數據的解析。
這讓人有點懊惱,端上的應用,我們希望它能夠盡可能穩定,而不是某些情況下遇到若干個基本類型不一致整個解析就停止,甚至是 Crash。
如下面表格所示,我們希望類型不匹配時,能夠這么處理:左列代表前端的類型,右列代表服務端類型,每一行代表前端類型為X時,能從服務端下發的哪些類型中轉化,比如String 可以從 IntorFloat轉化。這幾個類型基本能覆蓋日常服務端下發的數據,其它類型的轉化可根據自己的需求擴充。
前端 |
服務端 |
---|---|
String | Int,Float |
Float | String |
Double | String |
Bool | String, Int |
JSONDecoder沒有給我們便利的這種異常處理的API。如何解決呢?最直接的想法,在具體的model內實現init(decoder: Decoder)手動解析可以實現,但每個都這么處理太麻煩。
解決方案:KeyedDecodingContainer方法覆蓋
研究JSONDecoder的源碼,在解析自定義Model過程中,會發現這樣一個調用關系。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 入口方法 JSONDecoder decoder(type:Type data:Data) // 內部類,真實用來解析的 _JSONDecoder unbox(value:Any type:Type) // Model調用init方法 Decodable init(decoder: Decoder) // 自動生成的init方法調用container Decoder container(keyedBy:CodingKeys) // 解析的容器 KeyedDecodingContainer decoderIfPresent(type:Type) or decode(type:Type) // 內部類,循環調用unbox _JSONDecoder unbox(value:Any type:Type) ...循環,直到基本類型 |
最終的解析落到,_JSONDecoder的unbox 及 KeyedDecodingContainer的decoderIfPresent decode方法。但_JSONDecoder是內部類,我們處理不了。最終決定對KeyedDecodingContainer下手,其中部分代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
extension KeyedDecodingContainer { ....... /// Decode (Int, String) -> Int if possiable public func decodeIfPresent(_ type: Int.Type, forKey key: K) throws -> Int? { if let value = try ? decode(type, forKey: key) { return value } if let value = try ? decode(String.self, forKey: key) { return Int(value) } return nil } ....... /// Avoid the failure just when decoding type of Dictionary, Array, SubModel failed public func decodeIfPresent<T>(_ type: T.Type, forKey key: K) throws -> T? where T : Decodable { return try ? decode(type, forKey: key) } } |
上述代碼中,第一個函數decodeIfPresent(_ type: Int.Type, forKey key: K)
是以key的信息解析出Int?值。這里覆蓋了KeyedDecodingContainer中的該函數的實現,現在已try?的形式以Int類型解析,解析成功則直接返回,失敗則以String類型解析出一個StringValue,如果解析成功,再把String轉換成Int?值。
為什么要寫第二個函數呢?
場景:當我們Model內有其他的非基本類型的Model,比如其他自定義Model,Dictionary<String, Any>,Array<String>等,當這些Model 類型不匹配或者出錯誤時也會拋出異常,導致整個大Model解析失敗。
覆蓋decodeIfPresent<T>(_ type: T.Type, forKey key: K)
可以避免這些場景。至此,當類型過程中出現解析的Optional類型出現不匹配時,我們要不是通過轉換,要不就是給其賦值nil,避免了系統此時直接throw exception導致退出整個解析過程的尷尬。
為何不覆蓋decode方法?decodeIfPresent可以返回Optional值,decode返回確定類型值。考慮到如果Model內如果定義的類型是No-Optional型,那么可以認為開發者確定該值必須存在,如果不存在Model很可能是錯誤的,所以直接fail。
總結
Swift4.0 JSONDecoder確實為解析數據帶來了極大的便利。使用方式上類似Objective-C下的JSONModel。但實際開發中還是需要一些改造才能更好地服務于我們。
好了,以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對服務器之家的支持。
原文鏈接:https://juejin.im/post/5a928d2e5188257a7450d357