详解 Codable 的用法和原理

Codable 是什么

Codable 本身就是个类型别名

typealias Codable = Decodable & Encodable

代表一个同时符合 Decodable 和 Encodable 协议的类型,即可解码且可编码的类型。

Codable 也可以代表苹果为 Swift 开发的一套编解码系统,从 Swift 4 开始引入,包含了 Encoder 和 Decoder 协议和他们的两个实现 JSONEncoderJSONDecoderPropertyListEncoderPropertyListDecoder。其中 Codable 及其相关协议放在了标准库中,而具体的 Encoder、Decoder 类放在了 Foundation 框架中。

Codable 的用法

Codable 是用来做系统自身数据结构和外部公共数据结构做转换的。系统内部数据结构可以是基础类型、结构体、枚举、类等,外部公共数据结构可以是 JSON、XML 等。

JSON 和 模型的相互转换

用 Objective-C 做 JSON 和模型转换时,一般要使用一些第三方库,这些第三方库基本上都是利用了 Objective-C Runtime 的强大特性来实现 JSON 和模型互转的。

但是 Swift 是一门静态语言,本身是没有像 Objective-C 那样的动态 Runtime 的。虽然在 Swift 中也可以通过继承 NSObject 的方式,来使用基于 OC Runtime 的 JSON 模型互转方案。但是这样就很不 Swift,也放弃了 Swift 作为一门静态语言的高性能,等于说自己降低了整个项目的运行性能,这是无法忍受的。

好在苹果提供了 JSONEncoderJSONDecoder 这两个结构体来方便得在 JSON 数据和自定义模型之间互相转换。苹果可以利用一些系统私有的机制来实现转换,而不需要通过 OC Runtime

只要让自己的数据类型符合 Codable 协议,就可以用系统提供的编解码器进行编解码。

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

具体编解码代码如下:

解码(JSON Data -> Model):

let json = """
 {
 "name": "zhangsan",
 "age": 25
 }
 """.data(using: .utf8)!
let user = JSONDecoder().decode(User.self, from: json)

编码(Model -> JSON Data):

let data = JSONEncoder().encode(user)

Codable 支持的数据类型

基础数据类型

在 Swift 标准库的声明文件中可以看到,基础类型都通过 extension 实现了 Codable 协议。

对于基础类型的属性,JSONEncoder 和 JSONDecoder 都可以正确的处理。

Date

JSONEncoder 提供了 dateEncodingStrategy 属性来指定日期编码策略。
同样 JSONDecoder 提供了 dateDecodingStrategy 属性。

就拿 dateDecodingStrategy 为例,它是一个枚举类型。枚举类型有以下几个 case:

case 名作用
case deferredToDate默认的 case
case iso8601按照日期的 ios8601 标准来解码日期
case formatted(DateFormatter)自定义日期解码策略,需要提供一个 DateFormatter 对象
case custom((_ decoder: Decoder) throws -> Date)自定义日期解码策略,需要提供一个 Decoder -> Date 的闭包

通常使用比较多的就是 .iso8601 了,因为后端返回日期通常都是已 ios8601 格式返回的。只要 JSON 中的日期是 ios8601 规范的字符串,只要设置一行代码就能让 JSONDecoder 完成日期的解码。

struct User: Codable {
 var name: String
 var age: Int
 var birthday: Date
}
let json = """
 {
 "name": "zhangsan",
 "age": 25,
 "birthday": "2022-09-12T10:25:41+00:00"
 }
 """.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let user = decoder.decode(User.self, from: json)
// user.birthday 正确解码为 Date 类型

嵌套对象

在自定义模型中嵌套对象的时候,只要这个嵌套对象也符合 Codable 协议,那整个对象就可以正常使用 JSONEncoderJSONDecoder 编解码。

struct UserInfo: Codable {
 var name: String
 var age: Int
}
struct User: Codable {
 var info: UserInfo
}

枚举

枚举类型必须它的 RawValue 的类型是可解码的,并且 RawValue 的类型和 JSON 字段类型对应,即可正确解码。

自定义 CodingKeys

自定义 CodingKeys 主要是两个目的

  1. 当数据类型属性名和 JSON 中字段名不同时,做 key 的映射。
  2. 通过在不添加某些字段的 case,来跳过某些字段的编解码过程。
struct User: Codable {
 var name: String
 var age: Int
 var birthday: Date?
 enum CodingKeys: String, CodingKey {
 case name = "userName"
 case age = "userAge"
 }
}

