Swift 中的 JSON 反序列化

雲音樂技術團隊發表於2022-07-15
圖片來自:https://unsplash.com/photos/f...
本文作者:無帆

業界常用的幾種方案

手動解碼方案,如 Unbox(DEPRECATED)

Swift 早期普遍採用的方案,類似的還有 ObjectMapper

該方案需要使用者手動編寫解碼邏輯,使用成本比較高;目前已被 Swift 官方推出的 Codable 取代

示例:

struct User {
    let name: String
    let age: Int
}

extension User: Unboxable {
    init(unboxer: Unboxer) throws {
        self.name = try unboxer.unbox(key: "name")
        self.age = try unboxer.unbox(key: "age")
    }
}

阿里開源的 HandyJSON

HandyJSON 目前依賴於從 Swift Runtime 原始碼中推斷的記憶體規則,直接對記憶體進行操作。

在使用方面,不需要繁雜的定義,不需要繼承自 NSObject,宣告實現了協議即可

示例:

class Model: HandyJSON {
    var userId: String = ""
    var nickname: String = ""
    
    required init() {}
}

let jsonObject: [String: Any] = [
    "userId": "1234",
    "nickname": "lilei",
] 

let model = Model.deserialize(from: object)

但是存在相容和安全方面的問題,由於強依賴記憶體佈局規則,Swift 大版本升級時可能會有穩定性問題。同時由於要在執行時通過反射解析資料結構,會對效能有一定影響

基於 Sourcery 的超程式設計方案

Sourcery 是一款 Swift 程式碼生成器,使用 SourceKitten 解析 Swift 原始碼,根據 Stencil 模版生成最終程式碼

可定製能力非常強,基本可以滿足我們所有的需求

示例:

定義了 AutoCodable 協議,並且讓需要被解析的資料型別遵循該協議

protocol AutoCodable: Codable {}

class Model: AutoCodable {
    // sourcery: key = "userID"
    var userId: String = ""
    var nickname: String = ""
    
    required init(from decoder: Decoder) throws {
        try autoDecodeModel(from: decoder)
    }
}

之後通過 Sourcery 生成程式碼,這個過程 Sourcery 會掃描所有程式碼,對實現了 AutoCodable 協議的類/結構體自動生成解析程式碼

// AutoCodable.generated.swift
// MARK: - Model Codable
extension Model {
    enum CodingKeys: String, CodingKey {
        case userId = "userID"
        case nickname
    }

    // sourcery:inline:Model.AutoCodable
    public func autoDecodeModel(from decoder: Decoder) throws {
        // ...
    }
}

如上所示,還可以通過程式碼註釋(註解)來實現鍵值對映等自定義功能,但是需要對使用者有較強的規範要求。其次在元件化過程中需要對每個元件進行侵入/改造,內部團隊可以通過工具鏈解決,作為跨團隊通用方案可能不是太合適

Swift build-in API Codable

Swift 4.0 之後官方推出的 JSON 序列化方案,可以理解為 Unbox+Sourcery 的組合,編譯器會根據資料結構定義,自動生成編解碼邏輯,開發者使用特定的 Decoder/Encoder 對資料進行轉化處理。

Codable 作為 Swift 官方推出的方案,使用者可以無成本的接入。不過在具體實踐過程中,碰到了一些問題

  • Key 值對映不友好,例如以下情況:
// swift
struct User: Codable {
    var name: String
    var age: Int
    // ...
}

// json1
{
    "name": "lilei"
}

// json2
{
    "nickname": "lilei"
}

// json3
{
    "nickName": "lilei"
}

Swift 編譯器會自動幫我們生成完整的 CodingKeys,但是如果需要將 json 中的 nicknamenickName 解析為 User.name 時,需要重寫整個 CodingKeys,包括其他無關屬性如 age

  • 容錯處理能力不足、無法提供預設值

    Swift 設計初衷之一就是安全性,所以對於一些型別的強校驗從設計角度是合理的,不過對於實際使用者來說會增加一些使用成本

    舉個例子:

enum City: String, Codable {
    case beijing
    case shanghai
    case hangzhou
}

struct User: Codable {
    var name: String
    var city: City?
}

// json1
{
    "name": "lilei",
    "city": "hangzhou"
}

// json2
{
    "name": "lilei"
}

// json3
{
    "name": "lilei",
    "city": "shenzhen"
}

let decoder = JSONDecoder()

try {
    let user = try? decoder.decode(User.self, data: jsonData3)
}
catch {
    // json3 格式會進入該分支
    print("decode user error")
}

