我们来研究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
的类型;另一条,则是沿着__JSONDecoder
的Decoder
身份去看它是如何为自定义“开箱”提供支持。
本着和研究__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)
}
可以看到,如果value
是NSNull
或者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)
}
很明显,这要比开箱Bool
和Int
复杂多了。原因有两个:一个是这段代码前半段注释中说明的有可能编码的时候是整数,但却按照Double
开箱。这时,可以分成三种情况:
- 首先,是编码一个
UInt64
对象,开箱时超过253的部分会被忽略; - 其次,是编码一个
Double/Float
对象,开箱时就就会直接还原成Double
; - 最后,是编码一个
Decimal
对象,会还原成与其最接近的值;
但事情至此还没完,除了这些合法的浮点数之外,编码的时候我们看到过了,还可以用字符串定义各种非法的浮点数呢。因此,如果编码的时候采用了这种策略,开箱的时候必须能够处理,而这就是“开箱”Double
后半部分的代码。如果value
可以转换成String
,那就按照JSONDecoder
中关于解码浮点数的配置,把字符串分别转换成对应的infinity / nan
。
至此,这三种内建类型的解码就说完了。接下来还有什么呢?没错,编码的时候,我们还看过Date
和Data
,到了开箱,这两种类型只是根据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
就不难理解它的作用了。