Swift底层探索:Codable

Swift 4.0后引入的特性,目标是取代NSCoding协议。对结构体,枚举和类都支持,能够把JSON这种弱类型数据转换成代码中使用的强类型数据,同时由于编译器的帮助,可以少写很多重复代码。
Codable协议被设计出来用于替代NSCoding协议,所以遵从Codable协议的对象就可以无缝的支持NSKeyedArchiver和NSKeyedUnarchiver对象进行Archive&UnArchive持久化和反持久化。原有的解码过程和持久化过程需要单独处理,现在通过新的Codable协议一起搞定。

Codable定义:(由DecodableEncodable协议组成):

public typealias Codable = Decodable & Encodable

Decodable协议定义了一个初始化函数:init,遵从Decodable协议的类型可以使用任何Decoder对象进行初始化,完成解码过程。

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

Encodable协议定义了一个encode方法, 任何Encoder对象都可以创建遵从了Encodable协议类型的表示,完成编码过程。

public protocol Encodable {
    func encode(to encoder: Encoder) throws
}

Swift标准库中的类型StringIntDouble以及Foundation 框架中DataDateURL都是默认支持Codable协议的,只需声明支持协议即可。

基础语法

一个最简单的例子:

struct Hotpot: Codable{
    var name: String
    var age: Int
    var height: Double
}

Hotpot遵循了Codable协议,他就默认有了init(from:)encode(to:)方法。
原始数据(为了演示方便以字符串代替):

let jsonString = """
{
    "age": 10,
    "name": "cat",
    "height": 1.85
}
"""

解码

