淺談 Swift JSON 解析

主流 JSON 解析框架

  • SwiftyJSON Github 上 Star 最多的 Swift JSON 解析框架git

  • ObjectMapper 面向協議的 Swift JSON 解析框架github

  • HandyJSON 阿里推出的一個用於 Swift 語言中的 JSON 序列化/反序列化庫。swift

  • JSONDecoder Apple 官方推出的基於 Codable 的 JSON 解析類數組

我的分析

SwiftyJSON 採用下標方式獲取數據,使用起來比較麻煩,還容易發生拼寫錯誤、維護困難等問題。服務器

ObjectMapper 使用上相似 Codable,可是須要額外寫 map 方法,重複勞動過多。微信

HandyJSON 使用上相似於 YYModel,採用的是 Swift 反射 + 內存賦值的方式來構造 Model 實例。可是有內存泄露,兼容性差等問題。數據結構

Codable 是 Apple 官方提供的,更可靠,對原生類型支持更好。app

Codable 簡介

Codable 是 Swift 4.0 之後推出的一個編解碼協議,能夠配合 JSONDecoderJSONEncoder 用來進行 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)

// 蛇形命名轉駝峯
decoder.keyDecodingStrategy = .convertFromSnakeCase

// 日期解析使用 UNIX 時間戳
decoder.dateDecodingStrategy = .secondsSince1970
複製代碼

Codable 痛點

只要有一個屬性解析失敗則直接拋出異常致使整個解析過程失敗。ide

如下狀況均會解析失敗:

  • 類型不匹配,例如 APP 端是 Int 類型,服務器下發的是 String 類型
  • 不可選類型鍵不存在, 例如服務器下發的數據缺乏了某個字段
  • 不可選類型值爲 null,例如服務器因爲某種緣由致使數據爲 null

後兩個能夠經過使用可選類型避免,第一種狀況只能重寫協議方法來規避,可是很難徹底避免。而使用可選類型勢必會有大量的可選綁定,對於 enumBool 來講使用可選類型是很是痛苦的,並且這些都會增長代碼量。這時候就須要一種解決方案來解決這些痛點。

JSONDecoder 內部實現

調用關係

// 入口方法
JSONDecoder decode<T : Decodable>(_ type: T.Type, from data: Data)
    // 內部私有類,實際用來解析的
    __JSONDecoder unbox<T : Decodable>(_ value: Any, as type: T.Type)
        // 遵循 Decodable 協議的類調用協議方法
        Decodable init(from decoder: Decoder)
            // 自動生成的 init 方法調用 container
            Decoder container(keyedBy: CodingKeys) 
                // 解析的容器
                KeyedDecodingContainer decodeIfPresent(type: Type) or decode(type: Type)
                    // 內部私有類,循環調用 unbox
                    __JSONDecoder unbox(value:Any type:Type)
                        ...循環,直到基本類型
複製代碼

JSONDecoder 內部其實是使用 __JSONDecoder 這個私有類來進行解碼的,最終都是調用 unbox 方法。

解碼機制

如下代碼摘自 Swift 標準庫源碼,分別是解碼 BoolInt 類型,能夠看到一旦解析失敗直接拋出異常,沒有容錯機制。

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 是內部私有類,而 Decoder 協議暴露的接口太少,鑑於 Swift protocol extension 優先使用當前模塊的協議方法,因此能夠從 KeyedDecodingContainer 協議下手。

所以 初版解決方案 誕生了。經過擴展 KeyedDecodingContainer 協議,重寫 decodeIfPresentdecode 方法,捕獲異常並處理。若是是可選類型則將異常拋出改成返回 nil,若是是不可選類型則返回默認值。

缺點:

  1. 只能在當前模塊使用,不支持跨模塊。
  2. 不支持不可選枚舉的解析。
  3. 對於數組若是有一個出錯只能解析爲空數組,除非經過反射處理。

最終解決方案 CleanJSON

繼承自 JSONDecoder,在標準庫源碼基礎上作了改動,以解決 JSONDecoder 各類解析失敗的問題,如鍵值不存在,值爲 null,類型不一致。

實現原理

  • 從標準庫複製一份源碼

  • 在最底層的 unbox 方法裏面將異常拋出改成返回 nil

  • SingleValueDecodingContainerKeyedDecodingContainerProtocol 協議方法中經過 KeyNotFoundDecodingStrategyValueNotFoundDecodingStrategy 兩種策略處理異常,並經過 JSONAdapter 協議提供自定義適配方法。

  • 對於枚舉這種沒法肯定默認值的類型,提供一個 CaseDefaultable 協議,而後重寫 init(from decoder: Decoder) 方法來處理異常。

解決了什麼

  1. 類型不匹配的時候不會拋出異常而是根據是否可選返回 nil 或者默認值
  2. 提供了在異常時自定義解碼的策略
  3. 減小了大量的重複勞動和可選綁定
  4. 提升容錯率,能夠放心的使用不可選類型而不用擔憂解析失敗

用法

JSONDecoder 替換成 CleanJSONDecoder 便可。

let decoder = CleanJSONDecoder()
try decoder.decode(Foo.self, from: data)
複製代碼

對於不可選的枚舉類型請遵循 CaseDefaultable 協議,若是解析失敗會返回默認 case

NOTE:枚舉使用強類型解析,關聯類型和數據類型不一致不會進行類型轉換,會解析爲默認 case

enum Enum: Int, Codable, CaseDefaultable {
    
    case case1
    case case2
    case case3
    
    static var defaultCase: Enum {
        return .case1
    }
}
複製代碼

自定義解碼策略

能夠經過 valueNotFoundDecodingStrategy 在值爲 null 或類型不匹配的時候自定義解碼。

struct CustomAdapter: JSONAdapter {
    
    // 因爲 Swift 布爾類型不是非 0 即 true,因此默認沒有提供類型轉換。
    // 若是想實現 Int 轉 Bool 能夠自定義解碼。
    func adapt(_ decoder: CleanDecoder) throws -> Bool {
        // 值爲 null
        if decoder.decodeNil() {
            return false
        }
        
        if let intValue = try decoder.decodeIfPresent(Int.self) {
            // 類型不匹配,指望 Bool 類型,實際是 Int 類型
            return intValue != 0
        }
        
        return false
    }
}

decoder.valueNotFoundDecodingStrategy = .custom(CustomAdapter())
複製代碼

各項對比

性能

image

以上是對不一樣數量級的數據解析對比。數據結構越複雜,性能差距會更大。

倉庫地址:github.com/Pircate/JSO…

代碼量

JSONSerialization

image

CleanJSON

image

HandyJSON

image

ObjectMapper

image

總結

能夠看到 JSONSerialization 速度是最快的,但同時也是代碼量最多的,容錯處理最差的。CleanJSONObjectMapper 速度不相上下,但 ObjectMapper 代碼量較多,且對不可選類型的解析和 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.



做者:Pircate
連接:https://www.jianshu.com/p/1f194f09599a
來源:簡書

小編這呢,給你們推薦一個優秀的iOS交流平臺,平臺裏的夥伴們都是很是優秀的iOS開發人員,咱們專一於技術的分享與技巧的交流,你們能夠在平臺上討論技術,交流學習。歡迎你們的加入(想要進入的可加小編微信)。 18173184032

相關文章
相關標籤/搜索