Swift数据解析方案Codable源码解读

前言

在使用OC的项目中,对于网络请求获取json数据,然后进行解析成的自定义模型我们通常会用MJExtension来很方便的处理.在Swift中,我发现早期其实系统没有提供相应的API来解决类似的问题,直到Swift4.0的时候,发布了Codable来解决这个痛点.在使用Codable后,发现这个功能非常强大,使用起来非常方便,由于之前只是知道如何使用,并不知道深层次的原理,所以来学习一下Codable的源码,来加深理解.

痛苦的写法

没有对比就没有伤害,我首次使用swift做数据解析的时候是使用下面的方式来做的,虽然能完成现在的需求和功能,但是书写代码的成本和后续维护代码的成本都太大了.

//用户资料
@objc(UserProfileModel)
class UserProfileModel: NSObject, NSCoding {
    var nickname: String?//用户昵称
    var figureUrl: String?//用户头像
    var mobile: String?//手机号
    
    init(nickname: String?, figureUrl: String?, mobile: String?) {
        self.nickname = nickname
        self.figureUrl = figureUrl
        self.mobile = mobile
        super.init()
    }
    func encode(with coder: NSCoder) {
        coder.encode(nickname, forKey: "nickname")
        coder.encode(figureUrl, forKey: "figureUrl")
        coder.encode(mobile, forKey: "mobile")
    }
     
    required init?(coder aDecoder: NSCoder) { // 3
        nickname = aDecoder.decodeObject(forKey: "nickname") as? String
        figureUrl = aDecoder.decodeObject(forKey: "figureUrl") as? String
        mobile = aDecoder.decodeObject(forKey: "mobile") as? String
    }
}

上面的这种做法跟Codable对比起来实在是太繁琐了,首先去声明成了objc文件,然后去遵守NSCoding,然后手动实现encode和decode方法.我已经删除了特别多这个模型里的属性,如果属性多起来的化,这样的方式写确实是一件很痛苦的事情.后期维护起来如果需要添加属性或者删除属性,这一系列方法都需要修改.那就更痛苦了.

什么是Codable

/// A type that can convert itself into and out of an external representation.
public typealias Codable = Decodable & Encodable

/// A type that can encode itself to an external representation.
public protocol Encodable {
    public func encode(to encoder: Encoder) throws
}

/// A type that can decode itself from an external representation.
public protocol Decodable {
    public init(from decoder: Decoder) throws
}

从苹果公开的public文件内,我们能看到Codable 其实是Decodable和Encodable两个协议的总和.遵从Codable协议就需要遵从这两个协议内声明的方法.Decodable内声明的init(from)方法来实现解析并实例化,Encodable对应的encode(to encoder: Encoder)来实现归档.

如何使用Codable

先声明一个struct遵从Codable

struct Person: Codable {
    var name: String?
    var gender: String?
    var age: Int?
}
Struct --> JsonData Encode流程
let person = Person(name: "Tom", gender: "male", age: 27)
let encoder = JSONEncoder()
do {
     guard let data = try? encoder.encode(person), let encodedString = String(data: data, encoding: .utf8) else {
      return
    }
     print(encodedString)// 输出 {"name":"Tom","gender":"male","age":27}
 }
JsonData --> Struct Decode流程
let jsonString = "{\"name\":\"Tom\",\"gender\":\"male\",\"age\":27}"
guard let jsonData = jsonString.data(using: .utf8) else {
    return
   }
guard let model = try? JSONDecoder().decode(Person.self, from: jsonData) else {
    return
   }
print(model)// 输出: Person(name: "Tom", gender: "male", age: 27)

使用完Codable后发现,不管是encode还是decode,Codable都能很大程度上简化流程化的代码,非常方便使用起来.

第一个疑问:

