Swift编解码Codable协议

2,251 阅读6分钟

Codable使用

Codable协议是Swift4.0之后出来的编解码协议,在Json 转 model 时非常方便,如下所示

// 1
struct LYPerson:Codable {
    var name: String?
    var age: Int?
}
// 2
let dict: [String : Any] = ["name": "Jack", "age": 10]
let decoder = JSONDecoder()
let data = try JSONSerialization.data(withJSONObject: dict, options: .fragmentsAllowed)
let p = try decoder.decode(LYPerson.self, from: data)
  • 1,自定义对象,遵守Codable协议。
  • 2,初始化JSONDecoder对象,将 json数据进行序列化,然后进行解码。

这样我们就可以将 dict里面的键值对映射到 LYPerson对象中了。

源码分析

Decoder

我们打开编译好的swift-foundation(5.3.2版本) 先看下codable的定义

public typealias Codable = Decodable & Encodable

codableDecodableEncodable的别名

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:Decode)方法

我们在解码的时候:

1,初始化了一个JSONDecoder对象。

2,调用 decode<T : Decodable>(_ type: T.Type, from data: Data)方法。

接下来,我们看下decode方法做了哪些操作。

    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
    }

1,传入的参数 为解码的Type,在本例中是LYPerson。另一个参数为 Data数据,即 dict。

2,对 Data进行Json序列化

3,创建内部类 _JSONDecoder,并将Json数据解码策略

4,调用 unbox方法。

解码策略

当我们初始化JSONDecoder对象时,如果不设置解码策略,则会采用默认的解码策略

   fileprivate var options: _Options {
        return _Options(dateDecodingStrategy:  `dateDecodingStrategy = secondsSince1970`,
                        dataDecodingStrategy: dataDecodingStrategy,
                        nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
                        keyDecodingStrategy: keyDecodingStrategy,
                        userInfo: userInfo)
    }

我们以Date为例

    fileprivate func unbox(_ value: Any, as type: Date.Type) throws -> Date? {
        guard !(value is NSNull) else { return nil }

        switch self.options.dateDecodingStrategy {
        case .deferredToDate:
            self.storage.push(container: value)
            defer { self.storage.popContainer() }
            return try Date(from: self)
		// 1
        case .secondsSince1970:
            let double = try self.unbox(value, as: Double.self)!
            return Date(timeIntervalSince1970: double)

        case .millisecondsSince1970:
            let double = try self.unbox(value, as: Double.self)!
            return Date(timeIntervalSince1970: double / 1000.0)

        case .iso8601:
            if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
                let string = try self.unbox(value, as: String.self)!
                guard let date = _iso8601Formatter.date(from: string) else {
                    throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Expected date string to be ISO8601-formatted."))
                }

                return date
            } else {
                fatalError("ISO8601DateFormatter is unavailable on this platform.")
            }
	// 2
        case .formatted(let formatter):
            let string = try self.unbox(value, as: String.self)!
            guard let date = formatter.date(from: string) else {
                throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Date string does not match format expected by formatter."))
            }

            return date
	// 3
        case .custom(let closure):
            self.storage.push(container: value)
            defer { self.storage.popContainer() }
            return try closure(self)
        }
    }
  • 1,如果时间策略是 secondsSince1970,则会返回从 1970至今的时间戳。
  • 2,如果有确定的时间格式,则返回特定格式的时间。
  • 3,如果是自定义解码,则进行闭包回调。

通过设置不同的解码策略,来完成个性化的解码。

unbox

我们先看 unbox方法的实现

    fileprivate func unbox_(_ value: Any, as type: Decodable.Type) throws -> Any? {
        #if DEPLOYMENT_RUNTIME_SWIFT
        
        print(type)
        // Bridging differences require us to split implementations here
        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)
        }
  • 1,如果是DataDate,URL类型,则根据不同的编码策略,生成一个相对应类型的实例变量。
   fileprivate func unbox<T>(_ value: Any, as type: _JSONStringDictionaryDecodableMarker.Type) throws -> T? {
        guard !(value is NSNull) else { return nil }

        var result = [String : Any]()
        guard let dict = value as? NSDictionary else {
            throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
        }
        let elementType = type.elementType
        for (key, value) in dict {
            let key = key as! String
            self.codingPath.append(_JSONKey(stringValue: key, intValue: nil))
            defer { self.codingPath.removeLast() }

            result[key] = try unbox_(value, as: elementType)
        }

        return result as? T
    }
    
fileprivate protocol _JSONStringDictionaryDecodableMarker {
    static var elementType: Decodable.Type { get }
}

extension Dictionary : _JSONStringDictionaryDecodableMarker where Key == String, Value: Decodable {
    static var elementType: Decodable.Type { return Value.self }
}

  • 1,如果是Dictionary类型,则对 键值对 进行遍历,对value值进行解码。

在本例中,一开始传入的type == LYPerson,它会调用其 init(from decoder: Decoder)方法。 LYPerson对象的 init(from decoder: Decoder)方法我们没有进行声明,那它从哪里来的呢?编辑器为我们做了一些处理。

SIL分析

我们将代码转化为 SIL

