淺談 Swift JSON 解析

主流 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é)議,可以配合 JSONDecoderJSONEncoder 用來(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ì)于 enumBool 來(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ù)源碼,分別是解碼 BoolInt 類(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ě) decodeIfPresentdecode 方法,捕獲異常并處理。如果是可選類(lèi)型則將異常拋出改為返回 nil,如果是不可選類(lèi)型則返回默認(rèn)值。

缺點(diǎn):

  1. 只能在當(dāng)前模塊使用,不支持跨模塊。
  2. 不支持不可選枚舉的解析。
  3. 對(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

  • SingleValueDecodingContainerKeyedDecodingContainerProtocol 協(xié)議方法中通過(guò) KeyNotFoundDecodingStrategyValueNotFoundDecodingStrategy 兩種策略處理異常,并通過(guò) JSONAdapter 協(xié)議提供自定義適配方法。

  • 對(duì)于枚舉這種無(wú)法確定默認(rèn)值的類(lèi)型,提供一個(gè) CaseDefaultable 協(xié)議,然后重寫(xiě) init(from decoder: Decoder) 方法來(lái)處理異常。

解決了什么

  1. 類(lèi)型不匹配的時(shí)候不會(huì)拋出異常而是根據(jù)是否可選返回 nil 或者默認(rèn)值
  2. 提供了在異常時(shí)自定義解碼的策略
  3. 減少了大量的重復(fù)勞動(dòng)和可選綁定
  4. 提高容錯(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ì)比

性能

image

以上是對(duì)不同數(shù)量級(jí)的數(shù)據(jù)解析對(duì)比。數(shù)據(jù)結(jié)構(gòu)越復(fù)雜,性能差距會(huì)更大。

倉(cāng)庫(kù)地址:https://github.com/Pircate/JSONParsePerformance

代碼量

JSONSerialization

image

CleanJSON

image

HandyJSON

image

ObjectMapper

image

總結(jié)

可以看到 JSONSerialization 速度是最快的,但同時(shí)也是代碼量最多的,容錯(cuò)處理最差的。CleanJSONObjectMapper 速度不相上下,但 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.

參考文檔

  1. Swift Json解析探索 作者:桃紅宿雨
  2. Swift 標(biāo)準(zhǔn)庫(kù)源碼 作者:apple
  3. Codable vs. JSONSerialization Performance 作者:Mattt
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容