这里其实我就有了一个疑问,Codable为什么能写起来这么方便.对于简单的需求来说,只需要遵守Codable就可以了,连Decodable和Encodable内声明的方法都没有实现都能正常使用.这里先保留疑问,在后面的源码和编译中间码分析时我找到了答案.

如何自定义模型内的key

实际开发过程中,往往会有需要重新定义后台字段名的情况,比如如果后台的命名风格很乱或者和我们想要的风格不同,这个时候就需要重新命名json中的某些字段名

struct Person: Codable {
    var name: String?
    var gender: String?
    var age: Int?
    
    enum CodingKeys: String, CodingKey {
        case name = "N_name"
        case gender
        case age
    }
}
后台返回的结构如下
{
    "N_name": "Tom",
    "gender": "male",
    "age": 27
}
// 输出: Person(name: "Tom", gender: "male", age: 27)

//如果内部没有写全属性字段
    enum CodingKeys: String, CodingKey {
        case name = "N_name"
        case gender
    }
    
// 输出: Person(name: "Tom", gender: "male")  未声明的属性会不被解析

这种场景,只要在struct内声明一个名字叫CodingKeys的enum,同时需要遵守String和CodingKey,需要使用case把每个属性名都声明一下,针对需要自定义的key,把名字修改成对应的字段就可以.所有需要使用的属性都需要在这里进行声明,如果不声明的话,会在解析的时候忽略没声明的属性.

如何解决模型内嵌套模型的问题

方案1:

struct User: Codable {
      var name: String
      var pos: Point
  
   enum CodingKeys: CodingKey {
      case name
      case pos
  }

   enum PosCodingKeys: CodingKey {
      case x
     case y
  }

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

    var posContainer = container.nestedContainer(keyedBy: PosCodingKeys.self, forKey: .pos)
    try posContainer.encode(pos.x, forKey: .x)
    try posContainer.encode(pos.y, forKey: .y)
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decode(String.self, forKey: .name)

    let posContainer = try container.nestedContainer(keyedBy: PosCodingKeys.self, forKey: .pos)
    self.pos = Point( x: try posContainer.decode(Double.self, forKey: .x),y: try posContainer.decode(Double.self, forKey: .y)
    )
  }
}

方案1本质上是重写了Decodable和Encodable内声明的两个方法,然后在内部手动按照想要的User模型格式,对模型进行赋值.这里一样出现了有很多没营养代码的问题,还有后续维护的问题.

方案2:

struct User: Codable {
     var name: String

     var _point: _Point
     var pos: Point {
       get {
           return Point(x: _point.x, y: _point.y)
            }
        set {
             _point = _Point(x: newValue.x, y: newValue.y)
         }
      }

       enum CodingKeys: String, CodingKey {
        case name
      case _point = "pos"
     }

     init(name: String, pos: Point) {
         self.name = name
         self._point = _Point(x: pos.x, y: pos.y)
      self.pos = pos
      }

     struct _Point: Codable {
          var x: Double
        var y: Double
      }
}

相比方案1,代码量减少一些,但是感觉还是不是很舒服这样写.

第二个疑问:

为什么需要使用CodingKeys才能自定义模型的key呢?我查找了很多文档,很多关于这方面的原因都是仅仅说了一句“系统内部实现”,我觉得这个说法很不信服,所以继续研究了下为什么会是这个现象.

系统提供的一些类型默认支持Codable

