Swift数据解析(第三篇) - Codable源码学习

这是Swift数据解析方案的系列文章:

Swift数据解析(第一篇) - 技术选型

Swift数据解析(第二篇) - Codable 上

Swift数据解析(第二篇) - Codable 下

Swift数据解析(第三篇) - Codable源码学习

Swift数据解析(第四篇) - SmartCodable 上

Swift数据解析(第四篇) - SmartCodable 下

一. 如何找到源码

Swift的源码是公开在github上的,可以通过 Swift源码 该链接查看。

跟Codable有关的源码在这两个文件中 Codable.swiftJSONDecoder.swift

二. 如何阅读源码

我对codable相关的源码读了三遍。这是因为:

第一遍

直接读这两个文件代码,代码量太多,几乎没收获。

第二遍

尝试对这两个文件进行拆分,按照类/结构体直接对相互关系,分为三大类。这才理解了各个类之间的作用。

你可以下载 Codable源码拆分

第三遍

虽然读过两遍之后,大致理解了各个类/结构体的作用。但是看到更底层的逻辑,比如带键解码容器的实现, 使用了4层设计KeyedDecodingContainerProtocol, KeyedDecodingContainer, _KeyedDecodingContainerBox, _KeyedDecodingContainerBase。 理解起来非常吃力。

我更希望知道这些 类/协议/结构体 是如何工作的?各自承担了什么职责?这么设计的目的是什么?

于是我从最开始的调用代码开始,跟随着代码执行的顺序,一步步的理解它。

三. decode过程

我们以这个简单的示例为开始:

swift 复制代码
 do {
    let decoder = JSONDecoder()
    let feed = try decoder.decode(Person.self, from: jsonData)
    print(feed)
 } catch let error {
    print(error)
 }

JSONDecoder

swift 复制代码
 open class JSONDecoder {
 ​
    public init() {}
 ​
    open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
     
        let topLevel: Any
        do {
            topLevel = try JSONSerialization.jsonObject(with: data)
        } 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: T.self) else {
            throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
        }
 ​
        return value
    }
 }

JSONDecoder是对外的调用类,内部的实现靠 _JSONDecoder 完成。

  1. 入参的泛型 T 必须遵循 Decodable 协议。
  2. 使用 JSONSerializationdata 数据序列化。
  3. 调用内部类 _JSONDecoder ,依赖数据字典和编码策略生成 decoder 对象。
  4. decoder 对象调用 unbox方法,进行解码并返回解码值。

_JSONDecoder

_JSONDecoder是用来解码操作的内部类,它遵循了Decoder协议。 具体代码

swift 复制代码
 class _JSONDecoder : Decoder {
 ​
    fileprivate var storage: _JSONDecodingStorage
     
    fileprivate let options: JSONDecoder._Options
     
    fileprivate(set) public var codingPath: [CodingKey]
     
    public var userInfo: [CodingUserInfoKey : Any] {
        return self.options.userInfo
    }
     
    init(referencing container: Any, at codingPath: [CodingKey] = [], options: JSONDecoder._Options) {
        ......
    }
 ​
    public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
        ......
    }
     
    public func unkeyedContainer() throws -> UnkeyedDecodingContainer {
        ......
    }
     
    public func singleValueContainer() throws -> SingleValueDecodingContainer {
        return self
    }
 }

init方法

swift 复制代码
 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, 并存储当前的解码数据。分别初始化 optionscodingPath

_JSONDecodingStorage

swift 复制代码
 fileprivate struct _JSONDecodingStorage {
    /// The container stack.
    /// Elements may be any one of the JSON types (NSNull, NSNumber, String, Array, [String : Any]).
    private(set) fileprivate var containers: [Any] = []
     
    fileprivate init() {}
 ​
    // MARK: - Modifying the Stack
 ​
    fileprivate var count: Int {
        return self.containers.count
    }
 ​
    fileprivate var topContainer: Any {
        precondition(self.containers.count > 0, "Empty container stack.")
        return self.containers.last!
    }
 ​
    fileprivate mutating func push(container: Any) {
        self.containers.append(container)
    }
 ​
    fileprivate mutating func popContainer() {
        precondition(self.containers.count > 0, "Empty container stack.")
        self.containers.removeLast()
    }
 }