CodingKeys 必须是一个 RawValue 为 String 类型的枚举,并符合 CodingKey 协议。以上代码实现的效果为,为 name 和 age 字段做了 key 映射,让编解码过程中不包含 birthday 字段。

Codable 的原理

了解了 Codable 的用法,下面我们来看一看 Codable 的原理。

Decodable 协议

由于编码和解码的原理差不多只是方向不同,我们仅探索用的更多的解码过程。

如果想让一个对象支持解码应该怎么做呢,当然是符合 Decodable 协议。我们先看看一个对象符合 Decodable 协议需要做哪些事情。

Decodable 协议的定义如下:

public protocol Decodable {
 init(from decoder: Decoder) throws
}

也就是说只要实现一个传入 Decoder 参数的初始化方法,于是我们自己来实现 User。

struct User: Decodable {
 var name: String
 var age: Int
 init(from decoder: Decoder) throws {
 }
}

现在要来看看怎样让 User 的两个属性的值能从 Decoder 这个对象得到。

查看 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

会发现这三个方法返回的都是 XXXContainer,从字面上理解是个容器,容器里面一定是容纳了某些东西。

Container

再查看这些 Container 的定义,会发现里面都有一系列 decode... 方法,来对各种类型进行 decode。

一共有三种类型的 Container:

Container 类型作用
SingleValueDecodingContainer代表容器中只保存了一个值
KeyedDecodingContainer代表容器中保存的数据是按照键值对的形式保存的
UnkeyedDecodingContainer代表容器中保存的数据是没有键的,也就是说,保存的数据是一个数组

回到上面 User 的例子,JSON 数据如下:

{
 "user": "zhangsan",
 "age": 25
}

这种数据显然是键值对,因此要用 KeyedDecodingContainer 来取数据。KeyedDecodingContainer 应该是最常用的 Container 了。

struct User: Decodable {
 var name: String
 var age: Int
 init(from decoder: Decoder) throws {
 decoder.container(keyedBy: <#T##CodingKey.Protocol#>)
 }
}

参数需要传一个符合 CodingKey 协议的对象的类型,于是这里必须自己实现 CodingKeys 枚举,并把 CodingKeys.self 传入参数。

struct User: Decodable {
 var name: String
 var age: Int
 enum CodingKeys: String, CodingKey {
 case name
 case age
 }
 init(from decoder: Decoder) throws {
 let container = decoder.container(keyedBy: CodingKeys.self)
 }
}

然后就可以从 container 中取数据出来赋给自身的属性。由于这几个方法都会抛出异常,因此都要加上 try

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

同样的,我们也可以实现出编码。这时把 User 实现的协议改成 Codable

struct User: Codable {
 var name: String
 var age: Int
 enum CodingKeys: String, CodingKey {
 case name
 case age
 }
 init(from decoder: Decoder) throws {
 let container = try decoder.container(keyedBy: CodingKeys.self)
 name = try container.decode(String.self, forKey: .name)
 age = try container.decode(Int.self, forKey: .age)
 }
 func encode(to encoder: Encoder) throws {
 var encoder = encoder.container(keyedBy: CodingKeys.self)
 try encoder.encode(name, forKey: .name)
 try encoder.encode(age, forKey: .age)
 }
}

编码的过程就是和解码反过来,因为是键值对,从 encoder 中拿到 KeyedEncoderContainer,然后调用 encode 方法把属性的数据编码到 container 中,然后由 JSONEncoder 来处理接下来的事情。

接下来我们好奇的是,Container 中的数据是怎么保存的,Container 中的数据和 JSON 又是怎么互相转换的。

核心原理分析(Container <--> JSON)

JSONDecoder 的解码过程

JSONDecoder().decode(User.self, from: json) 这句开始分析。打开 swift-corelibs-foundation 中 JSONDecoder 的源码。

// 1
var parser = JSONParser(bytes: Array(data))
let json = try parser.parse()
// 2
return try JSONDecoderImpl(userInfo: self.userInfo, from: json, codingPath: [], options: self.options)
 .unwrap(as: T.self) // 3

decode 方法的实现主要是这三行代码。