let jsonData = jsonString.data(using: .utf8)
//创建解码器
let jsonDecoder = JSONDecoder()
if let data = jsonData {
    //解码,参数:数据类型,原始data数据
    let hotpot = try? jsonDecoder.decode(Hotpot.self, from: data)
    print(hotpot ?? "error")
}
//输出
Hotpot(name: "cat", age: 10, height: 1.85)
  • 创建解码器
  • 解码decode传入类型(Hotpot.self)和数据(data

编码

let jsonEncode = JSONEncoder()
let jsonData1 = try? jsonEncode.encode(hotpot)
if let data = jsonData1 {
    let jsonString1 = String(decoding: data, as: UTF8.self)
    print(jsonString1)
}
//输出
{"name":"cat","age":10,"height":1.8500000000000001}

这里发现在encode的时候精度出现了问题:
解决方案大致分为以下几种:

  • 浮点数转string处理。在encode时候double值转为String就能避免这个问题。
  • 使用NSDecimalNumber来接收,由于NSDecimalNumber无法遵循Codable协议(原因),所以需要用Decimal来接收数据,NSDecimalNumber作为计算属性来处理数据。
  • 三方库 金额计算。

对于第二点:
Synthesizing conformace to Codable, Equatable and Hashable in different source files is currently not supported by the Swift compiler。
At the moment (Xcode 10.2.1 / Swift 5.0.1) Codable currently isn't supported yet if an extension in one file adds conformance in a different file. Check this out at https://bugs.swift.org/. https://bugs.swift.org/browse/SR-6101
大概意思是目前Swift不支持在不同的源文件,合成conformaceCodableEquatableHashable
什么意思呢?看个例子就明白了:
A文件中定义一个class,在B文件扩展这个class遵循协议codable就会报错了。

//A文件:
class HPTestCodable {    
}
//B文件:
extension HPTestCodable:Codable {
}
image.png

解决精度问题
以第二种方式为例,这里仅仅是为了解决Encode过程中height精度问题:

struct Hotpot: Codable{
    var name: String
    var age: Int
    var height: Decimal
    var heightNumber: NSDecimalNumber {
            get {
                return NSDecimalNumber(decimal: height)
            }
            set {
                height = newValue.decimalValue
            }
        }
}

调用:

/**json数据*/
let jsonString = """
{
    "age": 10,
    "name": "cat",
    "height": 1.85
}
"""
let jsonData = jsonString.data(using: .utf8)

/**解码*/
var hotpot:Hotpot?
if let data = jsonData {
    //解码,参数:数据类型,原始data数据
    hotpot = try? JSONDecoder().decode(Hotpot.self, from: data)
    if let height = hotpot?.height.description {
        print(height)
    }
    print(hotpot ?? "error")
}
/**编码*/
let jsonEncode = JSONEncoder()
let jsonData1 = try? jsonEncode.encode(hotpot)
if let data = jsonData1 {
    let jsonString = String(decoding: data, as: UTF8.self)
    print(jsonString)
}

结果:

1.85
Hotpot(name: "cat", age: 10, height: 1.85)
{"name":"cat","age":10,"height":1.85}

这里看到decode、encode、取值精度都没有问题了。

归档反归档
由于JSONEncoder将数据转换成了data,我们可以直接对数据进行存储了,可以写入本地文件、数据库等。以下简单写入UserDefaults演示(当然用NSKeyedArchiver也可以):

if let data = try? JSONEncoder().encode(hotpot) {
    let jsonString = String(decoding: data, as: UTF8.self)
   //写入UserDefaults
    UserDefaults.standard.set(data, forKey: "hotpotData")
    print(jsonString)
}
// 从UserDefaults读取
if let data = UserDefaults.standard.data(forKey: "hotpotData") {
    let jsonString = String(decoding: data, as: UTF8.self)
    print(jsonString)
}
{"name":"cat","age":10,"height":1.85}
{"name":"cat","age":10,"height":1.85}

嵌套的模型

struct Hotpot: Codable{
    var name: String
    var age: Int
    var height: Double
    var cat: Cat
}

extension Hotpot {
    struct Cat: Codable {
        var name: String
        var age: Int
    }
}

let jsonString = """
{
    "name": "hotpot",
    "height": 1.85,
    "age": 18,
    "cat": {
        "age": 1,
        "name": "cat"
     }
}
"""

let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
if let data = jsonData {
    let result = try? decoder.decode(Hotpot.self, from: data)
    print(result ?? "解析失败")
}

输出:

Hotpot(name: "hotpot", age: 18, height: 1.85, cat: SwiftProtocol.Hotpot.Cat(name: "cat", age: 1))
  • 对于遵循了Codable协议的类型,解码器能够自动识别模型的嵌套。

包含数组

struct Hotpot: Codable{
    var name: String
    var age: Int
    var height: Double
    var cat: [Cat]
}

extension Hotpot {
    struct Cat: Codable {
        var name: String
        var age: Int
    }
}

let jsonString = """
{
    "name": "hotpot",
    "height": 1.85,
    "age": 18,
    "cat": [{
        "age": 1,
        "name": "cat"
     },{
        "age": 10,
        "name": "cat1"
     }]
}
"""

let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
if let data = jsonData{
    let result = try? decoder.decode(Hotpot.self, from: data)
    print(result ?? "解析失败")
}
Hotpot(name: "hotpot", age: 18, height: 1.85, cat: [SwiftProtocol.Hotpot.Cat(name: "cat", age: 1), SwiftProtocol.Hotpot.Cat(name: "cat1", age: 10)])

可以看到不需要做额外的操作。

JSON数据是数组集合

let jsonString = """
[
  {
    "age" : 18,
    "cat" : [
      {
        "age" : 1,
        "name" : "cat"
      },
      {
        "age" : 10,
        "name" : "cat1"
      }
    ],
    "name" : "hotpot",
    "height" : 1.85
  },
  {
    "age" : 18,
    "cat" : [
      {
        "age" : 1,
        "name" : "cat"
      },
      {
        "age" : 10,
        "name" : "cat1"
      }
    ],
    "name" : "hotpot",
    "height" : 1.85
  }
]"""

//调用
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
if let data = jsonData{
    let result = try? decoder.decode([Hotpot].self, from: data)
    print(result ?? "解析失败")
}
[SwiftProtocol.Hotpot(name: "hotpot", age: 18, height: 1.85, cat: [SwiftProtocol.Hotpot.Cat(name: "cat", age: 1), SwiftProtocol.Hotpot.Cat(name: "cat1", age: 10)]), SwiftProtocol.Hotpot(name: "hotpot", age: 18, height: 1.85, cat: [SwiftProtocol.Hotpot.Cat(name: "cat", age: 1), SwiftProtocol.Hotpot.Cat(name: "cat1", age: 10)])]

只需要解析模型对应的类型传数组[Hotpot].self就ok了。

JSON数据中有 Optional values

一般情况下后台可能会传给我们null值,如果上面的例子不做修改直接解析会失败。处理不当可能会crash。

image.png

一般情况下我们把可能为空的值声明为可选值解决。

struct Hotpot: Codable{
    var name: String
    var age: Int?
    var height: Double
    var cat: [Cat]
}

extension Hotpot {
    struct Cat: Codable {
        var name: String
        var age: Int
    }
}

let jsonString = """
[
  {
    "age" : 18,
    "cat" : [
      {
        "age" : 1,
        "name" : "cat"
      },
      {
        "age" : 10,
        "name" : "cat1"
      }
    ],
    "name" : "hotpot",
    "height" : 1.85
  },
  {
    "age" : null,
    "cat" : [
      {
        "age" : 1,
        "name" : "cat"
      },
      {
        "age" : 10,
        "name" : "cat1"
      }
    ],
    "name" : "hotpot",
    "height" : 1.85
  }
]
"""

let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
if let data = jsonData{
    let result = try? decoder.decode([Hotpot].self, from: data)
    print(result ?? "解析失败")
}
[SwiftProtocol.Hotpot(name: "hotpot", age: Optional(18), height: 1.85, cat: [SwiftProtocol.Hotpot.Cat(name: "cat", age: 1), SwiftProtocol.Hotpot.Cat(name: "cat1", age: 10)]), SwiftProtocol.Hotpot(name: "hotpot", age: nil, height: 1.85, cat: [SwiftProtocol.Hotpot.Cat(name: "cat", age: 1), SwiftProtocol.Hotpot.Cat(name: "cat1", age: 10)])]

元组类型

比如我们有一个坐标,后台服务器传给"location": [114.121344, 38.908766],这个时候用数组接显然不好维护。返回的数组怎么和模型对应上呢? 这个时候就需要自己写解析实现"init(from decoder: Decoder)"

struct Location: Codable {
    var x: Double
    var y: Double
    
    init(from decoder: Decoder) throws {
        //不解析当前的key,也就是解码时不要key值。
        var contaioner = try decoder.unkeyedContainer()
        //单方面把值给到x与y
        self.x = try contaioner.decode(Double.self)
        self.y = try contaioner.decode(Double.self)
    }
}

struct RawSeverResponse: Codable {
    var location: Location
}

let jsonString = """
{
    "location": [114.121344, 38.908766]
}
"""

let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(RawSeverResponse.self, from: jsonData!)
print(result)
RawSeverResponse(location: SwiftProtocol.Location(x: 114.121344, y: 38.908766))

继承

class Hotpot: Codable {
    var name: String?
}

class HotpotCat: Hotpot {
    var age: Int?
}

let jsonString = """
{
    "name": "hotpot",
    "age": 18
}
"""

let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(HotpotCat.self, from: jsonData!)
print(result.name)
print(result.age)

按照猜想上面的代码应该agename都能正常解析,看下输出:

Optional("hotpot")
nil

age没有解析?那么Codable放在子类上呢?

class Hotpot {
    var name: String?
}

class HotpotCat: Hotpot, Codable {
    var age: Int?
}

let jsonString = """
{
    "name": "hotpot",
    "age": 18
}
"""

let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(HotpotCat.self, from: jsonData!)
print(result.name)
print(result.age)
nil
Optional(18)

name没有解析。
这个时候就需要我们在派生类中重写init(from decoder: Decoder),如果要编码还需要encode(to encoder: Encoder)并且需要定义CodingKeys

class Hotpot: Codable {
    var name: String?
}

class HotpotCat: Hotpot {
    var age: Int?
    
    private enum CodingKeys: String,CodingKey{
        case name
        case age
     }
    
    required init(from decoder: Decoder) throws {
        try super.init(from: decoder)
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.age = try container.decode(Int.self, forKey: .age)
    }
    
    override 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)
        try super.encode(to: encoder)
    }
}

