Codable 是什么
Codable 自身便是个类型别名
typealias Codable = Decodable & Encodable
代表一个一起契合 Decodable 和 Encodable 协议的类型,即可解码且可编码的类型。
Codable 也能够代表苹果为 Swift 开发的一套编解码体系,从 Swift 4 开端引入,包括了 Encoder 和 Decoder 协议和他们的两个完结 JSONEncoder
、JSONDecoder
和 PropertyListEncoder
、PropertyListDecoder
。其间 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 作为一门静态语言的高性能,等于说自己降低了整个项目的运转性能,这是无法忍受的。
好在苹果供给了 JSONEncoder
和 JSONDecoder
这两个结构体来便利得在 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 协议,那整个目标就能够正常运用 JSONEncoder
和 JSONDecoder
编解码。
struct UserInfo: Codable {
var name: String
var age: Int
}
struct User: Codable {
var info: UserInfo
}
枚举
枚举类型有必要它的 RawValue 的类型是可解码的,并且 RawValue 的类型和 JSON 字段类型对应,即可正确解码。
自界说 CodingKeys
自界说 CodingKeys 主要是两个目的
- 当数据类型特点名和 JSON 中字段名不一起,做 key 的映射。
- 经过在不添加某些字段的 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 办法的完结主要是这三行代码。
- 先把 data 转化为一个类型为
JSONValue
的 json 目标。 - 然后结构一个 JSONDecoderImpl 目标
- 调用 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 的过程如下:
- JSONParser 对传入的二进制 JSON data 进行解析,解析为 JSONValue 目标。
- 构建 JSONDecoderImpl,将相关的数据保存在里边。
- 调用 JSONDecoderImpl 的 unwrap 办法,开端调用目标完结的
init(from: decoder: Decoder)
办法 - 在 “init(from: decoder: Decoder)` 办法中,首要依据数据类型获取对应的 Container。
- 调用 Container 的 decodeXXX 办法得到详细的值赋值给特点。
Model -> JSON 的过程也是差不多的,仅仅方向反过来,有兴趣能够自己看一下源码。
在 Swift 的 JSON 转模型办法中,经过调查 Github 上的开源库能够发现一共有三种完结计划:
- Objective-C Runtime 一众自身便是 OC 开发的库根本都用的这个计划,比方 YYModel,这种计划运用起来十分简略,代码十分少,但不契合 Swift。
- Key 映射 比方 ObjectMapper 便是这种,这种的缺点是每个目标都要写一大堆映射代码,比较麻烦
- 运用目标底层内存布局 SwiftyJSON 就归于这种,这种办法运用起来相同很便利,可是依赖苹果的私有代码,苹果假如调整了内部完结就会失效。
经过上面剖析 Codable 原理发现,Codable 根本上便是 Key 映射的计划,只不过编译器帮咱们主动组成了许多代码来让咱们运用起来相同能够十分简略。由于编译器不会帮第三方库组成代码,因而 Codable 秒杀了一众依据 key 映射完结的第三方库。
编译器帮咱们做了什么?
咱们发现,只需让自己的目标契合 Codable 协议,就能够正常用 JSONEncoder
和 JSONDecoder
编解码,并不需求完结协议中界说的办法。
那是由于编译器帮咱们生成了。这种编译器组成代码在许多当地都会用到,例如为结构体和枚举主动组成完结 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 项目在运用,完好代码在这儿: github.com/liuduoios/C…
参考资料:
- 《the swift programming language》
- 《Advanced Swift》
- swift-corelibs-foundation 源码