主流 JSON 解析框架
SwiftyJSON Github 上 Star 最多的 Swift JSON 解析框架
ObjectMapper 面向協(xié)議的 Swift JSON 解析框架
HandyJSON 阿里推出的一個(gè)用于 Swift 語(yǔ)言中的 JSON 序列化/反序列化庫(kù)。
JSONDecoder Apple 官方推出的基于
Codable的 JSON 解析類(lèi)
個(gè)人分析
SwiftyJSON 采用下標(biāo)方式獲取數(shù)據(jù),使用起來(lái)比較麻煩,還容易發(fā)生拼寫(xiě)錯(cuò)誤、維護(hù)困難等問(wèn)題。
ObjectMapper 使用上類(lèi)似 Codable,但是需要額外寫(xiě) map 方法,重復(fù)勞動(dòng)過(guò)多。
HandyJSON 使用上類(lèi)似于 YYModel,采用的是 Swift 反射 + 內(nèi)存賦值的方式來(lái)構(gòu)造 Model 實(shí)例。但是有內(nèi)存泄露,兼容性差等問(wèn)題。
Codable 是 Apple 官方提供的,更可靠,對(duì)原生類(lèi)型支持更好。
Codable 簡(jiǎn)介
Codable 是 Swift 4.0 以后推出的一個(gè)編解碼協(xié)議,可以配合 JSONDecoder 和 JSONEncoder 用來(lái)進(jìn)行 JSON 解碼和編碼。
Codable 使用方法
struct Foo: Codable {
let bar: Int
enum CodingKeys: String, CodingKey {
// key 映射
case bar = "rab"
}
init(from decoder: Decoder) throws {
// 自定義解碼
let container = try decoder.container(keyedBy: CodingKeys.self)
if let intValue = try container.decodeIfPresent(String.self, forKey: .bar) {
self.bar = intValue
} else {
self.bar = try container.decode(Int.self, forKey: .bar)
}
}
let decoder = JSONDecoder()
try decoder.decode(Foo.self, from: data)
// 蛇形命名轉(zhuǎn)駝峰
decoder.keyDecodingStrategy = .convertFromSnakeCase
// 日期解析使用 UNIX 時(shí)間戳
decoder.dateDecodingStrategy = .secondsSince1970
Codable 痛點(diǎn)
只要有一個(gè)屬性解析失敗則直接拋出異常導(dǎo)致整個(gè)解析過(guò)程失敗。
以下情況均會(huì)解析失敗:
- 類(lèi)型不匹配,例如 APP 端是 Int 類(lèi)型,服務(wù)器下發(fā)的是 String 類(lèi)型
- 不可選類(lèi)型鍵不存在, 例如服務(wù)器下發(fā)的數(shù)據(jù)缺少了某個(gè)字段
- 不可選類(lèi)型值為 null,例如服務(wù)器由于某種原因?qū)е聰?shù)據(jù)為 null
后兩個(gè)可以通過(guò)使用可選類(lèi)型避免,第一種情況只能重寫(xiě)協(xié)議方法來(lái)規(guī)避,但是很難完全避免。而使用可選類(lèi)型勢(shì)必會(huì)有大量的可選綁定,對(duì)于 enum 和 Bool 來(lái)說(shuō)使用可選類(lèi)型是非常痛苦的,而且這些都會(huì)增加代碼量。這時(shí)候就需要一種解決方案來(lái)解決這些痛點(diǎn)。
JSONDecoder 內(nèi)部實(shí)現(xiàn)
調(diào)用關(guān)系
// 入口方法
JSONDecoder decode<T : Decodable>(_ type: T.Type, from data: Data)
// 內(nèi)部私有類(lèi),實(shí)際用來(lái)解析的
__JSONDecoder unbox<T : Decodable>(_ value: Any, as type: T.Type)
// 遵循 Decodable 協(xié)議的類(lèi)調(diào)用協(xié)議方法
Decodable init(from decoder: Decoder)
// 自動(dòng)生成的 init 方法調(diào)用 container
Decoder container(keyedBy: CodingKeys)
// 解析的容器
KeyedDecodingContainer decodeIfPresent(type: Type) or decode(type: Type)
// 內(nèi)部私有類(lèi),循環(huán)調(diào)用 unbox
__JSONDecoder unbox(value:Any type:Type)
...循環(huán),直到基本類(lèi)型
JSONDecoder 內(nèi)部實(shí)際上是使用 __JSONDecoder 這個(gè)私有類(lèi)來(lái)進(jìn)行解碼的,最終都是調(diào)用 unbox 方法。
解碼機(jī)制
以下代碼摘自 Swift 標(biāo)準(zhǔn)庫(kù)源碼,分別是解碼 Bool 和 Int 類(lèi)型,可以看到一旦解析失敗直接拋出異常,沒(méi)有容錯(cuò)機(jī)制。
fileprivate func unbox(_ value: Any, as type: Bool.Type) throws -> Bool? {
guard !(value is NSNull) else { return nil }
if let number = value as? NSNumber {
// TODO: Add a flag to coerce non-boolean numbers into Bools?
if number === kCFBooleanTrue as NSNumber {
return true
} else if number === kCFBooleanFalse as NSNumber {
return false
}
/* FIXME: If swift-corelibs-foundation doesn't change to use NSNumber, this code path will need to be included and tested:
} else if let bool = value as? Bool {
return bool
*/
}
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}
fileprivate func unbox(_ value: Any, as type: Int.Type) throws -> Int? {
guard !(value is NSNull) else { return nil }
guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
}
let int = number.intValue
guard NSNumber(value: int) == number else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
}
return int
}
解決方案
由于 __JSONDecoder 是內(nèi)部私有類(lèi),而 Decoder 協(xié)議暴露的接口太少,鑒于 Swift protocol extension 優(yōu)先使用當(dāng)前模塊的協(xié)議方法,所以可以從 KeyedDecodingContainer 協(xié)議下手。
因此 第一版解決方案 誕生了。通過(guò)擴(kuò)展 KeyedDecodingContainer 協(xié)議,重寫(xiě) decodeIfPresent 和 decode 方法,捕獲異常并處理。如果是可選類(lèi)型則將異常拋出改為返回 nil,如果是不可選類(lèi)型則返回默認(rèn)值。
缺點(diǎn):
- 只能在當(dāng)前模塊使用,不支持跨模塊。
- 不支持不可選枚舉的解析。
- 對(duì)于數(shù)組如果有一個(gè)出錯(cuò)只能解析為空數(shù)組,除非通過(guò)反射處理。
最終解決方案 CleanJSON
繼承自 JSONDecoder,在標(biāo)準(zhǔn)庫(kù)源碼基礎(chǔ)上做了改動(dòng),以解決 JSONDecoder 各種解析失敗的問(wèn)題,如鍵值不存在,值為 null,類(lèi)型不一致。
實(shí)現(xiàn)原理
從標(biāo)準(zhǔn)庫(kù)復(fù)制一份源碼
在最底層的
unbox方法里面將異常拋出改為返回nil在
SingleValueDecodingContainer和KeyedDecodingContainerProtocol協(xié)議方法中通過(guò)KeyNotFoundDecodingStrategy和ValueNotFoundDecodingStrategy兩種策略處理異常,并通過(guò)JSONAdapter協(xié)議提供自定義適配方法。對(duì)于枚舉這種無(wú)法確定默認(rèn)值的類(lèi)型,提供一個(gè)
CaseDefaultable協(xié)議,然后重寫(xiě)init(from decoder: Decoder)方法來(lái)處理異常。
解決了什么
- 類(lèi)型不匹配的時(shí)候不會(huì)拋出異常而是根據(jù)是否可選返回
nil或者默認(rèn)值 - 提供了在異常時(shí)自定義解碼的策略
- 減少了大量的重復(fù)勞動(dòng)和可選綁定
- 提高容錯(cuò)率,可以放心的使用不可選類(lèi)型而不用擔(dān)心解析失敗
用法
將 JSONDecoder 替換成 CleanJSONDecoder 即可。
let decoder = CleanJSONDecoder()
try decoder.decode(Foo.self, from: data)
對(duì)于不可選的枚舉類(lèi)型請(qǐng)遵循 CaseDefaultable 協(xié)議,如果解析失敗會(huì)返回默認(rèn) case
NOTE:枚舉使用強(qiáng)類(lèi)型解析,關(guān)聯(lián)類(lèi)型和數(shù)據(jù)類(lèi)型不一致不會(huì)進(jìn)行類(lèi)型轉(zhuǎn)換,會(huì)解析為默認(rèn) case
enum Enum: Int, Codable, CaseDefaultable {
case case1
case case2
case case3
static var defaultCase: Enum {
return .case1
}
}
自定義解碼策略
可以通過(guò) valueNotFoundDecodingStrategy 在值為 null 或類(lèi)型不匹配的時(shí)候自定義解碼。
struct CustomAdapter: JSONAdapter {
// 由于 Swift 布爾類(lèi)型不是非 0 即 true,所以默認(rèn)沒(méi)有提供類(lèi)型轉(zhuǎn)換。
// 如果想實(shí)現(xiàn) Int 轉(zhuǎn) Bool 可以自定義解碼。
func adapt(_ decoder: CleanDecoder) throws -> Bool {
// 值為 null
if decoder.decodeNil() {
return false
}
if let intValue = try decoder.decodeIfPresent(Int.self) {
// 類(lèi)型不匹配,期望 Bool 類(lèi)型,實(shí)際是 Int 類(lèi)型
return intValue != 0
}
return false
}
}
decoder.valueNotFoundDecodingStrategy = .custom(CustomAdapter())
各項(xiàng)對(duì)比
性能
以上是對(duì)不同數(shù)量級(jí)的數(shù)據(jù)解析對(duì)比。數(shù)據(jù)結(jié)構(gòu)越復(fù)雜,性能差距會(huì)更大。
倉(cāng)庫(kù)地址:https://github.com/Pircate/JSONParsePerformance
代碼量
JSONSerialization
CleanJSON
HandyJSON
ObjectMapper
總結(jié)
可以看到 JSONSerialization 速度是最快的,但同時(shí)也是代碼量最多的,容錯(cuò)處理最差的。CleanJSON 和 ObjectMapper 速度不相上下,但 ObjectMapper 代碼量較多,且對(duì)不可選類(lèi)型的解析和 JSONDecoder 一樣解析失敗直接拋出異常。HandyJSON 性能較差。
引用 Mattt 大神的分析:
On average, Codable with JSONDecoder is about half as fast as the equivalent implementation with JSONSerialization.
But does this mean that we shouldn’t use Codable? Probably not.
A 2x speedup factor may seem significant, but measured in absolute time difference, the savings are unlikely to be appreciable under most circumstances — and besides, performance is only one consideration in making a successful app.