let jsonString = """
{
    "name": "hotpot",
    "age": 18
}
"""

let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(HotpotCat.self, from: jsonData!)
print(result.name)
print(result.age)
Optional("hotpot")
Optional(18)

为什么派生类不能自动解析?原理后面再分析。

协议

如果有协议的参与下解析会怎么样呢?

protocol HotpotProtocol: Codable {
    var name: String{ get set }
}

struct Hotpot: HotpotProtocol {
    var name: String
    var age: Int?
}

let jsonString = """
{
    "name": "hotpot",
    "age": 18
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(Hotpot.self, from: jsonData!)
print(result)
Hotpot(name: "hotpot", age: Optional(18))

可以看到能够正常解析。

key值不同

对于一些不符合规范的字段,我们一般是会给别名来解析,那么在Codable中怎么实现呢?
比如后台返回了这样的数据,正常情况下应该返回一个数组和对应的type。

let jsonString = """
{
  "01Type" : "hotpot",
  "item.1" : "mouse",
  "item.2" : "dog",
  "item.3" : "hotpot",
  "item.0" : "cat"
}
"""
//如果不需要编码,直接遵循 Decodable 就好了
struct Hotpot: Decodable {
    var type: String?
    let elements: [String]
    enum CodingKeys: String, CaseIterable, CodingKey {
        case type = "01Type"
        case item0 = "item.0"
        case item1 = "item.1"
        case item2 = "item.2"
        case item3 = "item.3"
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.type = try container.decode(String.self, forKey: .type)
        var element: [String]  = []
        //方式一:遍历 container
        for item in container.allKeys {
            switch item {
                case .item0,.item1,.item2,.item3:
                    element.append(try container.decode(String.self, forKey: item))
                default:
                    continue
            }

        }
        //方式二:遍历CodingKeys 和 contains比较装入集合
//        for item in CodingKeys.allCases {
//            guard container.contains(item) else {
//                continue
//            }
//            if item != .type {
//                element.append(try container.decode(String.self, forKey: item))
//            }
//        }
        self.elements = element
    }
}

let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(Hotpot.self, from: jsonData!)

print(result)
Hotpot(type: Optional("hotpot"), elements: ["cat", "mouse", "dog", "hotpot"])

源码解析

Decodable

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

对应的Decoder提供具体解码

public protocol Decoder {
    var codingPath: [CodingKey] { get }
    var userInfo: [CodingUserInfoKey : Any] { get }
    func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey
    func unkeyedContainer() throws -> UnkeyedDecodingContainer
    func singleValueContainer() throws -> SingleValueDecodingContainer
}

Encodable

public protocol Encodable {
    func encode(to encoder: Encoder) throws
}

对应的Encoder提供具体编码

public protocol Encoder {
    var codingPath: [CodingKey] { get }
    var userInfo: [CodingUserInfoKey : Any] { get }
    func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey
    func unkeyedContainer() -> UnkeyedEncodingContainer
    func singleValueContainer() -> SingleValueEncodingContainer
}

JSONDecoder

open class JSONDecoder {
    public enum DateDecodingStrategy {
        /// Defer to `Date` for decoding. This is the default strategy.
        case deferredToDate
        /// 距离 1970.01.01的秒数
        case secondsSince1970
        /// 距离 1970.01.01的毫秒数
        case millisecondsSince1970
       ///日期编码格式
        @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
        case iso8601
        ///后台自定义格式,这个时候可以创建自定义DataFormatter来解析
        case formatted(DateFormatter)
       ///自定义格式,提供一个闭包表达式
        case custom((Decoder) throws -> Date)
    }

    /// 二进制数据解码策略
    public enum DataDecodingStrategy {
        /// 默认
        case deferredToData
        /// base64
        case base64
        /// 自定义
        case custom((Decoder) throws -> Data)
    }

    /// 不合法浮点数编码策略
    public enum NonConformingFloatDecodingStrategy {
        /// Throw upon encountering non-conforming values. This is the default strategy.
        case `throw`
        /// Decode the values from the given representation strings.
        case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String)
    }

    /// key值编码策略
    public enum KeyDecodingStrategy {
        /// Use the keys specified by each type. This is the default strategy.
        case useDefaultKeys
        //指定去掉中间下划线,变成驼峰命名
        case convertFromSnakeCase
        //自定义
        case custom(([CodingKey]) -> CodingKey)
    }

    open var nonConformingFloatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy
    /// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.
    open var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy
    /// Contextual user-provided information for use during decoding.
    open var userInfo: [CodingUserInfoKey : Any]
    /// Initializes `self` with default strategies.
   ///默认init方法
    public init()
    /// Decodes a top-level value of the given type from the given JSON representation.
    /// - parameter type: The type of the value to decode.
    /// - parameter data: The data to decode from.
    /// - returns: A value of the requested type.
    /// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid JSON.
    /// - throws: An error if any value throws an error during decoding.
    open func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable
}

DateDecodingStrategy

以何种策略解析日期格式。

let jsonString = """
{
  "name" : "hotpot",
  "age" : 18,
  "date" : 1610390703
}
"""

struct Hotpot: Codable {
    var name: String
    var age: Double
    var date: Date
}

let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(Hotpot.self, from: jsonData!)
print(result)

deferredToDate

  "date" : 1610390703
decoder.dateDecodingStrategy = .deferredToDate
Hotpot(name: "hotpot", age: 18.0, date: 2052-01-12 18:45:03 +0000)

使用默认格式无法正确推导出时间。
secondsSince1970

decoder.dateDecodingStrategy = .secondsSince1970
Hotpot(name: "hotpot", age: 18.0, date: 2021-01-11 18:45:03 +0000)

millisecondsSince1970

  "date" : 1610390703000
decoder.dateDecodingStrategy = .millisecondsSince1970
Hotpot(name: "hotpot", age: 18.0, date: 2021-01-11 18:45:03 +0000)

iso8601

  "date" : "2021-01-11T19:20:20Z"
decoder.dateDecodingStrategy = .iso8601
Hotpot(name: "hotpot", age: 18.0, date: 2021-01-11 19:20:20 +0000)

formatted

  "date" : "2021/01/11 19:20:20"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
Hotpot(name: "hotpot", age: 18.0, date: 2021-01-11 11:20:20 +0000)

custom
custom相对比较灵活,可以做一些错误处理以及格式不固定的情况下使用。

  "date" : "1610390703"
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
    let container = try decoder.singleValueContainer()
    let dateStr = try container.decode(String.self)
    return Date(timeIntervalSince1970: Double(dateStr)!)
})
Hotpot(name: "hotpot", age: 18.0, date: 2021-01-11 18:45:03 +0000)

func decode源码:

    open func decode(_ type: T.Type, from data: Data) throws -> T {//泛型函数,同时当前泛型约束为遵循了Decodable的协议
        let topLevel: Any
        do {
            //JSONSerialization将二进序列化成json
           topLevel = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)
        } catch {
            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
        }
        // __JSONDecoder 传入序列化的json  和  self.options 编码策略创建decoder对象
        let decoder = __JSONDecoder(referencing: topLevel, options: self.options)
        //unbox 拆 topLevel 数据 返回解码后的value
        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
    }

1.这里是一个泛型函数,传入的参数T 要求遵守 Decodable 协议;
2.调用 JSONSerializationg 对当前 data 进行序列话为json数据;
3.通过__JSONDecoder传入json数据和编码策略创建decoder对象;
4.unbox解码json串,返回value
__JSONDecoder私有类源码(初始化方法):

   init(referencing container: Any, at codingPath: [CodingKey] = [], options: JSONDecoder._Options) {
        //内部类 _JSONDecodingStorage
        self.storage = _JSONDecodingStorage()
        //存放要解码的数据
        self.storage.push(container: container)
        self.codingPath = codingPath
        self.options = options
    }

_JSONDecodingStorage本身用数组存放数据,是一个容器

private struct _JSONDecodingStorage {
    // MARK: Properties

    /// The container stack.
    /// Elements may be any one of the JSON types (NSNull, NSNumber, String, Array, [String : Any]).
    private(set) var containers: [Any] = []

    // MARK: - Initialization

    /// Initializes `self` with no containers.
    init() {}

    // MARK: - Modifying the Stack

    var count: Int {
        return self.containers.count
    }

    var topContainer: Any {
        precondition(!self.containers.isEmpty, "Empty container stack.")
        return self.containers.last!
    }

    mutating func push(container: __owned Any) {
        self.containers.append(container)
    }

    mutating func popContainer() {
        precondition(!self.containers.isEmpty, "Empty container stack.")
        self.containers.removeLast()
    }
}

unbox源码:

    func unbox(_ value: Any, as type: T.Type) throws -> T? {
        return try unbox_(value, as: type) as? T
    }
    func unbox_(_ value: Any, as type: Decodable.Type) throws -> Any? {
        //日期格式
        if type == Date.self || type == NSDate.self {
            return try self.unbox(value, as: Date.self)
        } else if type == Data.self || type == NSData.self {
            return try self.unbox(value, as: Data.self)
        } else if type == URL.self || type == NSURL.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 || type == NSDecimalNumber.self {
            return try self.unbox(value, as: Decimal.self)
        } 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)
        }
    }

分析下日期格式的处理

image.png

这也就是我们设置不同的日期处理策略能够正常解析的原因。不同的策略走不同的分支解析。
_JSONStringDictionaryDecodableMarker

#if arch(i386) || arch(arm)
internal protocol _JSONStringDictionaryDecodableMarker {
    static var elementType: Decodable.Type { get }
}
#else
private protocol _JSONStringDictionaryDecodableMarker {
    static var elementType: Decodable.Type { get }
}
#endif

extension Dictionary : _JSONStringDictionaryDecodableMarker where Key == String, Value: Decodable {
    static var elementType: Decodable.Type { return Value.self }
}
  • 字典扩展遵循了这个协议,限制条件:Key == String, Value: Decodable
    _JSONStringDictionaryDecodableMarkerunbox:
    image.png

    是一个递归调用。unbox_就是最开始的区分类型(DateData……)的方法。

看一个例子:

struct Hotpot: Codable {
    var name: String
    var age: Double
}

let jsonString = """
{
  "name" : "hotpot",
  "age" : 18
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(Hotpot.self, from: jsonData!)
print(result)

对应的SIL代码:

image.png

  • 解码对应的key值通过CodingKeys去找,decodable约束的。
  • decode方法。
  • encode方法。

init方法源码中是:

   return try type.init(from: self)

传入的decoderself,这里的selfJSONDecoder
init(from SIL的实现:

image.png

查看_ JSONDecoder源码确实实现了Decoder协议
image.png

sil中调用的是_JSONDecoder实现的container方法:
image.png

这里返回的是KeyedDecodingContainer(container)对象。
KeyedDecodingContainer内容:
image.png

可以看到有对应每种类型的decode方法,意味着会根据类型匹配对应的方法解码。这么多方法苹果其实是用内部的一个工具生成的。具体是用源码中的Codable.swift.gyb文件。通过Codable.swift.gyb生成Codable.swift源文件。
Codable.swift.gyb文件内容:
image.png

集合中放了可编解码类型,%%代表语句的开始和结束。通过python控制的。相当于模板文件。
image.png

自己实现下init方法:

struct Hotpot: Codable {
    var name: String
    var age: Double
    
    //_JSONDecoder,返回container
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.age = try container.decode(Double.self, forKey: .age)
    }
}

可以看到对应的CodingKeysdecode正是系统生成调用的方法,CodingKeys系统帮我们生成了,这也就是我们可以在CodingKeys中操作key不一致的原因。

那么如果继承自OC的类呢?
HPOCModel是一个OC类:

@interface HPOCModel : NSObject

@property (nonatomic, assign) double height;

@end

@implementation HPOCModel

@end
class Hotpot:HPOCModel, Decodable {
    var name: String
    var age: Double
    
     private enum CodingKeys: String,CodingKey{
        case name
        case age
        case height
     }

    //_JSONDecoder,返回container
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.age = try container.decode(Double.self, forKey: .age)
        super.init()
        self.height = try container.decode(Double.self, forKey: .height)
    }
}

