Codable原理解析

530 阅读13分钟

我正在参加「掘金·启航计划」

概述

在使用OC开发过程中,我们可以使用MJExtension、YYModel等三方库进行json模型解析工作,这些三方库都是利用OC的runtime机制,取出类的属性并使用kvc机制映射字段。

但是Swift并没有runtime,怎么去进行模型解析呢?

Swift4之前大家可能选择使用HandyJson、KaKaJson等来进行模型解析。Swift的类/结构体等都有属于自己的metadata,这里面记录了类的信息。由于metadata并不对外开放,所以这些三方库仿照系统自己实现了一份metadata,然后把类指向自定义的metadata,映射的时候直接根据对应的指针地址进行内存赋值。这种实现方式扩展性很好,效率也高。但是由于Swift5之前ABI不稳定,每一次大版本升级系统都可能对metadata进行改造,内存布局也有变化,所以在每次升级版本的时候这些三方库可能会造成崩溃,非常依赖于作者的维护,似乎不是一种很好的选择。

Swift4之后苹果标准库中新增了Codable协议。遵循了Codable协议,就可以自动进行编解码操作。而在Codable出了之后,HandyJson不再进行维护,作者已不建议使用此库了。

所以了解并学习Codable是非常必要的

public typealias Codable = Decodable & Encodable

可以看到Codable是Decodable和Encodable两个协议的组合

/// A type that can encode itself to an external representation.
public protocol Encodable {
​
    /// Encodes this value into the given encoder.
    ///
    /// If the value fails to encode anything, `encoder` will encode an empty
    /// keyed container in its place.
    ///
    /// This function throws an error if any values are invalid for the given
    /// encoder's format.
    ///
    /// - Parameter encoder: The encoder to write data to.
    func encode(to encoder: Encoder) throws
}
​
/// A type that can decode itself from an external representation.
public protocol Decodable {
​
    /// Creates a new instance by decoding from the given decoder.
    ///
    /// This initializer throws an error if reading from the decoder fails, or
    /// if the data read is corrupted or otherwise invalid.
    ///
    /// - Parameter decoder: The decoder to read data from.
    init(from decoder: Decoder) throws
}

协议也只需要实现一个方法,看起来很简单。本文就通过一个例子来窥探下Decodable的实现原理

使用

struct Person: Decodable {
    var name: String
    var age: Int
    var width: Double
    var height: Double
}
​
let json = """
{
    "name": "张三",
    "age": 18,
    "width": 100,
    "height": 110
}
"""if let data = json.data(using: .utf8) {
    let decoder = JSONDecoder()
    do {
        let person = try decoder.decode(Person.self, from: data)
        print(person)
    } catch let error {
        print(error)
    }
}
​
// 输出结果
Person(name: "张三", age: 18, width: 100.0, height: 110.0)

首先创建JSONDecoder对象,调用decode方法

    /// 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<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable

decode方法接收两个参数:

  • type:需要解码的值类型,必须遵循Decodable协议。(Person.self)
  • data:需要解码的数据,Data类型,故需要转换json为data

返回要解码类型的对象。如果解析失败(缺少字段、字段类型不准确等)则会抛出DecodingError类型错误

原理解析

要想知道decode具体实现,就要从Foundation源码中查看了(Sources/Foundation/JSONEncoder.swift),搜索 open class JSONDecoder 找到JSONDecoder类

Tips:本文解析的是release/5.4分支下的代码,release/5.5之后的JSONDecoder内部实现方式变了。但是通过Xcode编译出来的还是release/5.4分支下对应的JSONDecoder代码(通过Xcode13.1,Swift5.5查看汇编所得到的结果)

    /// Decodes a top-level value of the given type from the given JSON representation.
    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
    }