主要是用来管理要解码的数据。

  • 解码前:通过push方法存入containers里面。
  • 解码时:通过topContainer获取需要解码的数据。
  • 解码后:调用popContainer方法移除记录的数据。

unbox

unbox方法用于解码操作,匹配对应的类型然后执行条件分支。

swift 复制代码
 extension _JSONDecoder {
    fileprivate func unbox<T : Decodable>(_ value: Any, as type: T.Type) throws -> T? {
        // 判断type的类型,针对不同的类型,调用不同的方法。
        let decoded: T
        if T.self == Date.self || T.self == NSDate.self {
            guard let date = try self.unbox(value, as: Date.self) else { return nil }
            decoded = date as! T
        } else if T.self == Data.self || T.self == NSData.self {
            guard let data = try self.unbox(value, as: Data.self) else { return nil }
            decoded = data as! T
        } else if T.self == URL.self || T.self == 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."))
            }
             
            decoded = (url as! T)
        } else if T.self == Decimal.self || T.self == NSDecimalNumber.self {
            guard let decimal = try self.unbox(value, as: Decimal.self) else { return nil }
            decoded = decimal as! T
        } else {
             
            self.storage.push(container: value)
            decoded = try T(from: self)
            self.storage.popContainer()
        }
         
        return decoded
    }
 }
 
 
 /// 如果类型是String
 fileprivate func unbox(_ value: Any, as type: String.Type) throws -> String? {
    guard !(value is NSNull) else { return nil }
    guard let string = value as? String else {
        throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
    }
    return string
}

/// 如果类型是Bool
fileprivate func unbox(_ value: Any, as type: Bool.Type) throws -> Bool? { }

/// 还有其他几种类型,不再一一列举

在该方法中,通过判断要解码的类型type,决定使用哪个解码方法。我们是使用的model解码,会进入else逻辑中

php 复制代码
 self.storage.push(container: value)
 decoded = try T(from: self)
 self.storage.popContainer()

container

swift 复制代码
 public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
    // 最后一个推入的json数据 是否为 null
    guard !(self.storage.topContainer is NSNull) else {
        throw DecodingError.valueNotFound(
        KeyedDecodingContainer<Key>.self,
        DecodingError.Context(codingPath:self.codingPath,
        debugDescription: "Cannot get keyed decoding container -- found null value instead."))
    }
     
    // 最后一个json数据是否为字典类型
    guard let topContainer = self.storage.topContainer as? [String : Any] else {
        throw DecodingError._typeMismatch(at: self.codingPath, expectation: [String : Any].self, reality: self.storage.topContainer)
    }
     
    let container = _JSONKeyedDecodingContainer<Key>(referencing: self, wrapping: topContainer)
    return KeyedDecodingContainer(container)
 }

四. 解码异常情况

JSONDecoder中的decode方法

swift 复制代码
 open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
     
     
    let topLevel: Any
    do {
        topLevel = try JSONSerialization.jsonObject(with: data)
    } 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: T.self) else {
        throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
    }
 ​
    return value
 }

数据序列化失败: 数据是非法的json。

_JSONDecoder.unbox 失败:给定的数据不包含顶级值。

_JSONKeyedDecodingContainer

swift 复制代码
 public func decode<T : Decodable>(_ type: T.Type, forKey key: Key) throws -> T {
    guard let entry = self.container[key.stringValue] else {
        throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key (key) ("(key.stringValue)")."))
    }
 ​
    self.decoder.codingPath.append(key)
    defer { self.decoder.codingPath.removeLast() }
 ​
    guard let value = try self.decoder.unbox(entry, as: T.self) else {
        throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected (type) value but found null instead."))
    }
 ​
    return value
 }
  • 键缺失的情况的异常
  • null值的异常。

unbox