let jsonString = """
{
  "name" : "hotpot",
  "age" : 18,
  "height":1.85
}
"""

let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()

let result = try decoder.decode(Hotpot.self, from: jsonData!)
print(result.height)
1.85

这个时候init方法就要我们自己去实现了。

整个流程:


image.png

JSONEncoder


class Hotpot: Codable {
    var name: String?
    var age: Int?
}

class Cat: Hotpot {
    var height: Double?
}

let cat = Cat()
cat.name = "cat"
cat.age = 18
cat.height = 1.85

let enCoder = JSONEncoder()
let jsonData = try enCoder.encode(cat)
let jsonStr = String(data: jsonData, encoding: .utf8)
print(jsonStr)
Optional("{\"name\":\"cat\",\"age\":18}")

源码分析:
encode方法:

    open func encode(_ value: T) throws -> Data {
        let encoder = _JSONEncoder(options: self.options)
        //先box包装成string
        guard let topLevel = try encoder.box_(value) else {
            throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) did not encode any values."))
        }
        //序列化
        let writingOptions = JSONSerialization.WritingOptions(rawValue: self.outputFormatting.rawValue).union(.fragmentsAllowed)
        //返回data
        do {
            return try JSONSerialization.data(withJSONObject: topLevel, options: writingOptions)
        } catch {
            throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Unable to encode the given top-level value to JSON.", underlyingError: error))
        }
    }
  • 在这里真正处理数据的是_JSONEncoder
  • box根据不同数据类型,把value包装成对应的数据类型;
  • JSONSerialization来返回data数据。