上述程式碼中,json1 和 json2 可以正確反序列化成 User 結構,json3 由於 “shenzhen” 無法轉化成 City,導致整個 User 結構解析失敗,而不是 name 解析成功,city 失敗後變成 nil
  • 巢狀結構解析繁瑣
  • JSONDecoder 只接受 data,不支援 dict,特殊場景使用時的型別轉化存在效能損耗

屬性裝飾器,如 BetterCodable

Swift 5.0 新增的語言特性,通過該方案可以補足原生 Codable 方案一些補足之處,比如支援預設值、自定義解析兜底策略等,具體原理也比較簡單,有興趣的可自行了解

示例:

struct UserPrivilege: Codable {
    @DefaultFalse var isAdmin: Bool
}

let json = #"{ "isAdmin": null }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

print(result) // UserPrivilege(isAdmin: false)

不過在實際編碼中,需要對資料結構的屬性顯式描述,增加了使用成本

各個方案優缺點對比

CodableHandyJSONBetterCodableSourcery
型別相容
支援預設值
鍵值對映
接入/使用成本
安全性
效能

上述方案都有各自的優缺點,基於此我們希望找到更適合雲音樂的方案。從使用接入和使用成本上來說,Codable 無疑是最佳選擇,關鍵點在於如何解決存在的問題

Codable 介紹

原理淺析

先看一組資料結構定義,該資料結構遵循 Codable 協議

enum Gender: Int, Codable {
    case unknown
    case male
    case female
}

struct User: Codable {
    var name: String
    var age: Int
    var gender: Gender
}

使用命令 swiftc main.swift -emit-sil | xcrun swift-demangle > main.sil 生成 SIL(Swift Intermediate Language),分析一下編譯器具體做了哪些事情

可以看到編譯器會自動幫我們生成 CodingKeys 列舉和 init(from decoder: Decoder) throws 方法

enum Gender : Int, Decodable & Encodable {
  case unknown
  case male
  case female
  init?(rawValue: Int)
  typealias RawValue = Int
  var rawValue: Int { get }
}

struct User : Decodable & Encodable {
  @_hasStorage var name: String { get set }
  @_hasStorage var age: Int { get set }
  @_hasStorage var gender: Gender { get set }
  enum CodingKeys : CodingKey {
    case name
    case age
    case gender
    @_implements(Equatable, ==(_:_:)) static func __derived_enum_equals(_ a: User.CodingKeys, _ b: User.CodingKeys) -> Bool
    func hash(into hasher: inout Hasher)
    init?(stringValue: String)
    init?(intValue: Int)
    var hashValue: Int { get }
    var intValue: Int? { get }
    var stringValue: String { get }
  }
  func encode(to encoder: Encoder) throws
  init(from decoder: Decoder) throws
  init(name: String, age: Int, gender: Gender)
}

下面摘錄了部分用於解碼的 SIL 片段,不熟悉的讀者可以跳過該部分,直接看後面轉譯過的虛擬碼