swift 复制代码
 fileprivate func unbox(_ value: Any, as type: Int.Type) throws -> Int? {
    guard !(value is NSNull) else { return nil }
     
    guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else {
        throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
    }
 ​
    let int = number.intValue
    guard NSNumber(value: int) == number else {
        throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <(number)> does not fit in (type)."))
    }
     
    return int
 }
  • 类型不匹配时的异常
  • 数据损坏时候的异常。
  • 我们将针对这些异常提供兼容代码,避免一个字段解析错误导致整个解析失败。

五. 源码阅读过程中的疑惑

疑惑1: 系统的提供的数据类型(Int / String / Bool等)为什么可以直接进行解码?

数据类型默认实现了Codable协议。

swift 复制代码
extension Bool : Codable {
    @_inlineable // FIXME(sil-serialize-all)
    public init(from decoder: Decoder) throws {
        self = try decoder.singleValueContainer().decode(Bool.self)
    }

    @_inlineable // FIXME(sil-serialize-all)
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(self)
    }
}

疑惑2: decodeIfPresent 和 decode 两个方法什么时候会调用?

在Swift的Codable中,使用decodeIfPresentdecode的区别在于处理的属性是否可选值(Optional)。

  • decodeIfPresent方法用于解码可选值,如果解码成功,则返回解码后的值,如果解码失败,则返回nil
  • decode方法用于解码非可选值,如果解码成功,则返回解码后的值,如果解码失败,则抛出一个错误。

因此,当你需要解码一个可能为nil的值时,你可以使用decodeIfPresent方法。而当你需要解码一个非可选值时,你应该使用decode方法。

在Codable的源码中,调用decodeIfPresent还是decode方法是由编译器在编译时根据上下文来决定的。编译器会根据解码值的类型和键的存在与否来选择调用适当的方法。

疑惑3: 使用decodeIfPresent解码,遇到类型不匹配的情况还是会抛出异常?

swift 复制代码
public extension KeyedDecodingContainerProtocol {
    public func decodeIfPresent(_ type: Bool.Type, forKey key: Key) throws -> Bool? {
        guard try self.contains(key) && !self.decodeNil(forKey: key) else { return nil }
        return try self.decode(Bool.self, forKey: key)
    }
}

在decodeIfPresent的实现中,进行了这样两次的判断:

  • 是否包含这个key:contains(key)
  • 是否为nil:decodeNil(forKey: key)

没有针对类型不匹做处理。

疑惑4: 枚举值是如何编码和解码的?

swift 复制代码
public extension RawRepresentable where RawValue == Int, Self : Encodable {
    @_inlineable // FIXME(sil-serialize-all)
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(self.rawValue)
    }
}

public extension RawRepresentable where RawValue == Int, Self : Decodable {
    @_inlineable // FIXME(sil-serialize-all)
    public init(from decoder: Decoder) throws {
      let decoded = try decoder.singleValueContainer().decode(RawValue.self)
      guard let value = Self(rawValue: decoded) else {
          throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Cannot initialize \(Self.self) from invalid \(RawValue.self) value \(decoded)"))

      }
      self = value
    }
}

先按照枚举项的类型decode出来值let decoded = try decoder.singleValueContainer().decode(RawValue.self), 然后通过rawValue生成对应的枚举项。 这个过程会抛出两种异常情况: decode失败转换枚举项失败

疑惑5: URL是如何解码的?

swift 复制代码
fileprivate func unbox<T : Decodable>(_ value: Any, as type: T.Type) throws -> T? {

    ......
    } else if T.self == URL.self || T.self == 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."))
        }
        decoded = (url as! T)
    }
    ......

    return decoded
}

unbox(value, as: String.self) 出来String类型的值, 如果转URL失败抛出异常。

疑问6: 遵守Codable协议没有实现对应的协议方法,为什么不报错?

实现一个简单编解码只需要遵守Codable就行了,Codable书写起来很方便。这在个美好的体验中,会有这样的疑惑: 为什么会这么方便? 协议方法是谁来实现的,怎么实现的?