_JSONEncoder:

   //实现Encoder的方法
    public func container(keyedBy: Key.Type) -> KeyedEncodingContainer {
        // If an existing keyed container was already requested, return that one.
        let topContainer: NSMutableDictionary
        if self.canEncodeNewValue {
            // We haven't yet pushed a container at this level; do so here.
            topContainer = self.storage.pushKeyedContainer()
        } else {
            guard let container = self.storage.containers.last as? NSMutableDictionary else {
                preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.")
            }

            topContainer = container
        }

        let container = _JSONKeyedEncodingContainer(referencing: self, codingPath: self.codingPath, wrapping: topContainer)
       //返回KeyedEncodingContainer,最终KeyedEncodingContainer encode根据数据类型编码数据
        return KeyedEncodingContainer(container)
    }
  • _JSONEncoder遵循Encoder协议实现了container方法,最终KeyedEncodingContainer提供对应数据类型encode方法编码数据。

box_:

image.png

我们自定义数据类型会走到value.encode,参数self就是_JSONEcoder。正好的解码过程相反。
最终都会都到value is _JSONStringDictionaryEncodableMarker
_JSONStringDictionaryEncodableMarker:
image.png

在最开始的例子中,Cat中的height并没有被编码出来。
先看下继承和协议遵循关系:

image.png

那么Hotpot遵循了Codable协议,编译器默认生成了encode方法,在调用过程中由于Cat中没有对应encode方法,所以会去父类中找。所以编码结果中没有height
SIL验证下:

class Hotpot : Decodable & Encodable {
  @_hasStorage @_hasInitialValue var name: String? { get set }
  @_hasStorage @_hasInitialValue var age: Int? { get set }
  @objc deinit
  init()
  enum CodingKeys : CodingKey {
    case name
    case age
    @_implements(Equatable, ==(_:_:)) static func __derived_enum_equals(_ a: Hotpot.CodingKeys, _ b: Hotpot.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)
  }
  required init(from decoder: Decoder) throws
  func encode(to encoder: Encoder) throws
}

@_inheritsConvenienceInitializers class Cat : Hotpot {
  @_hasStorage @_hasInitialValue var height: Double? { get set }
  @objc deinit
  override init()
  required init(from decoder: Decoder) throws
}

Hotpot中有initencode方法。而在Cat中编译器覆盖了init(from decoder: Decoder)方法,而没有覆盖encode。这也就解释了解码能够无缝对接,编码不行的原因。

那么我们需要在子类中重写encode方法:

class Hotpot: Codable {
    var name: String?
    var age: Int?
}

class Cat: Hotpot {
    var height: Double?
    
    enum CodingKeys: String,CodingKey {
        case height
    }
    
    override func encode(to encoder: Encoder) throws {
       var container = encoder.container(keyedBy: CodingKeys.self)
       try container.encode(height, forKey: .height)
       try super.encode(to: encoder)
    }
}

let cat = Cat()
cat.name = "cat"
cat.age = 18
cat.height = 1.8

let enCoder = JSONEncoder()
let jsonData = try enCoder.encode(cat)
let jsonStr = String(data: jsonData, encoding: .utf8)
Optional("{\"name\":\"cat\",\"age\":18,\"height\":1.8}")

当然这里要注意数据的精度问题,前面用法里面已经说过了。

有一个小的点try super.encode(to: encoder)如果用自己的编码器呢?

try super.encode(to: container.superEncoder())
Optional("{\"super\":{\"name\":\"cat\",\"age\":18},\"height\":1.8}")

编码值中多了super。所以这里最好就传当前类的encoder。否则多次编解码会错误解析不到数据。

对于上面编码后的值,再解码看看:

let cat2 = try JSONDecoder().decode(Cat.self, from: jsonData)
Optional("{\"name\":\"cat\",\"age\":18,\"height\":1.8}")
SwiftProtocol.Cat
(lldb) po cat2.name
▿ Optional
  - some : "cat"

(lldb) po cat2.age
▿ Optional
  - some : 18

(lldb) po cat2.height
nil

可以看到height没有解析出来。这和前面的结论一样,看下SIL:

image.png

在最后确实调用了Hotpotdecode方法,所以父类中的数据解析没有问题,但是heightSIL中只有创建没有赋值。没有创建Cat自己本身的container。再看下和Hotpot的区别:
image.png

可以看到区别是仅仅Cat中根本没有创建Container,仅是调用了父类的decode方法,这也是为什么没有解析出来自己的属性。
(Swift源码中继承后在vscode中调试报错,只能编译一份Xcode源码再调试了,后面再补充吧).
image.png

源码调试待补充
我们自己实现catinit方法看下sil代码:

class Hotpot:Codable {
    var name: String?
    var age: Int?
}

class Cat: Hotpot {
    var height: Double?
    enum CodingKeys: String, CodingKey {
        case height
    }
    required init(from decoder: Decoder) throws {
        var container = try decoder.container(keyedBy: CodingKeys.self)
        self.height = try container.decode(Double.self, forKey: .height)
        try! super.init(from: decoder)
    }
}


let jsonString = "{\"name\" : \"hotpot\",\"age\" : 18,\"height\":1.8}"

let cat2 = try JSONDecoder().decode(Cat.self, from: jsonString.data(using: .utf8)!)
print(cat2)
SwiftProtocol.Cat
(lldb) po cat2.age
▿ Optional
  - some : 18

(lldb) po cat2.height
▿ Optional
  - some : 1.8

这样就能正常解析了,注释掉不必要的代码看下SIL

image.png

  • Cat创建了自己的container
  • bb1中对height进行了解析赋值;
  • bb2分支中调用了Hotpotinit方法。

编解码多态中的应用

protocol Hotpot: Codable {
    var age: String { get set }
    var name: String { get set }
}

struct Cat: Hotpot {
    var age: String
    var name: String
}

struct Dog: Hotpot {
    var age: String
    var name: String
}

struct Animal: Codable{
    var animals: [Hotpot]
    var desc: String
    
    enum CodingKeys: String, CodingKey {
        case animals
        case desc
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(animals, forKey: .animals)
        try container.encode(desc, forKey: .desc)
    }
}

上面的例子中,Animalanimals存储的是遵循Hotpot协议的属性:

image.png