%{
codable_types = ['Bool', 'String', 'Double', 'Float',
                 'Int', 'Int8', 'Int16', 'Int32', 'Int64',
                 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64']
}%

系统的这些类型默认已经支持了Codable,而且已经帮我们实现了相应的代码.所以在写这几种类型的时候,不需要声明Codable也可以直接使用了.

如果进入这些类型相应的头文件,可以看到:

extension String : Codable {

    public init(from decoder: Decoder) throws

    public func encode(to encoder: Encoder) throws
}

Swift编译中间码

类似于OC中的clang,Swift也会在编译过程中,转换成中间码“SIL”.这部分由于会在后面解释源码过程中使用,所以提前先引出这个概念.


image
swiftc Test.swift -emit-sil

可以使用上面的命令将swift代码转换成SIL中间代码.查看编译器是否在编译过程中背后默默的自动添加了一些额外代码.

Test.swift
struct Student: Codable {
    var name: String?
    var gender: String?
    var age: Int?
    
    enum CodingKeys: String, CodingKey {
            case name = "lala"
            case gender
            case age
        }
}
命令行执行 swiftc Test.swift -emit-sil

sil_stage canonical

import Builtin
import Swift
import SwiftShims

struct Student : Decodable & Encodable {
  @_hasStorage @_hasInitialValue var name: String? { get set }
  @_hasStorage @_hasInitialValue var gender: String? { get set }
  @_hasStorage @_hasInitialValue var age: Int? { get set }
  enum CodingKeys : String, CodingKey {
    case name
    case gender
    case age
    typealias RawValue = String
    init?(rawValue: String)
    var rawValue: String { get }
    var stringValue: String { get }
    init?(stringValue: String)
    var intValue: Int? { get }
    init?(intValue: Int)
  }
  init(name: String? = nil, gender: String? = nil, age: Int? = nil)
  init()
  init(from decoder: Decoder) throws
  func encode(to encoder: Encoder) throws
}

看到SIL代码后,前面我的两个疑问自动就解开了
1.Xcode在编译swift代码的时候,会对遵守Codable的代码,自动添加一个enum CodingKeys.并且内部会自动根据外层书写的属性生成相应的代码.这也就是为什么我们定义一个叫做CodingKeys的枚举,就能覆盖一些属性.
2.同时,编译器会自动添加init(from decoder: Decoder) throws和 func encode(to encoder: Encoder) throws两个方法.这也就是为什么我们仅仅需要遵守Codable协议就可以实现相应的功能,是因为编译器在背后默默的帮我们插入了相关的代码.

Codable源码分析

从上么使用的过程中,我发现单纯来看Codable源码是不够的,因为真正功能的实现方是JsonEncoder和JsonDecoder来实现的.所以我们从这两方面来学习Codable的内部流程.
Codable源码
JsonEncoder源码

初次看这两个文件时,并没有头绪,大致浏览一边后感觉还是毫无收获,大概只是知道了Codable文件内大部分都是协议的声明,内部还有CodingKey ,Encoder,Decoder,KeyedDecodingContainer, _KeyedDecodingContainerBase ,KeyedEncodingContainer,_KeyedEncodingContainerBase,真正功能的实现是依靠的 _KeyedDecodingContainerBase和_KeyedEncodingContainerBase这种更底层的struct.更深入的东西我再反复看后特别吃力.

这是我转变了一个方向,先大概了解Codable是什么东西后,从使用方JSONEncoder和JSONDecoder的两个API作为入口,按照内部调用的顺序对这部分功能有个更深的了解,最终证明阅读这类源码使用一个切入点然后一点一点理解,效果应该会更好.

JSONEncoder

    try? JSONEncoder().encode(person)

    open func encode<T : Encodable>(_ value: T) throws -> Data {
        let encoder = _JSONEncoder(options: self.options)

        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."))
        }

        if topLevel is NSNull {
            throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as null JSON fragment."))
        } else if topLevel is NSNumber {
            throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as number JSON fragment."))
        } else if topLevel is NSString {
            throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded as string JSON fragment."))
        }

        let writingOptions = JSONSerialization.WritingOptions(rawValue: self.outputFormatting.rawValue)
        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))
        }
    }

