图片来自: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 Codableextension 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 值映射不敌对,例如以下状况:
// swiftstruct 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: %12bb1(%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: %25bb2(%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 种类型的容器,具体关系如下

容器须要实现各自的 decode 办法,进行具体的解析工作

KeyedDecodingContainerProtocol - 键值对字典容器协定(KeyedDecodingContainer 用于类型擦除)

func decodeNil(forKey key: Self.Key) throws -> Boolfunc decode(_ type: Bool.Type, forKey key: Self.Key) throws -> Boolfunc 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 -> Boolmutating func decode(_ type: Int64.Type) throws -> Int64mutating 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)

解析流程如下:

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 的属性装璜器,能够让开发者更加便捷的实现上述性能:

@propertyWrapperclass 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)        }    }}

单元测试

序列化/反序列化过程会存在很多边界状况,须要针对各场景结构单元测试,确保所有行为合乎预期

性能比照

上图是各反序列化库执行 10000 次后失去的后果,可能看到从 Data 数据转换为 Model 时 JSONDecoder 性能最佳,从 JSON Object 传换为 Model 时 NEJSONDecoder 性能最佳,HandyJSON 耗时均最长

测试代码:test.swift

本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!