看到直接报错,有自定义类型就需要我们自己实现initdecode方法了。并且animals不能解析,需要各自的类型(CatDog)自己去实现相应的initdecode方法。这在类型比较少的时候还能接受,类型多了的情况下呢?
那么可以用一个中间层HotpotBox去专门做解析:

protocol Hotpot {
    var age: Int { get set }
    var name: String { get set }
}

struct HotpotBox: Hotpot, Codable {
    var age: Int
    var name: String
    
    init(_ hotpot: Hotpot) {
        self.age = hotpot.age
        self.name = hotpot.name
    }
}

struct Cat: Hotpot {
    var age: Int
    var name: String
}

struct Dog: Hotpot {
    var age: Int
    var name: String
}

struct Animal: Codable{
    var animals: [HotpotBox]
    var desc: String
}

调用

let hotpots: [Hotpot] = [Cat(age: 18, name: "cat"),Dog(age: 28, name: "dog")]
//对hotpots数组中的集合执行HotpotBox.init,也就是用hotpots初始化animals
let animal = Animal(animals: hotpots.map(HotpotBox.init), desc: "Animal")
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
let jsonData = try jsonEncoder.encode(animal)

if let jsonString = String(data: jsonData, encoding: .utf8) {
    print(jsonString)
}

输出

{
  "animals" : [
    {
      "age" : 18,
      "name" : "cat"
    },
    {
      "age" : 28,
      "name" : "dog"
    }
  ],
  "desc" : "Animal"
}

decode一下:

let animal1: Animal = try JSONDecoder().decode(Animal.self, from: jsonData)
print(animal1)

输出:

Animal(animals: [SwiftProtocol.HotpotBox(age: 18, name: "cat"), SwiftProtocol.HotpotBox(age: 28, name: "dog")], desc: "Animal")

可以看到这里输出的是HotpotBox,如果要还原当前的类型信息呢?
1.可以写一个对应的unBox来还原数据:

struct Cat: Hotpot {
    var age: Int
    var name: String
    static func unBox(_ value: HotpotBox) -> Cat {
        var cat = Cat(age: value.age, name: value.name)
        return cat
    }
}
[SwiftProtocol.Cat(age: 18, name: "cat"), SwiftProtocol.Cat(age: 28, name: "dog")]

2.编码过程中将类型信息编码进去。

enum HotpotType: String, Codable {
    case cat
    case dog
    
    var metadata: Hotpot.Type {
        switch self {
            case .cat:
                return Cat.self
            case .dog:
                return Dog.self
        }
    }
}

protocol Hotpot: Codable {
    static var type: HotpotType { get }//计算属性,编解码过程中不会被编码进去。
    var age: Int { get set }
    var name: String { get set }
}


struct HotpotBox: Codable {
    var hotpot: Hotpot
    
    init(_ hotpot: Hotpot) {
        self.hotpot = hotpot
    }
    
    private enum CodingKeys: String, CodingKey {
        case type
        case hotpot
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(HotpotType.self, forKey: .type)
        self.hotpot = try type.metadata.init(from: container.superDecoder(forKey: .hotpot))
    }
    
    func encode(to encoder: Encoder) throws {
        var container = try encoder.container(keyedBy: CodingKeys.self)
        try container.encode(type(of: hotpot).type, forKey: .type)
        try hotpot.encode(to: container.superEncoder(forKey: .hotpot))
    }
}

struct Cat: Hotpot {
    static var type: HotpotType = .cat
    var age: Int
    var name: String
}

struct Dog: Hotpot {
    static var type: HotpotType {
        .dog
    }
    var age: Int
    var name: String
}

struct Animal: Codable {
    var animals: [HotpotBox]
    var desc: String
}


let hotpots: [Hotpot] = [Cat(age: 18, name: "cat"),Dog(age: 28, name: "dog")]
//对hotpots数组中的集合执行HotpotBox.init,也就是用hotpots初始化animals
let animal = Animal(animals: hotpots.map(HotpotBox.init), desc: "Animal")
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
let jsonData = try jsonEncoder.encode(animal)

if let jsonString = String(data: jsonData, encoding: .utf8) {
    print(jsonString)
}

let animal1: Animal = try JSONDecoder().decode(Animal.self, from: jsonData)
print(animal1)

输出

{
  "animals" : [
    {
      "type" : "cat",
      "hotpot" : {
        "age" : 18,
        "name" : "cat"
      }
    },
    {
      "type" : "dog",
      "hotpot" : {
        "age" : 28,
        "name" : "dog"
      }
    }
  ],
  "desc" : "Animal"
}
Animal(animals: [SwiftProtocol.HotpotBox(hotpot: SwiftProtocol.Cat(age: 18, name: "cat")), SwiftProtocol.HotpotBox(hotpot: SwiftProtocol.Dog(age: 28, name: "dog"))], desc: "Animal")

这个时候看到有无用信息,再改造下:

self.hotpot = try type.metadata.init(from: container.superDecoder(forKey: .hotpot))

改为

 self.hotpot = try type.metadata.init(from: decoder)
{
  "animals" : [
    {
      "type" : "cat",
      "age" : 18,
      "name" : "cat"
    },
    {
      "type" : "dog",
      "age" : 28,
      "name" : "dog"
    }
  ],
  "desc" : "Animal"
}
Animal(animals: [SwiftProtocol.HotpotBox(hotpot: SwiftProtocol.Cat(age: 18, name: "cat")), SwiftProtocol.HotpotBox(hotpot: SwiftProtocol.Dog(age: 28, name: "dog"))], desc: "Animal")

