Swift5 解码对象的工作流程

我们来研究Swift中对象解码的实现过程。有了前面编码的内容做铺垫,形式上,解码过程基本就是编码的“逆过程”。这个过程中用到的类型,数据结构和编码过程是一一对应的。因此,我们就不再像之前研究编码一样去逐个讨论这些类型的细节,而是顺着解码的执行过程,来过一遍这部分的实现方式。

JSONDecoder

同样,我们还是从和用户直接交互的API说起。和JSONEncoder一样,JSONDecoder同样只是一个包装类,它定义在这里

@_objcRuntimeName(_TtC10Foundation13__JSONDecoder)
open class JSONDecoder {

}

在这个类的一开始,是和JSONEncoder对应的解码配置:

open class JSONDecoder {
 /// The strategy to use in decoding dates. Defaults to `.deferredToDate`.
  open var dateDecodingStrategy: DateDecodingStrategy = .deferredToDate

  /// The strategy to use in decoding binary data. Defaults to `.base64`.
  open var dataDecodingStrategy: DataDecodingStrategy = .base64

  /// The strategy to use in decoding non-conforming numbers. Defaults to `.throw`.
  open var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .throw

  /// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.
  open var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys

  /// Contextual user-provided information for use during decoding.
  open var userInfo: [CodingUserInfoKey : Any] = [:]
}

这些配置的含义,和JSONEncoder中的属性是完全一样的。也就是说,如果在JSONEncoder中定义了它们,在解码的时候,也要对JSONDecoder做同样的配置。

接下来,是一个用于包含默认配置的内嵌类型和属性:

open class JSONDecoder {
  fileprivate struct _Options {
    let dateDecodingStrategy: DateDecodingStrategy
    let dataDecodingStrategy: DataDecodingStrategy
    let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
    let keyDecodingStrategy: KeyDecodingStrategy
    let userInfo: [CodingUserInfoKey : Any]
  }

  /// The options set on the top-level decoder.
  fileprivate var options: _Options {
    return _Options(dateDecodingStrategy: dateDecodingStrategy,
                    dataDecodingStrategy: dataDecodingStrategy,
                    nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
                    keyDecodingStrategy: keyDecodingStrategy,
                    userInfo: userInfo)
  }
}

大家知道有这么个东西就行了,它的作用和JSONEncoder是一样的。然后,是JSONDecoder的默认构造函数:

open class JSONDecoder {
  // MARK: - Constructing a JSON Decoder
  /// Initializes `self` with default strategies.
  public init() {}
}

这部分很简单,没什么好说的。最后,就是公开给用户的decode方法了:

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: type) else {
        throw DecodingError.valueNotFound(type,
          DecodingError.Context(codingPath: [],
            debugDescription:
              "The given data did not contain a top-level value."))
    }

    return value
}

除了各种不正常情况抛出的异常之外,decode的执行流程是这样的:首先,用JSONSerialization.jsonObject把传递的Data变成一个Any对象;其次,用得到的Any对象和解码配置创建一个__JSONDecoder对象;最后,用这个__JSONDecoder对象的unbox方法,把Any对象“开箱”变成具体的类型。按照之前的经验不难想象,这个__JSONDecoder应该是一个遵从了Decoder的类型。事实上也的确如此,它的定义在这里

fileprivate class __JSONDecoder : Decoder {}

于是,接下来的探索就分成了两条路,一条是沿着unbox方法去看它是如何“开箱”Swift内置类型以及任意遵从了Decodable的类型;另一条,则是沿着__JSONDecoderDecoder身份去看它是如何为自定义“开箱”提供支持。

本着和研究__JSONEncoder的顺序一致,我们就先走unbox这条路。

unbox

根据之前的经验,__JSONDecoder自身应该有一大套用于解码Swift内建类型的unbox方法。当然,实际也是如此,这些方法定义在这里。同样,我们找一些有代表性的来看看。

由于在编码的时候,Bool、各种形式的Int / UInt以及浮点数都编码成了NSNumber。在解码的时候,要根据NSNumber的值,把原始的数据还原回来。因此,我们分别来看下Bool / Int / Double这三种类型的“开箱”过程。

首先,是Bool的开箱,它的定义在这里

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

  if let number = value as? NSNumber {
    // TODO: Add a flag to coerce non-boolean numbers into Bools?
    if number === kCFBooleanTrue as NSNumber {
        return true
    } else if number === kCFBooleanFalse as NSNumber {
        return false
    }

    /* FIXME: If swift-corelibs-foundation doesn't change to use NSNumber, this code path will need to be included and tested:
    } else if let bool = value as? Bool {
        return bool
    */
  }

  throw DecodingError._typeMismatch(
    at: self.codingPath, expectation: type, reality: value)
}

可以看到,如果valueNSNull或者value不能转型成NSNumber,都会进行对应的错误处理,不过这部分我们就忽略了。如果value是一个NSNumber,那么就根据它的值是kCFBooleanTrue / kCFBooleanFalse,返回Swift对应的true / false。这样,就把从Foundation得到的Any对象转型成了Bool