从这个入口函数里,我们能看出来
1.JSONEncoder本身并没有实现Encoder协议,而是内部另外一个_JSONEncoder来实现了Encoder协议, JSONEncoder才是真正的Encoder.
2.函数的下半部分是判断顶层对象是否为NSNull, NSNumber , NSString ,如果是的话会报对应的错误,其他类型的会使用JSONSerialization相关的API进行转换,这部分代码相对比较好理解.
3.我把注意力都集中到了, let topLevel = try encoder.box
(value),这端代码上,encoder是如何获取顶层对象的呢?

    fileprivate func box_<T : Encodable>(_ value: T) throws -> NSObject? {
        if T.self == Date.self || T.self == NSDate.self {
            // Respect Date encoding strategy
            return try self.box((value as! Date))
        } else if T.self == Data.self || T.self == NSData.self {
            // Respect Data encoding strategy
            return try self.box((value as! Data))
        } else if T.self == URL.self || T.self == NSURL.self {
            // Encode URLs as single strings.
            return self.box((value as! URL).absoluteString)
        } else if T.self == Decimal.self || T.self == NSDecimalNumber.self {
            // JSONSerialization can natively handle NSDecimalNumber.
            return (value as! NSDecimalNumber)
        }

        // The value should request a container from the _JSONEncoder.
        let depth = self.storage.count
        try value.encode(to: self)

        // The top container should be a new container.
        guard self.storage.count > depth else {
            return nil
        }

        return self.storage.popContainer()
    }

这部分代码的上半部分还挺好理解,其实_JSONEncoder内部实现了大量的box方法,对应不同的类型,把swift内的类型都包装成,NS开头的OC类型.

   fileprivate func box(_ value: Bool)   -> NSObject { return NSNumber(value: value) }
    fileprivate func box(_ value: Int)    -> NSObject { return NSNumber(value: value) }
    fileprivate func box(_ value: Int8)   -> NSObject { return NSNumber(value: value) }
    fileprivate func box(_ value: Int16)  -> NSObject { return NSNumber(value: value) }
    fileprivate func box(_ value: Int32)  -> NSObject { return NSNumber(value: value) }
    fileprivate func box(_ value: Int64)  -> NSObject { return NSNumber(value: value) }
    fileprivate func box(_ value: UInt)   -> NSObject { return NSNumber(value: value) }
    fileprivate func box(_ value: UInt8)  -> NSObject { return NSNumber(value: value) }
    fileprivate func box(_ value: UInt16) -> NSObject { return NSNumber(value: value) }
    fileprivate func box(_ value: UInt32) -> NSObject { return NSNumber(value: value) }
    fileprivate func box(_ value: UInt64) -> NSObject { return NSNumber(value: value) }
    fileprivate func box(_ value: String) -> NSObject { return NSString(string: value) }

这里为什么要转换成OC中常使用的NS开头的对象呢?
可以从新回到encode的入口函数中,最终其实encode到data使用的是

try JSONSerialization.data(withJSONObject: topLevel, options: writingOptions)

/* Generate JSON data from a Foundation object. If the object will not produce valid JSON then an exception will be thrown. Setting the NSJSONWritingPrettyPrinted option will generate JSON with whitespace designed to make the output more readable. If that option is not set, the most compact possible JSON will be generated. If an error occurs, the error parameter will be set and the return value will be nil. The resulting data is a encoded in UTF-8.
     */
    open class func data(withJSONObject obj: Any, options opt: JSONSerialization.WritingOptions = []) throws -> Data

JSONSerialization.data的文档中显示了withJSONObject接受参数的类型.需要使用一个Foundation对象.所以这里也就理解了为什么_JSONEncoder内部要实现这么多对应的包装函数了.

box_函数中,我有个挺大的疑问,为什么 try value.encode(to: self)后,直接返回self.storage.popContainer(),就能对应上需要返回的对象呢?

  fileprivate mutating func popContainer() -> NSObject {
        precondition(self.containers.count > 0, "Empty container stack.")
        return self.containers.popLast()!
    }

查看_JSONEncodingStorage 中的popContainer()可知道,这个函数就是简单的返回storage中的最后一个对象.那么系统如何做到返回的最后一个对象就一定是我们encode后的结果呢?

