图片来自:unsplash.com/photos/fvdd…

本文作者:无帆

业界常用的几种计划

手动解码计划,如 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)

不过在实践编码中,需求对数据结构的特点显式描绘,添加了运用本钱

各个计划优缺点比照

Codable HandyJSON BetterCodable Sourcery
类型兼容
支撑默许值
键值映射
接入/运用本钱
安全性
功用

上述计划都有各自的优缺点,根据此咱们希望找到更适合云音乐的计划。从运用接入和运用本钱上来说,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!