其次,是Int的开箱,它的定义在这里

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
}

可以看到,大体的流程和解码Bool是类似的,判断value可以成功转型成NSNumber之后,就把NSNumber.intValue赋值给了Swift中对应类型Int的变量。完成后,unbox还做了一层额外的检查,也就是确保目标变量int可以容纳下NSNumber表示的值。否则,就会生成一个数据损坏的异常。无论是Swift中有符号数或无符号数,也无论我们是否指定整数类型的长度,它们的解码逻辑是都一样的,只不过完成赋值之后,检查数据宽度时使用的类型不同而已,我们就不一一列举了,大家感兴趣的话,可以自己去看看。

最后,再来看浮点数的开箱,它的定义在这里

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

  if let number = value as? NSNumber,
    number !== kCFBooleanTrue,
    number !== kCFBooleanFalse {
    // We are always willing to return the number as a Double:
    // * If the original value was integral, it is guaranteed to fit in a Double; we are willing to lose precision past 2^53 if you encoded a UInt64 but requested a Double
    // * If it was a Float or Double, you will get back the precise value
    // * If it was Decimal, you will get back the nearest approximation
    return number.doubleValue
  } else if let string = value as? String,
      case .convertFromString(
        let posInfString,
        let negInfString,
        let nanString) = self.options.nonConformingFloatDecodingStrategy {
      if string == posInfString {
          return Double.infinity
      } else if string == negInfString {
          return -Double.infinity
      } else if string == nanString {
          return Double.nan
      }
  }

  throw DecodingError._typeMismatch(
    at: self.codingPath,
    expectation: type, reality: value)
}

很明显,这要比开箱BoolInt复杂多了。原因有两个:一个是这段代码前半段注释中说明的有可能编码的时候是整数,但却按照Double开箱。这时,可以分成三种情况:

  • 首先,是编码一个UInt64对象,开箱时超过253的部分会被忽略;
  • 其次,是编码一个Double/Float对象,开箱时就就会直接还原成Double
  • 最后,是编码一个Decimal对象,会还原成与其最接近的值;

但事情至此还没完,除了这些合法的浮点数之外,编码的时候我们看到过了,还可以用字符串定义各种非法的浮点数呢。因此,如果编码的时候采用了这种策略,开箱的时候必须能够处理,而这就是“开箱”Double后半部分的代码。如果value可以转换成String,那就按照JSONDecoder中关于解码浮点数的配置,把字符串分别转换成对应的infinity / nan

至此,这三种内建类型的解码就说完了。接下来还有什么呢?没错,编码的时候,我们还看过DateData,到了开箱,这两种类型只是根据JSONDecoder传递的类型解码配置,把Any还原成对应的类型罢了。我们来看个开箱Data的例子,它的定义在这里

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

  switch self.options.dataDecodingStrategy {
  case .deferredToData:
    self.storage.push(container: value)
    defer { self.storage.popContainer() }
    return try Data(from: self)

  case .base64:
    guard let string = value as? String else {
      throw DecodingError._typeMismatch(
        at: self.codingPath, expectation: type, reality: value)
    }

    guard let data = Data(base64Encoded: string) else {
      throw DecodingError.dataCorrupted(
        DecodingError.Context(
          codingPath: self.codingPath,
          debugDescription: "Encountered Data is not valid Base64."))
    }

    return data

  case .custom(let closure):
      self.storage.push(container: value)
      defer { self.storage.popContainer() }
      return try closure(self)
  }
}

看到了吧,其实关键点就是case语句中的几个return,要原始数据就是原始数据,要base64编码就base64编码,要执行定义过程就执行自定义过程,之后,把生成的Data返回就是了。至于解码Date的思路,和Data时类似的,只是操作的数据不同,大家可以自己去看代码,我们就不重复了。

看完了这些unbox方法之后,不难推测,在一开始__JSONDecoder里调用的unbox应该就是一个“开箱”的入口函数,它只负责把开箱工作转发给各种负责具体类型的unbox函数里。事实上的确如此,它的定义在这里

fileprivate func unbox<T : Decodable>(
  _ value: Any, as type: T.Type) throws -> T? {
  return try unbox_(value, as: type) as? T
}

而这个unbox_就是最终派发工作的人,它的定义在这里

fileprivate func unbox_(_ value: Any, as type: Decodable.Type) throws -> Any? {
  if type == Date.self || type == NSDate.self {
    return try self.unbox(value, as: Date.self)
  } else if type == Data.self || type == NSData.self {
    return try self.unbox(value, as: Data.self)
  } else if type == URL.self || type == 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."))
    }
    return url
  } else if type == Decimal.self || type == NSDecimalNumber.self {
    return try self.unbox(value, as: Decimal.self)
  } 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)
  }
}

看着挺长,实际上,只要你跟住每一个if里的return就不难理解它的作用了。

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

推荐阅读更多精彩内容