这里还需要查看相关的SIL中间代码

// Student.encode(to:)
sil hidden @$s4Test7StudentV6encode2toys7Encoder_p_tKF : $@convention(method) (@in_guaranteed Encoder, @guaranteed Student) -> @error Error {
// %0 "encoder"                                   // users: %6, %2
// %1 "self"                                      // users: %37, %24, %11, %3
bb0(%0 : $*Encoder, %1 : $Student):
  debug_value_addr %0 : $*Encoder, let, name "encoder", argno 1 // id: %2
  debug_value %1 : $Student, let, name "self", argno 2 // id: %3
  debug_value undef : $Error, var, name "$error", argno 3 // id: %4
  %5 = alloc_stack $KeyedEncodingContainer<Student.CodingKeys>, var, name "container" // users: %49, %48, %70, %69, %64, %63, %57, %56, %10, %17, %30, %42
  %6 = open_existential_addr immutable_access %0 : $*Encoder to $*@opened("ACB10248-D7CA-11EA-B2A1-88E9FE691DD4") Encoder // users: %10, %10, %9
  %7 = metatype $@thin Student.CodingKeys.Type
  %8 = metatype $@thick Student.CodingKeys.Type   // user: %10
  %9 = witness_method $@opened("ACB10248-D7CA-11EA-B2A1-88E9FE691DD4") Encoder, #Encoder.container : <Self where Self : Encoder><Key where Key : CodingKey> (Self) -> (Key.Type) -> KeyedEncodingContainer<Key>, %6 : $*@opened("ACB10248-D7CA-11EA-B2A1-88E9FE691DD4") Encoder : $@convention(witness_method: Encoder) <τ_0_0 where τ_0_0 : Encoder><τ_1_0 where τ_1_0 : CodingKey> (@thick τ_1_0.Type, @in_guaranteed τ_0_0) -> @out KeyedEncodingContainer<τ_1_0> // type-defs: %6; user: %10
  %10 = apply %9<@opened("ACB10248-D7CA-11EA-B2A1-88E9FE691DD4") Encoder, Student.CodingKeys>(%5, %8, %6) : $@convention(witness_method: Encoder) <τ_0_0 where τ_0_0 : Encoder><τ_1_0 where τ_1_0 : CodingKey> (@thick τ_1_0.Type, @in_guaranteed τ_0_0) -> @out KeyedEncodingContainer<τ_1_0> // type-defs: %6
  %11 = struct_extract %1 : $Student, #Student.name // users: %55, %23, %19, %12
  retain_value %11 : $Optional<String>            // id: %12
  %13 = metatype $@thin Student.CodingKeys.Type
  %14 = enum $Student.CodingKeys, #Student.CodingKeys.name!enumelt // user: %16
  %15 = alloc_stack $Student.CodingKeys           // users: %16, %22, %19, %54
  store %14 to %15 : $*Student.CodingKeys         // id: %16
  %17 = begin_access [modify] [static] %5 : $*KeyedEncodingContainer<Student.CodingKeys> // users: %21, %19, %53
  // function_ref KeyedEncodingContainer.encodeIfPresent(_:forKey:)
  %18 = function_ref @$ss22KeyedEncodingContainerV15encodeIfPresent_6forKeyySSSg_xtKF : $@convention(method) <τ_0_0 where τ_0_0 : CodingKey> (@guaranteed Optional<String>, @in_guaranteed τ_0_0, @inout KeyedEncodingContainer<τ_0_0>) -> @error Error // user: %19
  try_apply %18<Student.CodingKeys>(%11, %15, %17) : $@convention(method) <τ_0_0 where τ_0_0 : CodingKey> (@guaranteed Optional<String>, @in_guaranteed τ_0_0, @inout KeyedEncodingContainer<τ_0_0>) -> @error Error, normal bb1, error bb4 // id: %19

这里是.encode(to: )函数被编译后的中间代码.猛的一看感觉确实看不明白,有特别多不熟悉的语法.虽然我不了解SIL具体的语法,但是也能在其中找到一些有价值的信息.

 #Encoder.container : <Self where Self : Encoder><Key where Key : CodingKey> (Self) -> (Key.Type) -> KeyedEncodingContainer<Key>

中间我看到了有一行这个函数,那Encoder的container是干什么的呢?
切回遵守Encoder的_JSONEncoder的源码

public func container<Key>(keyedBy: Key.Type) -> KeyedEncodingContainer<Key> {
        // 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<Key>(referencing: self, codingPath: self.codingPath, wrapping: topContainer)
        return KeyedEncodingContainer(container)
    }

我发现这个函数正好会调用self.storage.pushKeyedContainer()来生成storage的存储空间.这样的话好像一切也都有了一些头绪.我们在调用 try value.encode(to: self)的时候,其实编译器在背后也生成了相关的代码,帮助我们把这个encode的结果放到了storage的最后.这样在调用self.storage.popContainer()的时候,自然就返回了encode的结果.

到这里整个encode流程就结束了.

JSONDecoder

JSONDecoder这个类在JSONEncoder文件的下方.类比上么的encoder,我同样从decoder的入口函数出发.

   try? JSONDecoder().decode(Person.self, from: jsonData)

   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
    }