decode方法主要分为三个步骤:

  • 第一步:try JSONSerialization.jsonObject(with: data, options: .allowFragments) 转换data为json类型,json类型可能是(NSNull, NSNumber, String, Array, [String : Any])

  • 第二步:初始化内部私有类_JSONDecoder,此类实现了Decoder协议,接手了具体的解析工作。

    codable_1

    _JSONDecoder 内部有一个storage字段,对应 _JSONDecodingStorage,这个结构体类似一个栈的结构,主要存储了解析的json数据

  • 第三步:调用 _JSONDecoder.unbox 方法,_JSONDecoder重载了unbox各种实现。

    Codable_2

    由于这里调用的type为Person.self,所以命中了 fileprivate func unbox<T : Decodable>(_ value: Any, as type: T.Type) throws -> T?

    Codable_3

    此方法内部会根据type的类型执行对应的逻辑。我们此时的类型为Person.self,所以命中了最后一个else逻辑,直接调用type的初始化方法 init(from decoder: Decoder) throws

    /// A type that can decode itself from an external representation.
    public protocol Decodable {
    ​
        /// Creates a new instance by decoding from the given decoder.
        ///
        /// This initializer throws an error if reading from the decoder fails, or
        /// if the data read is corrupted or otherwise invalid.
        ///
        /// - Parameter decoder: The decoder to read data from.
        init(from decoder: Decoder) throws
    }
    

    由于遵循了Decodable协议,所以就需要实现init(from decoder: Decoder)方法。但是我们并没有手动去实现,那为什么没有报错且还能正常解析呢???

至此,JSONDecoder.decode 方法的流程我们已经分析完了。

一句话总结一下:JSONDecoder内部调用传入类型的 init(from decoder: Decoder) 初始化方法,并把对象返回。

但是看完之后还是一头雾水。 具体怎么解析值,怎么赋值也不清楚? 等于没看。所以我们接下来就是看init方法是怎么实现的,方法内部到底做了什么。有两种方式可以窥探一下

  1. 断点查看汇编代码,跟踪调用栈

  2. 查看SIL中间代码

    swiftc -emit-sil test.swift | xcrun swift-demangle > ./main1.sil
    

汇编跟踪调用栈

为了方便汇编查看,简化一下代码,只留两个属性。设置展示汇编代码(Debug -> Debug Workflow -> Always show Disassembly),成功之后打断点运行代码,我们重点关注 call指令(这里运行的是模拟器,对应x86汇编,和真机的arm汇编不一样,但是整体逻辑是一样的)

接下来我们重点是要找到Person.init方法的具体实现,这里也分为两种方式

  1. decoder.decode()方法开始断点,一步步直到找到init方法
  2. 直接设置符号断点Person.init,运行之后直接就会定位到init方法,跳过前面繁琐的步骤,一步到位

第一种方式比较麻烦,需要一步步的跟踪,但是按照这种方式调试会加深对decode流程的了解。

第二种方式简单,如果只想研究init方法,则选择这种方式

从decode方法跟踪

Codable_4

首先在decode调用处设置断点运行

Codable_5

可以看到call了 Foundation.JSONDecoder.decodesi跟进去,直到进入此方法的真正实现

Codable_6

这个方法对应的就是之前分析过的decode方法。在汇编代码很长没头绪的情况下,我们可以通过源码中的实现反推出汇编的重点调用。

    /// Decodes a top-level value of the given type from the given JSON representation.
    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
    }

在源码中第三步可以看到是要调用_JSONDecoder对象的unbox方法,所以下一步要做的就是从汇编中找到哪callunbox

Codable_7

在unbox方法内部就有init的调用逻辑,所以在unbox汇编偏后的位置我们找到了Swift.Decodable.init

Codable_8

接着往下跟,一直到找到真实的Person.init方法。这一步比较难跟踪,经过了多层的方法派发过程。实际操作过程中可以参考下图中左边的调用栈

Codable_15

设置符号断点Person.init

这种方法上文已经说了是找到init方法最简单的方式

codable_16

设置成功之后记得去掉其他断点,然后直接运行

codable_17

左侧的调用栈和第一种方式最终看到的一模一样,这就是我们要研究的init方法了

窥探Person.init()方法

codable_10

codable_11