struct LYPerson : Decodable & Encodable {
  @_hasStorage @_hasInitialValue var name: String? { get set }
  @_hasStorage @_hasInitialValue var age: Int? { get set }
  init(name: String? = nil, age: Int? = nil)
  init()
  enum CodingKeys : CodingKey {
    case name
    case age
    @_implements(Equatable, ==(_:_:)) static func __derived_enum_equals(_ a: LYPerson.CodingKeys, _ b: LYPerson.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
}

我们可以看到,编译器为我们自动生成了CodingKeys编解码方法。初始化方法具体如下

// LYPerson.init(from:)
sil hidden @$s4main8LYPersonV4fromACs7Decoder_p_tKcfC : $@convention(method) (@in Decoder, @thin LYPerson.Type) -> (@owned LYPerson, @error Error) {
// %0 "decoder"                                   // users: %71, %52, %12, %3
// %1 "$metatype"
bb0(%0 : $*Decoder, %1 : $@thin LYPerson.Type):
  %2 = alloc_stack $LYPerson, var, name "self"    // users: %43, %27, %9, %6, %53, %72, %73, %54
  debug_value_addr %0 : $*Decoder, let, name "decoder", argno 1 // id: %3
  debug_value undef : $Error, var, name "$error", argno 2 // id: %4
  %5 = enum $Optional<String>, #Optional.none!enumelt // user: %7
  %6 = struct_element_addr %2 : $*LYPerson, #LYPerson.name // user: %7
  store %5 to %6 : $*Optional<String>             // id: %7
  %8 = enum $Optional<Int>, #Optional.none!enumelt // user: %10
  %9 = struct_element_addr %2 : $*LYPerson, #LYPerson.age // user: %10
  store %8 to %9 : $*Optional<Int>                // id: %10
  %11 = alloc_stack $KeyedDecodingContainer<LYPerson.CodingKeys>, let, name "container" // users: %48, %47, %40, %68, %67, %24, %62, %61, %16, %57
  %12 = open_existential_addr immutable_access %0 : $*Decoder to $*@opened("D0F7E680-7A9A-11EB-8646-186590D5CC1F") Decoder // users: %16, %16, %15
  %13 = metatype $@thin LYPerson.CodingKeys.Type
  %14 = metatype $@thick LYPerson.CodingKeys.Type // user: %16
  %15 = witness_method $@opened("D0F7E680-7A9A-11EB-8646-186590D5CC1F") Decoder, #Decoder.container : <Self where Self : Decoder><Key where Key : CodingKey> (Self) -> (Key.Type) throws -> KeyedDecodingContainer<Key>, %12 : $*@opened("D0F7E680-7A9A-11EB-8646-186590D5CC1F") Decoder : $@convention(witness_method: Decoder) <τ_0_0 where τ_0_0 : Decoder><τ_1_0 where τ_1_0 : CodingKey> (@thick τ_1_0.Type, @in_guaranteed τ_0_0) -> (@out KeyedDecodingContainer<τ_1_0>, @error Error) // type-defs: %12; user: %16
  try_apply %15<@opened("D0F7E680-7A9A-11EB-8646-186590D5CC1F") Decoder, LYPerson.CodingKeys>(%11, %14, %12) : $@convention(witness_method: Decoder) <τ_0_0 where τ_0_0 : Decoder><τ_1_0 where τ_1_0 : CodingKey> (@thick τ_1_0.Type, @in_guaranteed τ_0_0) -> (@out KeyedDecodingContainer<τ_1_0>, @error Error), normal bb1, error bb4 // type-defs: %12; id: %16
  • 1 ,首先创建了一个Container:KeyedDecodingContainer
  • 2 ,然后从协议目击表中,调用Decoder协议的container方法

查阅源码,我们可知 _JSONDecoder遵守 Decoder协议

public protocol Decoder {

    /// The path of coding keys taken to get to this point in decoding.
    var codingPath: [CodingKey] { get }

    /// Any contextual information set by the user for decoding.
    var userInfo: [CodingUserInfoKey : Any] { get }

    /// Returns the data stored in this decoder as represented in a container
    /// keyed by the given key type.
    ///
    /// - parameter type: The key type to use for the container.
    /// - returns: A keyed decoding container view into this decoder.
    /// - throws: `DecodingError.typeMismatch` if the encountered stored value is
    ///   not a keyed container.
    func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey

    /// Returns the data stored in this decoder as represented in a container
    /// appropriate for holding values with no keys.
    ///
    /// - returns: An unkeyed container view into this decoder.
    /// - throws: `DecodingError.typeMismatch` if the encountered stored value is
    ///   not an unkeyed container.
    func unkeyedContainer() throws -> UnkeyedDecodingContainer

    /// Returns the data stored in this decoder as represented in a container
    /// appropriate for holding a single primitive value.
    ///
    /// - returns: A single value container view into this decoder.
    /// - throws: `DecodingError.typeMismatch` if the encountered stored value is
    ///   not a single value container.
    func singleValueContainer() throws -> SingleValueDecodingContainer
}

Decoder协议中定义了三种容器:KeyedDecodingContainer,UnkeyedDecodingContainer,SingleValueDecodingContainer

  • KeyedDecodingContainer 类似于字典,键值对容器,键值是强类型。
  • UnkeyedDecodingContainer 类似于数组,连续值容器,没有键值。
  • SingleValueDecodingContainer 基础数据类型容器。

在 KeyedDecodingContainer中,对基本数据类型进行解码赋值操作

自定义解码方法


struct LYPerson: 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)
            
        self.name = try container.decode(String.self, forKey: .name)
        
        self.age = try container.decode(Int.self, forKey: .age)
        
    }
}


let dict: [String : Any] = ["name": "Jack", "age": 10]

let decoder = JSONDecoder()


let data = try JSONSerialization.data(withJSONObject: dict, options: .fragmentsAllowed)
let p = try decoder.decode(LYPerson.self, from: data)

print(p.age)

1,定义 CodingKeys,声明解码的key

2,通过JSONDecoder,获取 KeyedDecodingContainer

3,通过container对象对基本数据类型进行解码赋值。