1.decode的流程正好和encode的流程相反,关键方法在于let decoder = _JSONDecoder(referencing: topLevel, options: self.options) 生成了一个decoder 和 let value = try decoder.unbox(topLevel, as: T.self) 这两个API上.
2.同样的_JSONDecoder遵从了Decoder协议,是真正功能的实现者.
3._JSONDecoder也有大量的unbox方法来对不同的数据类型做解包.

 fileprivate func unbox<T : Decodable>(_ value: Any, as type: T.Type) throws -> T? {
        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
    }

这个函数的大部分是跟我们平时使用不相关的,我们重点关注的是遵从codable协议的自定义模型,这样的话最终我们需要注意的就只有一小部分.

self.storage.push(container: value)
decoded = try T(from: self)
self.storage.popContainer()

这里我有一个疑问.为什么对T进行Decodable的init(from:)操作可以对内部的全部属性进行decode呢?

查看SIL代码后发现:

// Student.init(from:)
sil hidden @$s4Test7StudentV4fromACs7Decoder_p_tKcfC : $@convention(method) (@in Decoder, @thin Student.Type) -> (@owned Student, @error Error) {
// %0 "decoder"                                   // users: %98, %72, %15, %3
// %1 "$metatype"
bb0(%0 : $*Decoder, %1 : $@thin Student.Type):
  %2 = alloc_stack $Student, var, name "self"     // users: %62, %46, %30, %12, %9, %6, %73, %99, %100, %74
  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 : $*Student, #Student.name // user: %7
  store %5 to %6 : $*Optional<String>             // id: %7
  %8 = enum $Optional<String>, #Optional.none!enumelt // user: %10
  %9 = struct_element_addr %2 : $*Student, #Student.gender // user: %10
  store %8 to %9 : $*Optional<String>             // id: %10
  %11 = enum $Optional<Int>, #Optional.none!enumelt // user: %13
  %12 = struct_element_addr %2 : $*Student, #Student.age // user: %13
  store %11 to %12 : $*Optional<Int>              // id: %13
  %14 = alloc_stack $KeyedDecodingContainer<Student.CodingKeys>, let, name "container" // users: %67, %66, %59, %95, %94, %43, %88, %87, %27, %82, %81, %19, %77
  %15 = open_existential_addr immutable_access %0 : $*Decoder to $*@opened("ACB0820A-D7CA-11EA-B2A1-88E9FE691DD4") Decoder // users: %19, %19, %18
  %16 = metatype $@thin Student.CodingKeys.Type
  %17 = metatype $@thick Student.CodingKeys.Type  // user: %19
  %18 = witness_method $@opened("ACB0820A-D7CA-11EA-B2A1-88E9FE691DD4") Decoder, #Decoder.container : <Self where Self : Decoder><Key where Key : CodingKey> (Self) -> (Key.Type) throws -> KeyedDecodingContainer<Key>, %15 : $*@opened("ACB0820A-D7CA-11EA-B2A1-88E9FE691DD4") 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: %15; user: %19
  try_apply %18<@opened("ACB0820A-D7CA-11EA-B2A1-88E9FE691DD4") Decoder, Student.CodingKeys>(%14, %17, %15) : $@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 bb5 // type-defs: %15; id: %19

编译后,会把自定义模型的每一个属性都提取出来,然后用KeyedDecodingContainer做decode操作,看了这部分代码后理解了为什么我们并不需要单独处理Codable内属性的值的decode.

Codable的继承解析和编码问题

在测试过程中,我发现Codable如果牵扯到模型的继承关系,会解析错误.

class Animal: Codable {
    var age: Int?
}
class Dog: Animal {
    var name : String?
    var color : String?
    var blue : Bool?
}

当我们使用的模型需要用到继承关系的时候,仅写Codable和声明继承关系,无法直接使用JSONDecoder和JSONEncoder进行解析,按照之前的方式进行解析Dog模型的话,只能解析到子类中属性,但是无法做到解析父类中已经声明的属性.

这里需要手动对父类的属性进行赋值,同时还必须声明CodingKeys枚举

class Animal: Codable {
    var age: Int?
    private enum CodingKeys: String, CodingKey {
        case age
    }
    init() {}
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        age = try container.decode(Int.self, forKey: .age)
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(age, forKey: .age)
    }
}