// User.init(from:)
sil hidden [ossa] @$s6source4UserV4fromACs7Decoder_p_tKcfC : $@convention(method) (@in Decoder, @thin User.Type) -> (@owned User, @error Error) {
// %0 "decoder"                                   // users: %83, %60, %8, %5
// %1 "$metatype"
bb0(%0 : $*Decoder, %1 : $@thin User.Type):
  %2 = alloc_box ${ var User }, var, name "self"  // user: %3
  %3 = mark_uninitialized [rootself] %2 : ${ var User } // users: %84, %61, %4
  %4 = project_box %3 : ${ var User }, 0          // users: %59, %52, %36, %23
  debug_value %0 : $*Decoder, let, name "decoder", argno 1, implicit, expr op_deref // id: %5
  debug_value undef : $Error, var, name "$error", argno 2 // id: %6
  %7 = alloc_stack [lexical] $KeyedDecodingContainer<User.CodingKeys>, let, name "container", implicit // users: %58, %57, %48, %80, %79, %33, %74, %73, %20, %69, %68, %12, %64
  %8 = open_existential_addr immutable_access %0 : $*Decoder to $*@opened("6CB1A110-E4DA-11EC-8A4C-8A05F3D75FB2") Decoder // users: %12, %12, %11
  %9 = metatype $@thin User.CodingKeys.Type
  %10 = metatype $@thick User.CodingKeys.Type     // user: %12
  %11 = witness_method $@opened("6CB1A110-E4DA-11EC-8A4C-8A05F3D75FB2") Decoder, #Decoder.container : <Self where Self : Decoder><Key where Key : CodingKey> (Self) -> (Key.Type) throws -> KeyedDecodingContainer<Key>, %8 : $*@opened("6CB1A110-E4DA-11EC-8A4C-8A05F3D75FB2") Decoder : $@convention(witness_method: Decoder) <τ_0_0 where τ_0_0 : Decoder><τ_1_0 where τ_1_0 : CodingKey> (@thick τ_1_0.Type, @in_guaranteed τ_0_0) -> (@out KeyedDecodingContainer<τ_1_0>, @error Error) // type-defs: %8; user: %12
  try_apply %11<@opened("6CB1A110-E4DA-11EC-8A4C-8A05F3D75FB2") Decoder, User.CodingKeys>(%7, %10, %8) : $@convention(witness_method: Decoder) <τ_0_0 where τ_0_0 : Decoder><τ_1_0 where τ_1_0 : CodingKey> (@thick τ_1_0.Type, @in_guaranteed τ_0_0) -> (@out KeyedDecodingContainer<τ_1_0>, @error Error), normal bb1, error bb5 // type-defs: %8; id: %12

bb1(%13 : $()):                                   // Preds: bb0
  %14 = metatype $@thin String.Type               // user: %20
  %15 = metatype $@thin User.CodingKeys.Type
  %16 = enum $User.CodingKeys, #User.CodingKeys.name!enumelt // user: %18
  %17 = alloc_stack $User.CodingKeys              // users: %22, %20, %67, %18
  store %16 to [trivial] %17 : $*User.CodingKeys  // id: %18
  // function_ref KeyedDecodingContainer.decode(_:forKey:)
  %19 = function_ref @$ss22KeyedDecodingContainerV6decode_6forKeyS2Sm_xtKF : $@convention(method) <τ_0_0 where τ_0_0 : CodingKey> (@thin String.Type, @in_guaranteed τ_0_0, @in_guaranteed KeyedDecodingContainer<τ_0_0>) -> (@owned String, @error Error) // user: %20
  try_apply %19<User.CodingKeys>(%14, %17, %7) : $@convention(method) <τ_0_0 where τ_0_0 : CodingKey> (@thin String.Type, @in_guaranteed τ_0_0, @in_guaranteed KeyedDecodingContainer<τ_0_0>) -> (@owned String, @error Error), normal bb2, error bb6 // id: %20

// %21                                            // user: %25
bb2(%21 : @owned $String):                        // Preds: bb1
  dealloc_stack %17 : $*User.CodingKeys           // id: %22
  %23 = begin_access [modify] [unknown] %4 : $*User // users: %26, %24
  %24 = struct_element_addr %23 : $*User, #User.name // user: %25
  assign %21 to %24 : $*String                    // id: %25
  end_access %23 : $*User                         // id: %26
  ...

大致上就是從 decoder 中獲取 container,在通過 decode 方法解析出具體的值,翻譯成對應的 Swift 程式碼如下:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: User.CodingKeys.Type)
    self.name = try container.decode(String.self, forKey: .name)
    self.age = try container.decode(Int.self, forKey: .age)
    self.gender = try container.decode(Gender.self, forKey: .gender)
}

由此可見反序列化中關鍵部分就在 Decoder 上,平常使用較多的 JSONDecoder 就是對 Decoder 協議的一種實現

編譯器自動生成的程式碼我們無法人工干預,如果想要讓反序列化結果達到我們的預期,需要定製化實現一個 Decoder

Swift 標準庫部分是開源的,有興趣的同學可移步 JSONDecoder.swift

Decoder、Container 協議

public protocol Decoder {
    var codingPath: [CodingKey] { get }
    var userInfo: [CodingUserInfoKey : Any] { get }
    func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey
    func unkeyedContainer() throws -> UnkeyedDecodingContainer
    func singleValueContainer() throws -> SingleValueDecodingContainer
}

Decoder 包含了 3 種型別的容器,具體關係如下

Untitled

容器需要實現各自的 decode 方法,進行具體的解析工作

KeyedDecodingContainerProtocol - 鍵值對字典容器協議(KeyedDecodingContainer 用於型別擦除)

func decodeNil(forKey key: Self.Key) throws -> Bool
func decode(_ type: Bool.Type, forKey key: Self.Key) throws -> Bool
func decode(_ type: String.Type, forKey key: Self.Key) throws -> String
...
func decodeIfPresent(_ type: Bool.Type, forKey key: Self.Key) throws -> Bool?
func decodeIfPresent(_ type: String.Type, forKey key: Self.Key) throws -> String?
...