  1. 先把 data 转化为一个类型为 JSONValue 的 json 对象。
  2. 然后构造一个 JSONDecoderImpl 对象
  3. 调用 JSONDecoderImpl 对象的 unwrap 方法得到要转换成的对象。

查看 JSONValue 的定义,它通过枚举嵌套把 JSON 的类型定义了出来。具体的数据通过关联值携带在了这个枚举类型中。

enum JSONValue: Equatable {
 case string(String)
 case number(String)
 case bool(Bool)
 case null
 case array([JSONValue])
 case object([String: JSONValue])
}

在获取 KeyedDecodingContainer 的时候也就是通过 JSONValue 构建 Container 对象。

// 这里 self.json 是保存在 JSONDecoderImpl 中的 JSONValue 类型
switch self.json {
case .object(let dictionary): // JSONValue 和 .object 这个 case 匹配,取出字典数据
 let container = KeyedContainer<Key>(
 impl: self,
 codingPath: codingPath,
 dictionary: dictionary // 传入字典数据
 )
 return KeyedDecodingContainer(container)

可以看到,KeyedDecodingContainer 只有当 self.json 匹配为字典时才能正确创建。数据在里面以 let dictionary: [String: JSONValue] 形式保存。

再看其他代码可以发现:

SingleValueContainer 就是直接存了一个 let value: JSONValue 在里面。

UnkeyedDecodingContainer 则是存了一个数组 let array: [JSONValue]

因此在 Container 调用 decode 方法获取数据时,就是根据参数 key 和类型从自身保存的数据中获取数据。这个源码很简单,看一下就明白了。

最后一步的 unwrap 方法,通过源码可以看到,最终调用的就是对象自己实现的 init(from decoder: Decoder) 方法

因此可以得出 JSON -> Model 的步骤如下:

  1. JSONParser 对传入的二进制 JSON data 进行解析,解析为 JSONValue 对象。
  2. 构建 JSONDecoderImpl,将相关的数据保存在里面。
  3. 调用 JSONDecoderImpl 的 unwrap 方法,开始调用对象实现的 init(from: decoder: Decoder) 方法
  4. 在 `init(from: decoder: Decoder) 方法中,首先根据数据类型获取对应的 Container。
  5. 调用 Container 的 decodeXXX 方法得到具体的值赋值给属性。

Model -> JSON 的步骤也是差不多的,只是方向反过来,有兴趣可以自己看一下源码。

在 Swift 的 JSON 转模型方法中,通过观察 Github 上的开源库可以发现一共有三种实现方案:

  • Objective-C Runtime 一众本身就是 OC 开发的库基本都用的这个方案,比如 YYModel,这种方案使用起来非常简单,代码非常少,但不符合 Swift。
  • Key 映射 比如 ObjectMapper 就是这种,这种的缺点是每个对象都要写一大堆映射代码,比较麻烦
  • 利用对象底层内存布局 SwiftyJSON 就属于这种,这种方法使用起来一样很方便,但是依赖苹果的私有代码,苹果如果调整了内部实现就会失效。

通过上面分析 Codable 原理发现,Codable 基本上就是 Key 映射的方案,只不过编译器帮我们自动合成了很多代码来让我们使用起来一样可以非常简单。由于编译器不会帮第三方库合成代码,因此 Codable 秒杀了一众基于 key 映射实现的第三方库。

编译器帮我们做了什么?

我们发现,只要让自己的对象符合 Codable 协议,就可以正常用 JSONEncoderJSONDecoder 编解码,并不需要实现协议中定义的方法。

那是因为编译器帮我们生成了。这种编译器合成代码在很多地方都会用到,例如为结构体和枚举自动合成实现 Equatable 和 Hashable 的代码,为枚举合成实现 CaseIterable 的代码等。

上面的 User 例子,编译器为我们合成的代码如下:

struct User: Codable {
 var name: String
 var age: Int
 // 编译器合成
 enum CodingKeys: String, CodingKey {
 case name
 case age
 }
 // 编译器合成
 init(from decoder: Decoder) throws {
 let container = try decoder.container(keyedBy: CodingKeys.self)
 name = try container.decode(String.self, forKey: .name)
 age = try container.decode(Int.self, forKey: .age)
 }
 // 编译器合成
 func encode(to encoder: Encoder) throws {
 var container = encoder.container(keyedBy: CodingKeys.self)
 try container.encode(name, forKey: .name)
 try container.encode(age, forKey: .age)
 }
}

可以见到,编译器自动合成了 CodingKeys 枚举的定义,并合成了实现 Encodable 和 Decodable 协议的代码。这给开发人员提供了方便。

默认值问题

编译器自动生成的编解码实现有个问题就是不支持默认值。如果需要支持默认值就需要自己来用 decodeIfPresent 来实现:

struct User: Decodable {
 var name: String
 var age: Int
 enum CodingKeys: String, CodingKey {
 case name
 case age
 }
 init(from decoder: Decoder) throws {
 let container = try decoder.container(keyedBy: CodingKeys.self)
 name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
 age = try container.decodeIfPresent(Int.self, forKey: .age) ?? 0
 }
}

但是这样每个结构体都要自己实现一次,非常麻烦。其实这个网上已经有很多文章在说了,就是用 @propertyWrapper 属性包装器来解决这个问题。

属性包装器 @propertyWrapper

属性包装器用来给属性和定义属性的结构之间包装一层,用来实现一些通用的 setter 和 getter 逻辑或初始化逻辑等。

例如对于 Int 型,可以如下定义属性包装器。

@propertyWrapper
public struct DefaultInt: Codable {
 public var wrappedValue: Int
 public init(from decoder: Decoder) throws {
 let container = try decoder.singleValueContainer()
 wrappedValue = (try? container.decode(BaseType.self)) ?? 0
 }
 public func encode(to encoder: Encoder) throws {
 try wrappedValue.encode(to: encoder)
 }
}

以上代码实现了 init(from decoder: Decoder) 方法来为属性在解码失败时提供一个默认值 0。实现 encode(to encoder: Encoder) 是为了编码时直接编码内部值而不是编码整个属性包装类型。

其它的很多基础类型都是一样的逻辑,为了避免重复代码,可以用范型来统一实现。

public protocol HasDefaultValue {
 static var defaultValue: Self { get set }
}
@propertyWrapper
public struct DefaultBaseType<BaseType: Codable & HasDefaultValue>: Codable {
 public var wrappedValue: BaseType
 public init(from decoder: Decoder) throws {
 let container = try decoder.singleValueContainer()
 wrappedValue = (try? container.decode(BaseType.self)) ?? BaseType.defaultValue
 }
 public func encode(to encoder: Encoder) throws {
 try wrappedValue.encode(to: encoder)
 }
}

然后可以考虑用类型别名来定义出各个类型的属性包装关键字。因为如果包含 <. 等字符,写起来会比较麻烦。

typealias DefaultInt = DefaultBaseType<Int>
typealias DefaultString = DefaultBaseType<String>

但是有些类型需要特殊实现一下。

枚举

枚举类型可以利用 rawValue 来进行数据和类型相互转换。

@propertyWrapper
public struct DefaultIntEnum<Value: RawRepresentable & HasDefaultEnumValue>: Codable where Value.RawValue == Int {
 private var intValue = Value.defaultValue.rawValue
 public var wrappedValue: Value {
 get { Value(rawValue: intValue)! }
 set { intValue = newValue.rawValue }
 }
 public init() {
 }
 public init(from decoder: Decoder) throws {
 let container = try decoder.singleValueContainer()
 intValue = (try? container.decode(Int.self)) ?? Value.defaultValue.rawValue
 }
 public func encode(to encoder: Encoder) throws {
 try intValue.encode(to: encoder)
 }
}

数组

由于数组需要通过 UnkeyedDecodingContainer 拿数据,需要单独特殊处理。

@propertyWrapper
public struct DefaultArray<Value: Codable>: Codable {
 public var wrappedValue: [Value]
 public init() {
 wrappedValue = []
 }
 public init(wrappedValue: [Value]) {
 self.wrappedValue = wrappedValue
 }
 public init(from decoder: Decoder) throws {
 var container = try decoder.unkeyedContainer()
 var results = [Value]()
 while !container.isAtEnd {
 let value = try container.decode(Value.self)
 results.append(value)
 }
 wrappedValue = results
 }
 public func encode(to encoder: Encoder) throws {
 try wrappedValue.encode(to: encoder)
 }
}

对象

因为对象的结构都是不一样的,没法给出一个的默认值。因此设计了一个 EmptyInitializable 协议,里面只有一个无参数的初始化方法。

public protocol EmptyInitializable {
 init()
}

需要提供默认值的对象可以实现这个协议。不过这里需要权衡一下,如果对内存空间占用有比较高的要求,用可选值可能是更好的方案,因为一个空对象占用的空间和有数据的对象占用的空间是一样多的。

属性包装器的使用

使用属性包装器封装各个类型后,只要像这样使用就可以了,decode 的时候就如果不存在对应字段数据属性就会初始化为默认值。

struct User {
 @DefaultString var name: String
 @DefaultInt var age: Int
}

我简单封装了一个库,目前我们的新 Swift 项目在使用,完整代码在这里: https://github.com/liuduoios/CodableDefaultValue

参考资料:

  • 《the swift programming language》
  • 《Advanced Swift》
  • swift-corelibs-foundation 源码
作者:独孤星岳

%s 个评论

要回复文章请先登录注册