初始化方法很长,通过右侧的注释可以明显的看到三次调用

  1. 首先调用Swift.Decoder.container()方法。从上面分析的init方法传参来看,Decoder就是内部类_JSONDecoder ,所以此次调用最终应该是进入_JSONDecoder.container()。跟进去看一下

    codable_12

    // MARK: - Decoder Methods
        public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
            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."))
            }
    ​
            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)
        }
    

    找到对应的源码查看,返回值 KeyedDecodingContainer(container),container对应的是 _JSONKeyedDecodingContainer 私有类,遵循了KeyedDecodingContainerProtocol协议。

    所以后续看到KeyedDecodingContainer.decode类似的调用,实际上都是_JSONKeyedDecodingContainer 私有类接手处理的

  2. 接下来有两次 Swift.KeyedDecodingContainer.decode() 调用,因为Person有两个成员变量,所以分别调用了两次进行赋值。

    public struct KeyedDecodingContainer<K: CodingKey> :
      KeyedDecodingContainerProtocol
    {
      public typealias Key = K
    ​
      /// The container for the concrete decoder.
      internal var _box: _KeyedDecodingContainerBase
    ​
      /// Creates a new instance with the given container.
      ///
      /// - parameter container: The container to hold.
      public init<Container: KeyedDecodingContainerProtocol>(
        _ container: Container
      ) where Container.Key == Key {
        _box = _KeyedDecodingContainerBox(container)
      }
      ...
      ...
      public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
        return try _box.decode(Bool.self, forKey: key)
      }
    

    通过跟踪分析,得出decode的调用栈

    KeyedDecodingContainer.decode(Bool.self, forKey: key)
      -> _KeyedDecodingContainerBox.decode(Bool.self, forKey: key)
          -> _JSONKeyedDecodingContainer.decode(Bool.self, forKey: key)
            -> try self.decoder.unbox(entry, as: Bool.self)
    

    codable_21.png

    可以看到最终在_JSONKeyedDecodingContainer私有类内部调用了self.decoder.unbox(),这里的self.decoder对应的就是最开始的_JSONDecoder,所以最后都是调用了 _JSONDecoder.unbox() 方法来进行解析。再通过汇编跟踪验证一下

    codable_13

    codable_14

    最后通过代码佐证一下

    codable_18

    再来看一下具体类型的解析工作,具体的解析判断等都在这里写着了。

    codable_19

接下来看另一个问题。上面调用 KeyedDecodingContainer.decode(Bool.self, forKey: key) 时有两个参数,一个是类型,这个好理解。另一个是Key,这个key是什么呢?