SingleValueDecodingContainer - 單值容器協議

func decode(_ type: UInt8.Type) throws -> UInt8
...
func decode<T>(_ type: T.Type) throws -> T where T : Decodable

UnkeyedDecodingContainer - 陣列容器協議


mutating func decodeNil() throws -> Bool
mutating func decode(_ type: Int64.Type) throws -> Int64
mutating func decode(_ type: String.Type) throws -> String
...
mutating func decodeIfPresent(_ type: Bool.Type) throws -> Bool?
mutating func decodeIfPresent(_ type: String.Type) throws -> String?

典型的 JSONDecoder 使用姿勢

let data = ...
let decoder = JSONDecoder()
let user = try? decoder.decode(User.self, from: data)

解析流程如下:

Untitled

Decoder 的核心解析邏輯都在 Container 內部,下面會根據我們的需求,對該部分邏輯進行設計與實現

自研方案

功能設計

首先需要明確我們最終需要的效果

  1. 支援預設值
  2. 型別互相相容,如 JSON 中的 int 型別可以被正確的解析為 Model 中的 String 型別
  3. 解碼失敗允許返回 nil ,而不是直接判定解碼過程失敗
  4. 支援 key 對映
  5. 支援自定義解碼邏輯

這裡定義以下幾個協議

  • 預設值協議,預設實現了常見型別的預設值,自定義型別也可以按需實現
public protocol NECodableDefaultValue {
    static func codableDefaultValue() -> Self
}

extension Bool: NECodableDefaultValue {
    public static func codableDefaultValue() -> Self { false }
}
extension Int: NECodableDefaultValue {
    public static func codableDefaultValue() -> Self { 0 }
}
...
  • key 值對映協議
public protocol NECodableMapperValue {
    var mappingKeys: [String] { get }
}

extension String: NECodableMapperValue {
    public var mappingKeys: [String] {
        return [self]
    }
}

extension Array: NECodableMapperValue where Element == String {
    public var mappingKeys: [String] {
        return self
    }
}
  • Codable 協議擴充套件
public protocol NECodable: Codable {
    // key 值對映關係定義,類似 YYModel 功能
    static var modelCustomPropertyMapper: [String: NECodableMapperValue]? { get }
    
    // 除了 NECodableDefaultValue 返回的預設值,還可以在該函式中定義預設值
    static func decodingDefaultValue<CodingKeys: CodingKey>(for key: CodingKeys) -> Any?

    // 在解析完資料結構之後,提供二次修改的機會
    mutating func decodingCustomTransform(from jsonObject: Any, decoder: Decoder) throws -> Bool
}
  • 最終的使用姿勢
struct Model: NECodable {
    var nickName: String
    var age: Int
    
    static var modelCustomPropertyMapper: [String : NECodableMapperValue]? = [
        "nickName": ["nickname", "nickName"],
        "age": "userInfo.age"
    ]

    static func decodingDefaultValue<CodingKeys>(for key: CodingKeys) -> Any? where CodingKeys : CodingKey {
        guard let key = key as? Self.CodingKeys else { return nil }
        switch key {
        case .age:
            // 提供預設年齡
            return 18
        default:
            return nil
        }
    }
}

let jsonObject: [String: Any] = [
    "nickname": "lilei",
    "userInfo": [
        "age": 123
    ],
]

let model = try NEJSONDecoder().decode(Model.self, jsonObject: jsonObject)

XCTAssert(model.nickName == "lilei")
XCTAssert(model.age == 123)

Decoder、Container 具體實現

定義類 NEJSONDecoder 作為 Decoder 協議的具體實現,同時還要實現三個容器協議

在容器內部需要實現大量的 decode 方法用於解析具體值,我們可以抽象一個工具類,進行相應的型別解析、轉換、提供預設值等功能

下面給出一部分 keyedContainer 實現,大致流程如下:

  1. 先呼叫的 entry 方法,該方法根據 key、keyMapping 從 JSON 中獲取原始值
  2. 通過 unbox 方法,將原始值(可能是 String、Int 型別)轉化成預期型別(比如 Bool)
  3. 如果上述過程失敗,則進入預設值處理流程

    1. 首先通過模型定義的 decodingDefaultValue 方法獲取預設值,如果未獲取到進行步驟 b
    2. 通過 NECodableDefaultValue 協議獲取型別的預設值
  4. 解析完成