class Dog: Animal {
    var name : String?
    var color : String?
    var blue : Bool?
    
    private enum CodingKeys: String, CodingKey {
        case name
        case color
        case blue
    }
    
    override init() { super.init() }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        try super.init(from: decoder)//这里需要手动调用super方法
        
        name = try container.decode(String.self, forKey: .name)
        color = try container.decode(String.self, forKey: .color)
        blue = try container.decode(Bool.self, forKey: .blue)
    }
    
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        try super.encode(to: encoder)//这里需要手动调用super方法
        
        try container.encode(name, forKey: .name)
        try container.encode(color, forKey: .color)
        try container.encode(blue, forKey: .blue)
    }
}

Codable整体结构

image

学习Codable的实现方案的体会

1.分层明确

对于JSONDecoder和JSONEncoder内部实现的逻辑进行分层,JSONDecoder和JSONEncoder仅为接口层,实现层给_JSONDecoder和_JSONEncoder来实现.内部的分层非常清晰,各个层级都负责某个层级需要做的事情.

2.在编译层面进行了很多隐式处理,代码显得非常简洁

编译层面做了特别多的时候,这个层面感觉我们好像没办法做很多事情,如果能在这个层面像Codable这样做一些事情,感觉也是很好的处理方式,但是这样感觉有一些阅读成本在里面

3.Codable源码中有大量的面向协议编程的身影

Codable中有大量的协议,之前只是很浅薄的知道swift提供了这种能力,但是之前并没有像Codable内部这种写代码的方式去写过.面向协议我目前的体会是解藕能力很优秀.如果想要自定义一个我们自己的JSONDecoder或者JSONEncoder只需要使用Codable内的协议就好,并不会对其他的地方有什么耦合的地方.以后可以多尝试面向协议编程.
如果有一天想把JSON格式换成XML,只需要将负责序列化的Encoder换掉,而不需要改动数据结构本身

4.接口设计风格

对于外部暴露出来的参数和接口,做到了最简化.这部分在后续的书写项目中的API设计风格可以做参考.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345