前言
Codable是Swift 4.0后引入的特性,目标是取代NSCoding协议。
相信很多小伙伴已经用上了吧,虽然Codable给我们JSON数据解析带来一种解决方案,但是在很多情况下又不是那么好用。所以我们一起探索下Codable的底层实现,以及如何改进使用方式。
sil文件探索Codable实现
先来一段最简单的代码:
struct Teacher: Codable {
var name: String
var age: Int
}
let jsonString = """
{
"name": "Tom",
"age": 23,
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
if let jsonData = jsonData,
let result = try? decoder.decode(Teacher.self, from: jsonData) {
print(result)
} else {
print("解析失败")
}
我们看下sil文件中的Teacher的定义
struct Teacher : Decodable & Encodable {
@_hasStorage var name: String { get set }
@_hasStorage var age: Int { get set }
init(name: String, age: Int)
enum CodingKeys : CodingKey {
case name
case age
@_implements(Equatable, ==(_:_:)) static func __derived_enum_equals(_ a: Teacher.CodingKeys, _ b: Teacher.CodingKeys) -> Bool
var hashValue: Int { get }
func hash(into hasher: inout Hasher)
var stringValue: String { get }
init?(stringValue: String)
var intValue: Int? { get }
init?(intValue: Int)
}
init(from decoder: Decoder) throws
func encode(to encoder: Encoder) throws
}
我们看到,Teacher除了我们原本就有的定义外,还多出了init(from decoder: Decoder)、func encode(to encoder: Encoder、enum CodingKeys : CodingKey这个3个东西。
其中init(from decoder: Decoder)、func encode(to encoder: Encoder是Codable必须要实现的,而enum CodingKeys : CodingKey是实现上述两个方法用的一个枚举。换句话说,原本这3个的实现都是需要我们完成的。但是,在编译器的帮助下,我们不用自己完成这些实现,编译器会帮我们完善这些代码。
但是我们在享受编译器的便利的同时,同时也失去对Codable使用的灵活性,因为编译器会帮我们完善的代码都是定制的。但是解析数据往往会遇到很多意外的情况,比如关键字冲突,数据格式错误,数据缺失等等,所以,为了更好的运用Codable协议,我们得了解底层序列化和反序列化过程是如何实现的。
JSONDecoder
上面解析JSON数据是调用JSONDecoder的decode方法实现的,我们从这个地方看如何完成数据的解析的,我们先看下源码:
open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
let topLevel: Any
do {
topLevel = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
}
let decoder = _JSONDecoder(referencing: topLevel, options: self.options)
guard let value = try decoder.unbox(topLevel, as: type) else {
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
}
return value
}
核心一共有3步:
- 获得
topLevel,调用的JSONSerialization.jsonObject,这个很常见了,如果解析正确的话,topLevel是一个字典或者数组。 - 获得
decoder,但是这个decoder并不是JSONDecoder类型,而是_JSONDecoder类型,_JSONDecoder初始化方法一共传进去了两个参数,其中一个是第一步的topLevel,还有一个是JSONDecoder类型的options,我们看下options是怎么获得的:
fileprivate var options: _Options {
return _Options(dateDecodingStrategy: dateDecodingStrategy,
dataDecodingStrategy: dataDecodingStrategy,
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
keyDecodingStrategy: keyDecodingStrategy,
userInfo: userInfo)
}
options就是解析JSON数据的策略,dateDecodingStrategy是解析时间格式的,dataDecodingStrategy是解析数据流格式的,详细的可以自己搜索下,资料应该很多的。
- 获得
decoder后,调用了unbox方法获得了最终的value
所以核心就在_JSONDecoder类及它的unbox方法了
_JSONDecoder
我们看下刚才_JSONDecoder的初始化方法:
/// Initializes `self` with the given top-level container and options.
fileprivate init(referencing container: Any, at codingPath: [CodingKey] = [], options: JSONDecoder._Options) {
self.storage = _JSONDecodingStorage()
self.storage.push(container: container)
self.codingPath = codingPath
self.options = options
}
这里看到唯一不熟悉是_JSONDecodingStorage,这个类是一个栈结构,实现了栈的push及pop,就不点进去看了,相信成一个栈就行了,所以这个初始化方法就是把所有的值保存了起来。
我们看最核心的unbox方法:
fileprivate func unbox<T : Decodable>(_ value: Any, as type: T.Type) throws -> T? {
return try unbox_(value, as: type) as? T
}
fileprivate func unbox_(_ value: Any, as type: Decodable.Type) throws -> Any? {
if type == Date.self {
guard let date = try self.unbox(value, as: Date.self) else { return nil }
return date
} else if type == Data.self {
guard let data = try self.unbox(value, as: Data.self) else { return nil }
return data
} else if type == URL.self {
guard let urlString = try self.unbox(value, as: String.self) else {
return nil
}
guard let url = URL(string: urlString) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath,
debugDescription: "Invalid URL string."))
}
return url
} else if type == Decimal.self {
guard let decimal = try self.unbox(value, as: Decimal.self) else { return nil }
return decimal
} else if let stringKeyedDictType = type as? _JSONStringDictionaryDecodableMarker.Type {
return try self.unbox(value, as: stringKeyedDictType)
} else {
self.storage.push(container: value)
defer { self.storage.popContainer() }
return try type.init(from: self)
}
}
这里我去了一段宏判断的代码块,是关于NSDate之类的桥接实现,但不影响总体理解,主要是代码太长了。。
我们看到像Date、Data、URL等,会单独调用各自的unbox方法,因为涉及到前面说的解析策略,而我们自己声明的类或者结构体,最终会来到type.init(from: self),也就是前面我们说的init(from decoder: Decoder)方法了。
init(from decoder: Decoder)
我们先看下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
咋一看,都不知道是干什么的,我们先通过sil文件看下,编译器是怎么实现init(from decoder: Decoder)的:
我们看见生成了
UnkeyedDecodingContainer,由于太长了,我放弃展示sil文件,直接看完整的swift实现吧。。
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)
}
我们这边调用了decoder.container方法获得了KeyedDecodingContainer,KeyedDecodingContainer的大概定义:
public struct KeyedDecodingContainer<K> : KeyedDecodingContainerProtocol where K : CodingKey {
public typealias Key = K
public init<Container>(_ container: Container) where K == Container.Key, Container : KeyedDecodingContainerProtocol
public var codingPath: [CodingKey] { get }
public var allKeys: [KeyedDecodingContainer<K>.Key] { get }
public func contains(_ key: KeyedDecodingContainer<K>.Key) -> Bool
public func decodeNil(forKey key: KeyedDecodingContainer<K>.Key) throws -> Bool
public func decode(_ type: Bool.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Bool
public func decode(_ type: String.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> String
public func decode(_ type: Double.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Double
public func decode(_ type: Float.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Float
public func decode(_ type: Int.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Int
...
public func decode<T>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T where T : Decodable
public func decodeIfPresent(_ type: Bool.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Bool?
public func decodeIfPresent(_ type: String.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> String?
public func decodeIfPresent(_ type: Double.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Double?
public func decodeIfPresent(_ type: Float.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Float?
public func decodeIfPresent(_ type: Int.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Int?
...
public func decodeIfPresent<T>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T? where T : Decodable
...
}
KeyedDecodingContainer定义了很多decode和decodeIfPresent的解析方法,其中decodeIfPresent是用在可选值身上的。这么多解析方法,我们挑选其中的一个分析下:
public func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
guard let entry = self.container[key.stringValue] else {
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(_errorDescription(of: key))."))
}
self.decoder.codingPath.append(key)
defer { self.decoder.codingPath.removeLast() }
guard let value = try self.decoder.unbox(entry, as: Int.self) else {
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead."))
}
return value
}
我们看到核心方法是self.decoder.unbox,而self.decoder就是我们上面传进来的_JSONDecoder,转了一圈,最后还是调用的是_JSONDecoder的unbox方法
KeyedDecodingContainer、UnkeyedDecodingContainer、SingleValueDecodingContainer的区别
其实这3者的区别在于container中放的内容的区别,就拿KeyedDecodingContainer来说,在刚才decode代码中,我们看到这句代码
let entry = self.container[key.stringValue]
这里取值的方式是字典的样式,说明这里container放的就是字典。
我们在看下UnkeyedDecodingContainer的decode过程
public mutating func decode(_ type: Int.Type) throws -> Int {
guard !self.isAtEnd else {
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_JSONKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end."))
}
self.decoder.codingPath.append(_JSONKey(index: self.currentIndex))
defer { self.decoder.codingPath.removeLast() }
guard let decoded = try self.decoder.unbox(self.container[self.currentIndex], as: Int.self) else {
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath + [_JSONKey(index: self.currentIndex)], debugDescription: "Expected \(type) but found null instead."))
}
self.currentIndex += 1
return decoded
}
我们把从container中获取值的抽取出来就是:
let value = self.container[self.currentIndex]
self.currentIndex += 1
那么这次获取值是通过下标拿取的,说明这里container放的就是数组,而且每次decode完,下标都会加一,下次在decode时,会自动拿下个下标的值。
最后在研究下SingleValueDecodingContainer,这个比较特别,我们看下_JSONDecoder是如何生成SingleValueDecodingContainer的:
public func singleValueContainer() throws -> SingleValueDecodingContainer {
return self
}
我们看到SingleValueDecodingContainer返回就是_JSONDecoder自己本身,所以SingleValueDecodingContainer实现decode方法是在_JSONDecoder的分类中实现的:
public func decode(_ type: Int.Type) throws -> Int {
try expectNonNull(Int.self)
return try self.unbox(self.storage.topContainer, as: Int.self)!
}
所以SingleValueDecodingContainer中的container放的是单个的值,解析的时候是作为一个整体来解析的,我可以把3者的取值的过程抽象出来放在一起对比下:
// KeyedDecodingContainer,通过字典key来取值
let value = container[key]
//UnkeyedDecodingContainer,通过数组下标来取值
let value = container[index]
// SingleValueDecodingContainer,value就是container本身
let value = container
所以在SingleValueDecodingContainer中,container一般放的是String或者Int之类的,当然,也可以放数组字典,但是不在取里面的元素了,而是作为一个整体被解析。
虽然3者取值方式有所区别,但是最后拿到value后,调用的还是_JSONDecoder的unbox方法。
我们把3者的区别讲完了,但可能有些小伙伴还是没有具体的概念,我拿一些实际的例子讲解下,应该就能懂了。
KeyedDecodingContainer应用示例
KeyedDecodingContainer应该是用的最多的,只要JSON数据能被序列化成字典的,就应该用KeyedDecodingContainer来decode,拿上面的例子来说:
struct Teacher: Codable {
var name: String
var age: Int
enum CodingKeys: 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)
}
}
let jsonString = """
{
"name": "Tom",
"age": 23,
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
if let jsonData = jsonData,
let result = try? decoder.decode(Teacher.self, from: jsonData) {
print(result)
} else {
print("解析失败")
}
jsonString能被序列化成字典,那么在初始化container的时候选择初始化成KeyedDecodingContainer的方法。
UnkeyedDecodingContainer应用示例
现在有这样一个场景,你设计了一个坐标的结构体,有属性横坐标和纵坐标:
struct Location: Codable {
var x: Double
var y: Double
}
但是服务给的数据并不是这样的:
let jsonString = """
{
"x": 10,
"y": 20,
}
"""
而是这样的:
let jsonString = "[10, 20]"
这样就不能用KeyedDecodingContainer,而是用UnkeyedDecodingContainer,可以写成这样:
struct Location: Codable {
var x: Double
var y: Double
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
x = try container.decode(Double.self)
y = try container.decode(Double.self)
}
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(x)
try container.encode(y)
}
}
如果你的JSON数据能被序列化成数组,那么可以用UnkeyedDecodingContainer来解析。
UnkeyedDecodingContainer的取值过程前面也说了,每次decode完下标会加1,所以会把数组里的值依次取出来赋值,取x、y多次调用decode就行。
SingleValueDecodingContainer应用示例
我们再设一个场景,比如服务器返回一个字段,该字段可能是字符串,也可能是一个整型,怎么办?
我们可以自定义一个类型:
struct TStrInt: Codable {
var int: Int
var string: String
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let stringValue = try? container.decode(String.self) {
string = stringValue
int = Int(stringValue) ?? 0
} else if let intValue = try? container.decode(Int.self) {
int = intValue
string = String(intValue);
} else {
int = 0
string = ""
}
}
}
我们自定义了TStrInt类型,声明了Int、String两个类型的属性,在init(from decoder: Decoder)中可以用singleValueContainer方法把数据直接装载过来,然后分别尝试用Int、String类型来解析,这样就可以完成场景的需求了。
开发中遇到的特殊场景
我们在开发中,虽然大多数情况下,解析数据直接映射JSON数据中的关键字就行了,但是我们往往会遇到一些特殊情况,比如上面,坐标是用数组返回的,字段类型可能是字符串可能是整形,等等。
遇到这些情况,最正确的做法就是自己实现Codable协议,但是这样繁琐很多,失去了使用Codable协议的意义,也不是我们程序员喜欢的方式。所以我们探索一点如何最小的改动,让系统自动帮我们实现Codable,却又能完成特殊场景的需求。
关键字与系统冲突
最常见的字段冲突应该就是id了,这个解决方案比较简单,很多教程上都有:
enum CodingKeys: String, CodingKey {
case kid = "id"
}
我们把属性称为kid,然后把枚举的原始值设为id就行了,这样就可以把JSON数据中的id字段映射给kid属性了。
当值缺失时使用默认值
业务场景中,有一个字段存的是Bool值,如果该字段缺失,默认为true,比如:
struct Person: Codable {
var isMan: Bool
}
我们第一反应就是把isMan变成可选值:
struct Person: Codable {
var isMan: Bool?
}
但是使用的时候却没有那么方便了,必须做两层判断,如果在项目里使用多了,会觉得很恶心。一般有经验的程序员会封装一个方法来获取真正的Bool值,在swift里,我们可以用计算属性实现,并且把原有属性隐藏掉:
struct Person: Codable {
private var isMan: Bool?
var is_Man: Bool {
guard let isMan = isMan else {
return true
}
return isMan
}
}
服务端数据类型意外变动或缺失
其实所有的特殊情况只要事前能预料到,就有办法能解决,但是明明和服务端约定好的数据类型,结果返回的数据是其它类型的,或者服务端信誓旦旦的保证该值肯定有值,结果取线上却没有值了。
Codable令人最糟心一点是,一旦一个值解析失败了,那么程序就会向函数外面抛出一个错误,虽然你能拿到错误的地方在哪里,但是却拿不到已经解析正确的值。比如一个类似朋友圈的列表数据,结果里面有个头像的URL缺失,造成了整个列表的解析错误,明明展示UI的时候,只要把解析错误的头像替换成默认头像就行,结果整张列表都展示不出来了。
所以,我们要明白一点,服务端都是不可信的。那怎么办呢?你可能会想到,把所有模型的属性都变成可选值:
struct Teacher: Codable {
var name: String?
var age: Int?
}
虽然这样能保证解析函数能够正常的解析到最后,但是你在使用这些模型属性的时候,每个都要解包,想想就糟心,虽然你可以这样做:
extension Optional where Wrapped == String {
var value: String {
switch self {
case .some:
return self!
case .none:
return ""
}
}
}
这样会比每次解包要好一点,但是调用的时候,会比平时多点出一个属性,有没有更好的方法呢?
这个可能得用到@propertyWrapper属性包装器,有点像python的装饰器,不熟悉的同学可以看官方文档学习下。
我们先定义一个协议DefaultValue:
protocol DefaultValue {
static var defaultValue: Self { get }
}
extension String: DefaultValue {
static let defaultValue = ""
}
extension Int: DefaultValue {
static let defaultValue = 0
}
我们先定义每一种类型如果在解析的时候出现意外,应该默认的赋值是什么。
然后我们实现属性包装器:
typealias DefaultCodable = DefaultValue & Codable
@propertyWrapper
struct Default<T: DefaultCodable> {
var wrappedValue: T
}
extension Default: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
wrappedValue = (try? container.decode(T.self)) ?? T.defaultValue
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue)
}
}
被属性包装起来的类型T,应该自身要满足Codable协议,而且也要满足DefaultValue协议,这样解析失败的时候可以设默认值。而且我们的包装器Default也要实现Codable协议,虽然我们用了属性包装器,平时用的感知是T类型的,但是实际上还是Default类型,而且正好在协议方法内设置解析失败的默认值。
接下来我们还要实现KeyedDecodingContainer、UnkeyedDecodingContainer的扩展:
extension KeyedDecodingContainer {
func decode<T>(_ type: Default<T>.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Default<T> where T : DefaultCodable {
try decodeIfPresent(type, forKey: key) ?? Default(wrappedValue: T.defaultValue)
}
}
extension UnkeyedDecodingContainer {
mutating func decode<T>(_ type: Default<T>.Type) throws -> Default<T> where T : DefaultCodable {
try decodeIfPresent(type) ?? Default(wrappedValue: T.defaultValue)
}
}
写扩展是因为,在原本的decode方法中,当发现在从原本的container中取值为nil,直接向函数外面抛出错误了,根本不会调用_JSONDecoder的unbox的方法,也就不会调用Default类型中Codable的协议方法。
到这边,我们的属性包装器Default就完成了,接下来我们只要把模型的每个属性套上属性包装器就行了:
struct Teacher: Codable {
@Default var name: String
@Default var age: Int
}
我们试下缺失值的解析:
let jsonString = """
{
"name": "Tom",
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
if let jsonData = jsonData,
let result = try? decoder.decode(Teacher.self, from: jsonData) {
print(result)
print("name: \(result.name)")
print("age: \(result.age)")
} else {
print("解析失败")
}
//打印结果
Teacher(_name: SwiftSIL.Default<Swift.String>(wrappedValue: "Tom"), _age: SwiftSIL.Default<Swift.Int>(wrappedValue: 0))
name: Tom
age: 0
Program ended with exit code: 0
我们看到虽然age值缺失了,但是解析并没有错误,而是以默认值0赋值给了age。而且我们也可以看到,name是Default<String>类型,age是Default<Int>类型,但用起来和String、Int类型没有区别。
结语
虽然Codable好像还没有YYModel、HandyJson等好用,但是Codable毕竟是Swift的亲儿子,后续会有持续的特性推出,像@propertyWrapper就是在Swift5.1推出的。在Swift5之前,苹果致力于它的ABI稳定性,而在ABI稳定之后,苹果将会致力于Swift的语法,特性等。总而言之,Swift将会飞速发展,Swift也会变成一门越来越复杂的语言,伙伴们,一起加油吧!!