class NEJSONKeyedDecodingContainer<K : CodingKey> : KeyedDecodingContainerProtocol {
        public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
        do {
            return try _decode(type, forKey: key)
        }
        catch {
            if let value = self.defaultValue(for: key),
               let unbox = try? decoder.unbox(value, as: Bool.self) { return unbox }
            
            if self.provideDefaultValue {
                return Bool.codableDefaultValue()
            }
            throw error
        }
    }

        public func _decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
        guard let entry = self.entry(for: key) else {
            throw ...
        }

        self.decoder.codingPath.append(key)
        defer { self.decoder.codingPath.removeLast() }

        guard let value = try self.decoder.unbox(entry, as: Bool.self) else {
            throw ...
        }

        return value
    }
}

再議 PropertyWrapper

NECodable 協議中,保留了 YYModel 的使用習慣,key 對映以及預設值提供需要單獨實現 NECodable 協議的兩個方法

而利用 Swift 的屬性裝飾器,可以讓開發者更加便捷的實現上述功能:

@propertyWrapper
class NECodingValue<Value: Codable>: Codable {
    public convenience init(wrappedValue: Value) {
        self.init(storageValue: wrappedValue, keys: nil)
    }
    
    public convenience init(wrappedValue: Value, keys: String...) {
        self.init(storageValue: wrappedValue, keys: keys)
    }
    
    public convenience init<T>(wrappedValue: Optional<T> = .none, keys: String...) where Value == Optional<T> {
        self.init(storageValue: wrappedValue, keys: [])
    }
    
    public convenience init(keys: String...) {
        self.init(keys: keys)
    }

    // ....
}

struct Model: NECodable {
    @NECodingValue(keys: "nickname")
    var name: String

    // JSON 中不存在時,預設為 hangzhou
    @NECodingValue
    var city: String = "hangzhou"

    // JSON 中不存在時,預設為 false
    var enable: Bool
}

實現方式比較取巧:

通過屬性修飾器包裝例項變數,NECodingValue(keys: "nickname") 例項最先被初始化,其中包含我們定義的 keyswrapperValue,而後的 init(from decoder: Decoder) 過程又通過 decoder 生成 NECodingValue(from: decoder) 變數並賦值給 _name 屬性,此時第一個 NECodingValue 變數就會被釋放,從而獲得了一個程式碼執行時機,用來進行定製的解碼流程(將 defaultValue 複製過來,使用自定義的 key 進行解碼等等…)

應用場景示例

反序列化通常用於處理服務端返回的資料,基於 Swift 的語法特性,我們可以非常簡單的定義一個網路請求協議,舉個例子:

網路請求協議

protocol APIRequest {
    associatedtype Model

    var path: String { get }
    var parameters: [String: Any]? { get }
    
    static func parse(_ data: Any) throws -> Model
}

// 預設實現
extension APIRequest {
    var parameters: [String: Any]? { nil }

    static func parse(_ data: Any) throws -> Model {
        throw APIError.dataExceptionError()
    }
}

擴充套件 APIRequest 協議,通過 Swift 的型別匹配模式,自動進行反序列化

extension APIRequest where Model: NECodable {
    static func parse(_ data: Any) throws -> Model {
        let decoder = NEJSONDecoder()
        return try decoder.decode(Model.self, jsonObject: data)
    }
}

擴充套件 APIRequest 協議,增加網路請求方法

extension APIRequest {
    @discardableResult
    func start(completion: @escaping (Result<Model, APIError>) -> Void) -> APIToken<Self> {
        // 具體的網路請求流程,基於底層網路庫實現
    }
}

最終業務側可以非常簡單的定義一個網路介面,併發起請求

// 網路介面定義
struct MainRequest: APIRequest {
    struct Model: NECodable {
        struct Item: NECodable {
            var title: String
        }
        var items: [Item]
        var page: Int
    }

    let path = "/api/main"
}

// 業務側發起網路請求
func doRequest() {
    MainRequest().start { result in
        switch result {
            case .success(let model):
                // to do something
                print("page index: \(model.page)")
            case .failure(let error):
                HUD.show(error: error)
        }
    }
}

單元測試

序列化/反序列化過程會存在很多邊界情況,需要針對各場景構造單元測試,確保所有行為符合預期

效能對比

Untitled

上圖是各反序列化庫執行 10000 次後得到的結果,可能看到從 Data 資料轉換為 Model 時 JSONDecoder 效能最佳,從 JSON Object 傳換為 Model 時 NEJSONDecoder 效能最佳,HandyJSON 耗時均最長

測試程式碼:test.swift

本文釋出自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!

相關文章