如果不想要type

    func encode(to encoder: Encoder) throws {
        var container = try encoder.container(keyedBy: CodingKeys.self)
//        try container.encode(type(of: hotpot).type, forKey: .type)
        try hotpot.encode(to: encoder)
    }

去掉就好了,不过在解码的时候就没有type相关信息了。需要根据需求灵活处理

{
  "animals" : [
    {
      "age" : 18,
      "name" : "cat"
    },
    {
      "age" : 28,
      "name" : "dog"
    }
  ],
  "desc" : "Animal"
}

另外一种方式

protocol Meta: Codable {
    associatedtype Element
    
    static func metatype(for typeString: String) -> Self
    var type: Decodable.Type { get }
}

struct MetaObject: Codable {
    let object: M.Element
    
    init(_ object: M.Element) {
        self.object = object
    }
    
    enum CodingKeys: String, CodingKey {
        case metatype
        case object
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let typeStr = try container.decode(String.self, forKey: .metatype)
        let metatype = M.metatype(for: typeStr)
        
        let superDecoder = try container.superDecoder(forKey: .object)
        let obj = try metatype.type.init(from: superDecoder)
        guard let element = obj as? M.Element else {
            fatalError()
        }
        self.object = element
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        let typeStr = String(describing: type(of: object))
        try container.encode(typeStr, forKey: .metatype)
        
        let superEncoder = container.superEncoder(forKey: .object)
        let encodable = object as? Encodable
        try encodable?.encode(to: superEncoder)
    }
}


enum HotpotType: String, Meta {
    typealias Element = Hotpot
    case cat = "Cat"
    case dog = "Dog"

    static func metatype(for typeString: String) -> HotpotType {
        guard let metatype = self.init(rawValue: typeString) else {
            fatalError()
        }
        return metatype
    }

    var type: Decodable.Type {
        switch self {
            case .cat:
                return Cat.self
            case .dog:
                return Dog.self
        }
    }
}

class Hotpot: Codable {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class Cat: Hotpot {
    var height: Double
    init(name: String, age: Int, height: Double) {
        self.height = height
        super.init(name: name, age: age)
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        height = try container.decode(Double.self, forKey: .height)
        try super.init(from: decoder)
    }
    
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(height, forKey: .height)
        try super.encode(to: encoder)
    }
    
    enum CodingKeys: String, CodingKey {
        case height
    }
}

class Dog: Hotpot {
    var weight: Double
    
    init(name: String, age: Int, weight: Double) {
        self.weight = weight
        super.init(name: name, age: age)
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        weight = try container.decode(Double.self, forKey: .weight)
    
        let superDecoder = try container.superDecoder()
        try super.init(from: superDecoder)
    }
    
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(weight, forKey: .weight)
        
        let superdecoder = container.superEncoder()
        try super.encode(to: superdecoder)
    }
    
    enum CodingKeys: String, CodingKey {
        case weight
    }
}


let hotpot: Hotpot = Cat(name: "cat", age: 18, height: 1.8)
let jsonData = try JSONEncoder().encode(MetaObject(hotpot))
if let str = String(data: jsonData, encoding: .utf8) {
    print(str)
}

let decode: MetaObject = try JSONDecoder().decode(MetaObject.self, from: jsonData)

print(decode)
{"metatype":"Cat","object":{"name":"cat","age":18,"height":1.8}}

MetaObject(object: SwiftProtocol.Cat)
  • 中间层
  • 记录metadata
  • Type

在编解码过程中遇到多态的方式,可以通过中间层去解决处理。

SwiftUI中Codable

当我们在SwiftUI中定义如下代码会直接报错(ObservableObjectPublished可以实现组件数据一致性,而且可以自动更新):

class Hotpot: ObservableObject, Codable {
    @Published var name = "cat"
}

image.png

这实际上是Swift的一项基本原则,我们不能说var names: Set,因为Set是通用类型,必须创建Set的实例。Published也是同理,不能自己创建Published的实例,而是创建一个Published的实例。
这时候就需要我们自己编写编解码操作了:

class Hotpot: ObservableObject, Codable {
    @Published var name = "cat"
    enum CodingKeys: CodingKey {
        case name
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
    }
}

比较

Codable:继承多态模式下需要自己编写编解码。
HandyJSON:内存赋值的方式进行编解码操作,依赖metadata。如果metadata变化后会出现问题,迁移成本比较高。
SwiftyJSON:需要使用下标的方式取值。
ObjectMapper:需要手动对每一个属性提供映射关系。
如果项目中数据模型继承和多态比较少建议直接用Codable,否则就用HandyJSON吧。相对来说Codable效率更高。
具体的对比以后再补充吧。

参考:
https://zhuanlan.zhihu.com/p/50043306
https://www.jianshu.com/p/f4b3dce8bd6f
https://www.jianshu.com/p/1f194f09599a
https://www.jianshu.com/p/bdd9c012df15
https://www.jianshu.com/p/6db40c4c0ff9
https://www.jianshu.com/p/5dab5664a621?utm_campaign
https://www.jianshu.com/p/bf56a74323fa
https://www.jianshu.com/p/0088b62f698a
enum解析
解析框架对比
Double精度问题
NSDecimalNumber

你可能感兴趣的:(Swift底层探索:Codable)