Swift协议Codable底层探索及应用

3,702 阅读14分钟

前言

CodableSwift 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: Encoderenum CodingKeys : CodingKey这个3个东西。

其中init(from decoder: Decoder)func encode(to encoder: EncoderCodable必须要实现的,而enum CodingKeys : CodingKey是实现上述两个方法用的一个枚举。换句话说,原本这3个的实现都是需要我们完成的。但是,在编译器的帮助下,我们不用自己完成这些实现,编译器会帮我们完善这些代码。

但是我们在享受编译器的便利的同时,同时也失去对Codable使用的灵活性,因为编译器会帮我们完善的代码都是定制的。但是解析数据往往会遇到很多意外的情况,比如关键字冲突,数据格式错误,数据缺失等等,所以,为了更好的运用Codable协议,我们得了解底层序列化和反序列化过程是如何实现的。

JSONDecoder

上面解析JSON数据是调用JSONDecoderdecode方法实现的,我们从这个地方看如何完成数据的解析的,我们先看下源码:

    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步:

  1. 获得topLevel,调用的JSONSerialization.jsonObject,这个很常见了,如果解析正确的话,topLevel是一个字典或者数组。
  2. 获得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是解析数据流格式的,详细的可以自己搜索下,资料应该很多的。

  1. 获得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,这个类是一个栈结构,实现了栈的pushpop,就不点进去看了,相信成一个栈就行了,所以这个初始化方法就是把所有的值保存了起来。

我们看最核心的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之类的桥接实现,但不影响总体理解,主要是代码太长了。。

我们看到像DateDataURL等,会单独调用各自的unbox方法,因为涉及到前面说的解析策略,而我们自己声明的类或者结构体,最终会来到type.init(from: self),也就是前面我们说的init(from decoder: Decoder)方法了。

init(from decoder: Decoder)

我们先看下DecoderDecoder是一个协议:

    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方法获得了KeyedDecodingContainerKeyedDecodingContainer的大概定义:

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定义了很多decodedecodeIfPresent的解析方法,其中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,转了一圈,最后还是调用的是_JSONDecoderunbox方法

KeyedDecodingContainerUnkeyedDecodingContainerSingleValueDecodingContainer的区别

其实这3者的区别在于container中放的内容的区别,就拿KeyedDecodingContainer来说,在刚才decode代码中,我们看到这句代码

let entry = self.container[key.stringValue]

这里取值的方式是字典的样式,说明这里container放的就是字典。

我们在看下UnkeyedDecodingContainerdecode过程

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后,调用的还是_JSONDecoderunbox方法。

我们把3者的区别讲完了,但可能有些小伙伴还是没有具体的概念,我拿一些实际的例子讲解下,应该就能懂了。

KeyedDecodingContainer应用示例

KeyedDecodingContainer应该是用的最多的,只要JSON数据能被序列化成字典的,就应该用KeyedDecodingContainerdecode,拿上面的例子来说:

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,所以会把数组里的值依次取出来赋值,取xy多次调用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类型,声明了IntString两个类型的属性,在init(from decoder: Decoder)中可以用singleValueContainer方法把数据直接装载过来,然后分别尝试用IntString类型来解析,这样就可以完成场景的需求了。

开发中遇到的特殊场景

我们在开发中,虽然大多数情况下,解析数据直接映射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类型,而且正好在协议方法内设置解析失败的默认值。

接下来我们还要实现KeyedDecodingContainerUnkeyedDecodingContainer的扩展:

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,直接向函数外面抛出错误了,根本不会调用_JSONDecoderunbox的方法,也就不会调用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。而且我们也可以看到,nameDefault<String>类型,ageDefault<Int>类型,但用起来和StringInt类型没有区别。

结语

虽然Codable好像还没有YYModelHandyJson等好用,但是Codable毕竟是Swift的亲儿子,后续会有持续的特性推出,像@propertyWrapper就是在Swift5.1推出的。在Swift5之前,苹果致力于它的ABI稳定性,而在ABI稳定之后,苹果将会致力于Swift的语法,特性等。总而言之,Swift将会飞速发展,Swift也会变成一门越来越复杂的语言,伙伴们,一起加油吧!!

参考文献