swift 复制代码
 public protocol Encodable {
    func encode(to encoder: Encoder) throws
 }
 ​
 public protocol Decodable {
    init(from decoder: Decoder) throws
 }
 ​
 public typealias Codable = Decodable & Encodable

跟OC中的Clang一样,Swift代码在编译过程中也会转换成中间码,即:SIL

Swift编程语言是在LLVM上构建,并且使用LLVM IR和LLVM的后端去生成代码。但是Swift编译器还包含新的高级别的中间语言,称为SILSIL会对Swift进行较高级别的语义分析和优化。

可以通过 swiftc SomeOne.swift -emit-sil 将该代码

ini 复制代码
 ## SomeOne.swift 中的代码
 struct SomeOne: Codable {
    var name: String = ""
    var age: Int = 0
 }

转换成SIL中间码:

swift 复制代码
 sil_stage canonical
 ​
 import Builtin
 import Swift
 import SwiftShims
 ​
 struct SomeOne : Decodable & Encodable {
  @_hasStorage @_hasInitialValue var name: String { get set }
  @_hasStorage @_hasInitialValue var age: Int { get set }
  enum CodingKeys : CodingKey {
    case name
    case age
    @_implements(Equatable, ==(_:_:)) static func __derived_enum_equals(_ a: SomeOne.CodingKeys, _ b: SomeOne.CodingKeys) -> Bool
    func hash(into hasher: inout Hasher)
    init?(stringValue: String)
    init?(intValue: Int)
    var hashValue: Int { get }
    var intValue: Int? { get }
    var stringValue: String { get }
  }
  func encode(to encoder: Encoder) throws
  init()
  init(from decoder: Decoder) throws
  init(name: String = "", age: Int = 0)
 }

Xcode 在编译Swift代码的时,会对遵守codable协议的代码自动实现 func encode(to encoder: Encoder) throwsinit(from decoder: Decoder) throws

这种编译成面的隐式处理,简化了代码的同时,也提高了阅读成本。

疑问7: 为什么使用CodingKeys自定义模型中的key?

swift 复制代码
 enum CodingKeys : CodingKey {
    case name
    case age
    @_implements(Equatable, ==(_:_:)) static func __derived_enum_equals(_ a: SomeOne.CodingKeys, _ b: SomeOne.CodingKeys) -> Bool
    func hash(into hasher: inout Hasher)
    init?(stringValue: String)
    init?(intValue: Int)
    var hashValue: Int { get }
    var intValue: Int? { get }
    var stringValue: String { get }
  }

如上所示: SIL中间码会对遵守Codable的代码默认生成CodingKeys,并且会根据CodingKeys处理编解码的映射关系。 同样也可以解释为什么自定义的CodingKeys需要声明在结构体内部。

相关推荐
袁代码11 小时前
Swift 开发教程系列 - 第10章:泛型
开发语言·ios·swift·ios开发
袁代码11 小时前
Swift 开发教程系列 - 第12章:协议与协议扩展
开发语言·ios·swift·ios开发
袁代码14 小时前
SwiftUI开发教程系列 - 第1章:简介与环境配置
开发语言·ios·swiftui·swift·ios开发
西瓜本瓜@20 小时前
iPhone 17版本的开发者权限如何开启?
开发语言·ios·iphone
海绵波波10720 小时前
Webserver(5.4)项目整体
c++·ios·iphone
HH思️️无邪20 小时前
iOS SmartCodable 替换 HandyJSON 适配记录
ios·swift
chaosama1 天前
禁止uni小程序ios端上下拉伸(橡皮筋效果)
ios·小程序
Zender Han1 天前
Flutter自定义矩形进度条实现详解
android·flutter·ios
S0linteeH1 天前
iOS 18.2 六大新功能外媒實測|ChatGPT進化版SIRI、自製Genmoji
ios
DisonTangor2 天前
苹果发布iOS 18.2首个公测版:Siri接入ChatGPT、iPhone 16拍照按钮有用了
ios·chatgpt·iphone