__JSONEncoder
是一个遵从了Encoder
的类型,从JSONEncoder.encode
的实现看,它才是真正开始处理数据编码的类型。并且,在Encodable
约束的方法中,也需要传入一个遵从Encoder
类型的参数。那么,这个Encoder
究竟做了什么呢?为了搞清楚这个问题,我们只能追着__JSONEncoder
的实现去一探究竟了。
__JSONEncoder
首先,是它的属性和初始化方法:
fileprivate class __JSONEncoder : Encoder {
fileprivate var storage: _JSONEncodingStorage
fileprivate let options: JSONEncoder._Options
public var codingPath: [CodingKey]
public var userInfo: [CodingUserInfoKey : Any] {
return self.options.userInfo
}
fileprivate init(
options: JSONEncoder._Options,
codingPath: [CodingKey] = []) {
self.options = options
self.storage = _JSONEncodingStorage()
self.codingPath = codingPath
}
}
在上面的代码里,codingPath
和userInfo
对于理解编码的整体流程来说并不重要,因此接下来的代码分析里,我们先暂时忽略它们。options
是一个JSONEncoder._Options
对象,通过上一节我们知道,这就是JSON编码时使用的各种配置的集合。结合上一节看到的创建__JSONEncoder
的代码:
let encoder = __JSONEncoder(options: self.options)
就看的更清楚了,这里传递的就是JSONEncoder
中表示配置集合的options
属性。
_JSONEncodingStorage
那么剩下的问题就变成了_JSONEncodingStorage
是什么呢?它的定义在这里。其实,就像这个类型的名字一样,它就是__JSONEncoding
这个类型使用的存储空间:
fileprivate struct _JSONEncodingStorage {
// MARK: Properties
/// The container stack.
/// Elements may be any one of the JSON types
/// (NSNull, NSNumber, NSString, NSArray, NSDictionary).
private(set) fileprivate var containers: [NSObject] = []
fileprivate init() {}
}
上面的注释中说了,在这个存储中保存的,只能是NSNull / NSNumber / NSString / NSArray / NSDictionary
这些类型。至于为什么,我们一会儿往后看自然就明白了。
另外,_JSONEncodingStorage
还有一些向container
中添加删除元素的方法:
fileprivate mutating func pushKeyedContainer() -> NSMutableDictionary {
let dictionary = NSMutableDictionary()
self.containers.append(dictionary)
return dictionary
}
fileprivate mutating func pushUnkeyedContainer() -> NSMutableArray {
let array = NSMutableArray()
self.containers.append(array)
return array
}
fileprivate mutating func push(container: __owned NSObject) {
self.containers.append(container)
}
fileprivate mutating func popContainer() -> NSObject {
precondition(!self.containers.isEmpty, "Empty container stack.")
return self.containers.popLast()!
}
用一句话总结这些方法就是,我们可以向在队尾追加NSMutableDictionary / NSMutableArray / NSObject
对象,或者删除队尾的元素。最后,_JSONEncodingStorage
还有一个获取存储大小的计算属性:
fileprivate var count: Int {
return self.containers.count
}
以上这就是这个内部存储类型的全部了。
向storage中添加元素
不过,看到这你可能会想了,作为__JSONEncoder
的内部存储,如果只能存储之前提到的那些NS
开头的对象,Swift里的类型显然不止这些啊,这是怎么回事呢?实际上,__JSONEncoder
有一个扩展,它可以把Swift中的各种内置类型,变成适合存放在这个内部存储中的类型。这个扩展的定义在这里。
打包整数
先来看几个简单的:
extension __JSONEncoder {
fileprivate func box(_ value: Bool) -> NSObject {
return NSNumber(value: value)
}
fileprivate func box(_ value: Int) -> NSObject {
return NSNumber(value: value)
}
fileprivate func box(_ value: String) -> NSObject {
return NSString(string: value)
}
}
看到了吧,通过这些重载的box
方法,Bool/Int
统一被“打包”成NSNumber
,String
被“打包”成NSString
。实际上,Swift中的每一个整数类型,都有一个对应的box
方法,把它“打包”成一个NSNumber
对象。
打包浮点数
再来看个稍微复杂一点儿的:
fileprivate func box(_ double: Double) throws -> NSObject {
guard !double.isInfinite && !double.isNaN else {
guard case let .convertToString(
positiveInfinity: posInfString,
negativeInfinity: negInfString,
nan: nanString) = self.options.nonConformingFloatEncodingStrategy else {
throw EncodingError._invalidFloatingPointValue(double, at: codingPath)
}
if double == Double.infinity {
return NSString(string: posInfString)
} else if double == -Double.infinity {
return NSString(string: negInfString)
} else {
return NSString(string: nanString)
}
}
return NSNumber(value: double)
}
当然,如果一切正常,Swift中的Double
也会被打包成NSNumber
。但由于Double
中有infinity
和nan
这两种情况:
let nan: Double = Double.nan
let infinity = Double.infinity
显然,编码过程不能对其置之不理,于是就有了返回NSNumber
之前这一大坨代码。上一节我们说过,JSONEncoder
中有一个控制浮点数编码的配置:
public enum NonConformingFloatEncodingStrategy {
case `throw`
case convertToString(
positiveInfinity: String,
negativeInfinity: String,
nan: String)
}
这个配置的默认值是case throw
,因此遇到这些特殊浮点数的时候,box
方法就会抛出EncodingError._invalidFloatingPointValue
异常。但如果我们想自定义这些特殊浮点数编码,只要像下面这样改变处理浮点数的配置就好了:
let encoder = JSONEncoder()
encoder.nonConformingFloatEncodingStrategy =
.convertToString(positiveInfinity: "Infinity",
negativeInfinity: "Negtive infinity",
nan: "Not a number")
然后,当我们编码下面这个数组的时候:
let doubleData = try encoder.encode([
Double.infinity,
-Double.infinity,
Double.nan
])
最终,box
方法就会把它们变成我们指定的字符串并返回了。这就是打包Double
类型的过程(如果你去翻一下源代码就会发现,Float
的处理逻辑和Double
是完全一样的)。
打包Dictionary
我们刚才看过的这两类box
方法,都直接对传递给它的参数进行了打包。接下来,我们再来看一类需要借助_JSONEncodingStorage
进行打包的box
方法。这里,就用打包Dictionary
类型举例,它的定义在这里:
fileprivate func box(_ dict: [String : Encodable]) throws -> NSObject? {
/// 1\. Request a new container
let depth = self.storage.count
let result = self.storage.pushKeyedContainer()
/// 2\. Box data
do {
for (key, value) in dict {
self.codingPath.append(
_JSONKey(stringValue: key, intValue: nil))
defer { self.codingPath.removeLast() }
result[key] = try box(value)
}
} catch {
if self.storage.count > depth {
let _ = self.storage.popContainer()
}
throw error
}
// 3\. Return the result
guard self.storage.count > depth else {
return nil
}
return self.storage.popContainer()
}
这个方法里要说的东西还蛮多的,为了方便理解,我在它的实现里添加了注释,来区分它执行过程中的三个部分。第一部分,是从storage
中开辟一个新空间,通过之前的实现就知道,result
现在是一个空的NSMutableDictionary
对象。
第二部分,是在result
中存储dict
中的内容。在这个遍历dict
的循环中,_JSONKey(stringValue: key, intValue: nil)
从字面上说,就表示执行JSON编码时,某一项使用的key。至于它具体的实现,我们先放放,理解它的含义就好了。之所以要在codingPath
中保存这个值,是因为接下来,使用try box(value)
打包对应value
时,这是一个可能发生异常的方法,而这个异常,有可能会用到当前的这个key的信息,以帮助开发者更好的排错。例如,在我们刚刚看到过的打包Double
的方法里:
fileprivate func box(_ double: Double) throws -> NSObject {
guard !double.isInfinite && !double.isNaN else {
guard case let .convertToString(
positiveInfinity: posInfString,
negativeInfinity: negInfString,
nan: nanString) = self.options.nonConformingFloatEncodingStrategy else {
throw EncodingError._invalidFloatingPointValue(double, at: codingPath)
}
/// ...
}
想象一下dict
中,某个key对应的value是一个不合法的浮点数,抛出的异常中,就会同时带有key和value的信息了。
当然,如果对应的value成功打包了,就直接把它保存到result[key]
中就好了。无论这个打包的结果如何,每一次循环之后,这个_JSONKey
创建的索引就没用了,在开始下一次迭代之前,使用defer { self.codingPath.removeLast() }
确保了无论在任何情况下,都可以把刚插入的这个_JSONKey
值撤销掉。
另外,如果遍历dict
的过程中发生了异常,编码过程肯定就无法继续下去了,于是在catch
语句里,我们先把之前从storage
中申请到的空间清理掉,然后重新抛出发生的异常。
以上,就是打包dict
值的过程。
第三部分,是返回打包结果。如果可以成功执行到这里,应该可以说,dict
的值是成功打包完了的,这时,我们只要返回storage
中的最后一项,自然就是包含打包结果的NSMutableDictionary
对象了。
至于return
语句上面的guard
,多多少少应该有点儿防御性代码的味道。什么时候self.storage.count
会小于等于depth
呢?唯一的解释,也只能就是在一开始从storage
开辟空间的时候,就失败了,以至于整个编码过程都没执行才会如此,emm... 想必这种情况,是很少见的。但无论如何,只要这种情况发生了,就直接返回nil
。
这样,打包Dictionary
的过程就全部说完了。
通用的box_
希望你还记得,上一节说到encode
方法实现的时候,在创建完__JSONEncoder
对象之后,调用了一个叫做box_
的方法。在理解了所有打包具象类型的box
方法之后,现在是时候来看它的实现了。
简单来说,它就是一个打包数据方法的汇总,在它的前半段实现中,判断了要打包的数据类型,如果是之前实现过的有内建打包代码的类型,就调用对应的box
方法:
fileprivate func box_(_ value: Encodable) throws -> NSObject? {
let type = Swift.type(of: value)
if type == Date.self || type == NSDate.self {
// Respect Date encoding strategy
return try self.box((value as! Date))
} else if type == Data.self || type == NSData.self {
// Respect Data encoding strategy
return try self.box((value as! Data))
} else if type == URL.self || type == NSURL.self {
// Encode URLs as single strings.
return self.box((value as! URL).absoluteString)
} else if type == Decimal.self || type == NSDecimalNumber.self {
// JSONSerialization can natively handle NSDecimalNumber.
return (value as! NSDecimalNumber)
} else if value is _JSONStringDictionaryEncodableMarker {
return try self.box(value as! [String : Encodable])
}
}
否则,如果没有匹配,则说明value
仅仅是一个支持Encodable
的类型。该如何打包这个数据,则完全依赖这个类型自身提供的encode(to:)
方法了。这部分的实现和打包Dictionary
时的逻辑,是非常类似的:
fileprivate func box_(_ value: Encodable) throws -> NSObject? {
/// The same as before...
let depth = self.storage.count
do {
try value.encode(to: self)
} catch {
if self.storage.count > depth {
let _ = self.storage.popContainer()
}
throw error
}
// The top container should be a new container.
guard self.storage.count > depth else {
return nil
}
return self.storage.popContainer()
}
看到了吧,核心逻辑就是调用try value.encode(to: self)
编码value
,得到结果应该保存在storage
中的最后一个元素里。完成后,把这个元素返回,就是这个遵从了Encodable
类型的值的打包结果了。
看到这你可能会想了,哎~~~~等会等会,为什么编码的结果就保存在storage
的最后一个元素里呢?emm...要说清这个过程我们还有一段路要走,现在姑且就这么理解就行了。
因此,只要执行到这里,我们就应该相信,box_
的返回值,就是一个被包装好的了可以扔给Foundation API去执行JSON编码的值了。这时,我们不妨把上一节中的encode
代码在罗列出来看一下,之前省略掉了一些错误处理的逻辑,现在理解了box_
之后,就可以把它们呈现出来了:
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."))
}
/// The same as before...
}
虽然看着挺长,不过它表达的逻辑很简单,执行过box_
之后,只有最外层的类型是NSMutableArray
或者NSMutableDictionary
的时候,这个结果才能送往Foundation进行JSON编码。如果打包出来的类型只是某个单一形式的值,就会抛出对应的异常。例如,如果你执行下面的代码:
let data = try JSONEncoder().encode(11)
就会在控制台看到"Top-level Int encoded as number JSON fragment."
这样的错误提示了。