public struct KeyedDecodingContainer<K: CodingKey> :
  KeyedDecodingContainerProtocol
{
  public typealias Key = K
​
  /// The container for the concrete decoder.
  internal var _box: _KeyedDecodingContainerBase
​
  /// Creates a new instance with the given container.
  ///
  /// - parameter container: The container to hold.
  public init<Container: KeyedDecodingContainerProtocol>(
    _ container: Container
  ) where Container.Key == Key {
    _box = _KeyedDecodingContainerBox(container)
  }

从代码中看出了K是一个遵循CodingKey协议的泛型,再来找一找协议的声明

/// A type that can be used as a key for encoding and decoding.
public protocol CodingKey: Sendable,
                           CustomStringConvertible,
                           CustomDebugStringConvertible {
  /// The string to use in a named collection (e.g. a string-keyed dictionary).
  var stringValue: String { get }
​
  init?(stringValue: String)
​
  /// The value to use in an integer-indexed collection (e.g. an int-keyed
  /// dictionary).
  var intValue: Int? { get }
​
  init?(intValue: Int)
}

我们再写属性的时候也没有主动遵循这个协议,这个是哪来的呢,这就和Person.init 方法是哪来的存在同样的疑惑。上面我们只是看了实现逻辑,但是并没有解决从哪来的问题,接下来就通过SIL中间代码查看一下

查看SIL代码

新建一个test.swift文件,并通过命令转换为sil文件

struct Animal: Decodable {
    var cat: String
    var age: Int
}
swiftc -emit-sil test.swift | xcrun swift-demangle > ./main1.sil

打开main.sil文件,短短的4行代码,生成的sil达到了700多行。

struct Animal : Decodable {
  @_hasStorage var cat: String { get set }
  @_hasStorage var age: Int { get set }
  enum CodingKeys : CodingKey {
    case cat
    case age
    @_implements(Equatable, ==(_:_:)) static func __derived_enum_equals(_ a: Animal.CodingKeys, _ b: Animal.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 }
  }
  init(cat: String, age: Int)
  init(from decoder: Decoder) throws
}

可以看到编译器主动给生成了遵循CodingKey协议的枚举类型(enum CodingKeys : CodingKey {} ),枚举内部会把所有的属性罗列出来,作为后期编解码的key。在sil文件后面可以找到 init?(stringValue: String)var stringValue: String { get } 等实现,主要是key的get、set方法

所以平时开发中当遇到需要映射key时,则需要自己手动实现此方法(注意要把所有的属性都罗列出来)

struct Animal: Decodable {
    var cat: String
    var age: Int
    
    enum CodingKeys: String, CodingKey {
        case cat = "cat_name"
        case age
    }
}

找到了CodingKey的实现。接下来搜索一下编译器是不是也给默认实现了init(from decoder: Decoder)方法

// Animal.init(from:)
sil hidden @test.Animal.init(from: Swift.Decoder) throws -> test.Animal : $@convention(method) (@in Decoder, @thin Animal.Type) -> (@owned Animal, @error Error) {
// %0 "decoder"                                   // users: %69, %49, %9, %6
// %1 "$metatype"
bb0(%0 : $*Decoder, %1 : $@thin Animal.Type):
...
...
  debug_value_addr %0 : $*Decoder, let, name "decoder", argno 1 // id: %6
  debug_value undef : $Error, var, name "$error", argno 2 // id: %7
  %8 = alloc_stack $KeyedDecodingContainer<Animal.CodingKeys>, let, name "container" // users: %45, %44, %37, %66, %65, %21, %60, %59, %13, %55bb1(%14 : $()):                                   // Preds: bb0
  %15 = metatype $@thin String.Type               // user: %21
  %16 = metatype $@thin Animal.CodingKeys.Type
  %17 = enum $Animal.CodingKeys, #Animal.CodingKeys.cat!enumelt // user: %19
  %18 = alloc_stack $Animal.CodingKeys            // users: %19, %23, %21, %58
  store %17 to %18 : $*Animal.CodingKeys          // id: %19
  // function_ref KeyedDecodingContainer.decode(_:forKey:)
  %20 = function_ref @Swift.KeyedDecodingContainer.decode(_: Swift.String.Type, forKey: A) throws -> Swift.String : $@convention(method) <τ_0_0 where τ_0_0 : CodingKey> (@thin String.Type, @in_guaranteed τ_0_0, @in_guaranteed KeyedDecodingContainer<τ_0_0>) -> (@owned String, @error Error) // user: %21
  try_apply %20<Animal.CodingKeys>(%15, %18, %8) : $@convention(method) <τ_0_0 where τ_0_0 : CodingKey> (@thin String.Type, @in_guaranteed τ_0_0, @in_guaranteed KeyedDecodingContainer<τ_0_0>) -> (@owned String, @error Error), normal bb2, error bb5 // id: %21// %22                                            // users: %63, %48, %46, %29, %28
bb2(%22 : $String):                               // Preds: bb1
  dealloc_stack %18 : $*Animal.CodingKeys         // id: %23
  %24 = begin_access [modify] [static] %3 : $*Animal // users: %30, %25
  %25 = struct_element_addr %24 : $*Animal, #Animal.cat // user: %29
  %26 = integer_literal $Builtin.Int2, 1          // user: %27
  store %26 to %2 : $*Builtin.Int2                // id: %27
  retain_value %22 : $String                      // id: %28
  store %22 to %25 : $*String                     // id: %29
  end_access %24 : $*Animal                       // id: %30
  %31 = metatype $@thin Int.Type                  // user: %37
  %32 = metatype $@thin Animal.CodingKeys.Type
  %33 = enum $Animal.CodingKeys, #Animal.CodingKeys.age!enumelt // user: %35
  %34 = alloc_stack $Animal.CodingKeys            // users: %35, %39, %37, %64
  store %33 to %34 : $*Animal.CodingKeys          // id: %35
  // function_ref KeyedDecodingContainer.decode(_:forKey:)
  %36 = function_ref @Swift.KeyedDecodingContainer.decode(_: Swift.Int.Type, forKey: A) throws -> Swift.Int : $@convention(method) <τ_0_0 where τ_0_0 : CodingKey> (@thin Int.Type, @in_guaranteed τ_0_0, @in_guaranteed KeyedDecodingContainer<τ_0_0>) -> (Int, @error Error) // user: %37
  try_apply %36<Animal.CodingKeys>(%31, %34, %8) : $@convention(method) <τ_0_0 where τ_0_0 : CodingKey> (@thin Int.Type, @in_guaranteed τ_0_0, @in_guaranteed KeyedDecodingContainer<τ_0_0>) -> (Int, @error Error), normal bb3, error bb6 // id: %37// %38                                            // users: %42, %46
bb3(%38 : $Int):                                  // Preds: bb2
  dealloc_stack %34 : $*Animal.CodingKeys         // id: %39
  %40 = begin_access [modify] [static] %3 : $*Animal // users: %43, %41
  %41 = struct_element_addr %40 : $*Animal, #Animal.age // user: %42
  store %38 to %41 : $*Int                        // id: %42
  end_access %40 : $*Animal                       // id: %43
  destroy_addr %8 : $*KeyedDecodingContainer<Animal.CodingKeys> // id: %44
  dealloc_stack %8 : $*KeyedDecodingContainer<Animal.CodingKeys> // id: %45
  %46 = struct $Animal (%22 : $String, %38 : $Int) // users: %53, %47
  retain_value %46 : $Animal                      // id: %47
  release_value %22 : $String                     // id: %48
  destroy_addr %0 : $*Decoder                     // id: %49
  destroy_addr %3 : $*Animal                      // id: %50
  dealloc_stack %3 : $*Animal                     // id: %51
  dealloc_stack %2 : $*Builtin.Int2               // id: %52
  return %46 : $Animal                            // id: %53
...
...
...
} // end sil function 'test.Animal.init(from: Swift.Decoder) throws -> test.Animal'

SIL文件很长,也比较难读。总结一下,可以用下面伪代码来表示

init(from decoder: Decoder) throws {
	let container = decoder.container(key: CodingKeys.self)
	cat = @Swift.KeyedDecodingContainer.decode(_: Swift.String.Type, forKey: A)
	age = @Swift.KeyedDecodingContainer.decode(_: Swift.Int.Type, forKey: A)
	// 如果有多个属性,则继续添加对应方法
	...
	...
}

从这里就可以看出来是怎么赋值的了,decode方法从上文中已经分析过了,把decode返回的值赋值给对应的属性,这样就完成了模型解析工作

DerivedConformanceCodable

Swift源码swift/lib/Sema/ 路径下可以找到编译器生成的c++代码,感兴趣的可以看一看编译器是如何实现的

codable_20

Derivedxxxx.cpp开头的全是编译器用来生成默认代码的

DerivedConformanceCodable.cpp中找到编译器是如何生成Decodable协议init(from decoder: Decoder) throws 方法的

从这里的注释可以更清晰的看到Decodable是如何工作的

/// Synthesizes the body for `init(from decoder: Decoder) throws`.
///
/// \param initDecl The function decl whose body to synthesize.
static std::pair<BraceStmt *, bool>
deriveBodyDecodable_init(AbstractFunctionDecl *initDecl, void *) {
  // struct Foo : Codable {
  //   var x: Int
  //   var y: String
  //
  //   // Already derived by this point if possible.
  //   @derived enum CodingKeys : CodingKey {
  //     case x
  //     case y
  //   }
  //
  //   @derived init(from decoder: Decoder) throws {
  //     let container = try decoder.container(keyedBy: CodingKeys.self)
  //     x = try container.decode(Type.self, forKey: .x)
  //     y = try container.decode(Type.self, forKey: .y)
  //   }
  // }
​
  // The enclosing type decl.
  auto conformanceDC = initDecl->getDeclContext();
  auto *targetDecl = conformanceDC->getSelfNominalTypeDecl();
​
  auto *funcDC = cast<DeclContext>(initDecl);
  auto &C = funcDC->getASTContext();
  ...
  ...
}

总结

本文主要是为了讲解Deocdable的实现原理。下一篇会着重写关于Codable的坑点以及解决/替代方法

最后整体总结一下步骤

开发者需要做的:

  1. 首先模型要遵循CodableDecodable协议,要确保所有的属性都是遵循了Codable协议,Swift对基本类型都默认实现了Codable协议
  2. 使用 JSONDecoder().decode(Person.self, from: data) 方法来进行解码

编译器做的:

  1. 对遵循了协议的类或结构体默认生成enum CodingKeys : CodingKey { ... }枚举,里面罗列了所有的属性

     @derived enum CodingKeys : CodingKey {
       case x
       case y
     }
    
  2. 默认生成 init(from decoder: Decoder) 方法

     @derived init(from decoder: Decoder) throws {
       let container = try decoder.container(keyedBy: CodingKeys.self)
       x = try container.decode(Type.self, forKey: .x)
       y = try container.decode(Type.self, forKey: .y)
     }
    

至此,双方配合完成了整个解析工作

参考资料

Encoding and Decoding Custom Types

Swift

Foundation

Codable保姆级攻略