图片来自: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 中的 nickname 或 nickName 解析为 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 种类型的容器,详细关系如下
容器需求完成各自的 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)
解析流程如下:
Decoder 的核心解析逻辑都在 Container 内部,下面会根据咱们的需求,对该部分逻辑进行规划与完成
自研计划
功用规划
首要需求明确咱们最终需求的效果
- 支撑默许值
- 类型互相兼容,如 JSON 中的 int 类型能够被正确的解析为 Model 中的 String 类型
- 解码失败答应回来
nil
,而不是直接判定解码过程失败 - 支撑 key 映射
- 支撑自界说解码逻辑
这里界说以下几个协议
-
默许值协议,默许完成了常见类型的缺省值,自界说类型也能够按需完成
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 完成,大致流程如下:
- 先调用的
entry
办法,该办法根据 key、keyMapping 从 JSON 中获取原始值 - 经过
unbox
办法,将原始值(或许是 String、Int 类型)转化成预期类型(比方 Bool) - 假如上述过程失败,则进入默许值处理流程
- 首要经过模型界说的
decodingDefaultValue
办法获取默许值,假如未获取到进行步骤 b - 经过
NECodableDefaultValue
协议获取类型的默许值
- 首要经过模型界说的
- 解析完成
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")
实例最早被初始化,其中包含咱们界说的 keys、wrapperValue,而后的 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!