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需要声明在结构体内部。

相关推荐
二流小码农3 小时前
鸿蒙开发:DevEcoStudio中的代码提取
android·ios·harmonyos
Digitally9 小时前
如何用4 种可靠的方法更换 iPhone(2025 年指南)
ios·iphone
97650333512 小时前
iOS 审核 cocos 4.3a【苹果机审的“分层阈值”设计】
flutter·游戏·unity·ios
I烟雨云渊T12 小时前
iOS Alamofire库的使用
ios
程序员老刘·12 小时前
iOS 26 beta1 真机无法执行hot reload
flutter·ios·跨平台开发·客户端开发
EndingCoder12 小时前
React Native 构建与打包发布(iOS + Android)
android·react native·ios
程序员小刘13 小时前
HarmonyOS 5鸿蒙多端编译实战:从Android/iOS到HarmonyOS 5 的跨端迁移指南详
android·ios·华为·harmonyos
I烟雨云渊T13 小时前
iOS swiftUI的实用举例
ios·swiftui·swift
getapi14 小时前
将 App 安装到 iPhone 真机上测试
ios·iphone
90后的晨仔1 天前
RxSwift 中的 `Single`:单元素响应式编程简单实践
ios