Swift 4.0
后引入的特性,目标是取代NSCoding
协议。对结构体,枚举和类都支持,能够把JSON
这种弱类型数据转换成代码中使用的强类型数据,同时由于编译器的帮助,可以少写很多重复代码。
Codable
协议被设计出来用于替代NSCoding
协议,所以遵从Codable
协议的对象就可以无缝的支持NSKeyedArchive
r和NSKeyedUnarchiver
对象进行Archive&UnArchive
持久化和反持久化。原有的解码过程和持久化过程需要单独处理,现在通过新的Codable
协议一起搞定。
Codable
定义:(由Decodable
和Encodable
协议组成):
public typealias Codable = Decodable & Encodable
Decodable
协议定义了一个初始化函数:init
,遵从Decodable
协议的类型可以使用任何Decoder
对象进行初始化,完成解码过程。
public protocol Decodable {
init(from decoder: Decoder) throws
}
Encodable
协议定义了一个encode
方法, 任何Encoder
对象都可以创建遵从了Encodable
协议类型的表示,完成编码过程。
public protocol Encodable {
func encode(to encoder: Encoder) throws
}
Swift
标准库中的类型String
、Int
、Double
以及Foundation
框架中Data
、Date
、URL
都是默认支持Codable
协议的,只需声明支持协议即可。
基础语法
一个最简单的例子:
struct Hotpot: Codable{
var name: String
var age: Int
var height: Double
}
Hotpot
遵循了Codable
协议,他就默认有了init(from:)
和encode(to:)
方法。
原始数据(为了演示方便以字符串代替):
let jsonString = """
{
"age": 10,
"name": "cat",
"height": 1.85
}
"""
解码
let jsonData = jsonString.data(using: .utf8)
//创建解码器
let jsonDecoder = JSONDecoder()
if let data = jsonData {
//解码,参数:数据类型,原始data数据
let hotpot = try? jsonDecoder.decode(Hotpot.self, from: data)
print(hotpot ?? "error")
}
//输出
Hotpot(name: "cat", age: 10, height: 1.85)
- 创建解码器
- 解码
decode
传入类型(Hotpot.self
)和数据(data
)
编码
let jsonEncode = JSONEncoder()
let jsonData1 = try? jsonEncode.encode(hotpot)
if let data = jsonData1 {
let jsonString1 = String(decoding: data, as: UTF8.self)
print(jsonString1)
}
//输出
{"name":"cat","age":10,"height":1.8500000000000001}
这里发现在encode
的时候精度出现了问题:
解决方案大致分为以下几种:
- 浮点数转
string
处理。在encode
时候double
值转为String
就能避免这个问题。 - 使用
NSDecimalNumber
来接收,由于NSDecimalNumber
无法遵循Codable
协议(原因),所以需要用Decimal
来接收数据,NSDecimalNumber
作为计算属性来处理数据。 - 三方库 金额计算。
对于第二点:
Synthesizing conformace to Codable, Equatable and Hashable in different source files is currently not supported by the Swift compiler。
At the moment (Xcode 10.2.1 / Swift 5.0.1)Codable
currently isn't supported yet if an extension in one file adds conformance in a different file. Check this out at https://bugs.swift.org/. https://bugs.swift.org/browse/SR-6101
大概意思是目前Swift
不支持在不同的源文件,合成conformace
到Codable
、Equatable
和Hashable
。
什么意思呢?看个例子就明白了:
在A
文件中定义一个class
,在B
文件扩展这个class
遵循协议codable
就会报错了。//A文件: class HPTestCodable { } //B文件: extension HPTestCodable:Codable { }
解决精度问题
以第二种方式为例,这里仅仅是为了解决Encode
过程中height
精度问题:
struct Hotpot: Codable{
var name: String
var age: Int
var height: Decimal
var heightNumber: NSDecimalNumber {
get {
return NSDecimalNumber(decimal: height)
}
set {
height = newValue.decimalValue
}
}
}
调用:
/**json数据*/
let jsonString = """
{
"age": 10,
"name": "cat",
"height": 1.85
}
"""
let jsonData = jsonString.data(using: .utf8)
/**解码*/
var hotpot:Hotpot?
if let data = jsonData {
//解码,参数:数据类型,原始data数据
hotpot = try? JSONDecoder().decode(Hotpot.self, from: data)
if let height = hotpot?.height.description {
print(height)
}
print(hotpot ?? "error")
}
/**编码*/
let jsonEncode = JSONEncoder()
let jsonData1 = try? jsonEncode.encode(hotpot)
if let data = jsonData1 {
let jsonString = String(decoding: data, as: UTF8.self)
print(jsonString)
}
结果:
1.85
Hotpot(name: "cat", age: 10, height: 1.85)
{"name":"cat","age":10,"height":1.85}
这里看到decode、encode、取值
精度都没有问题了。
归档反归档
由于JSONEncoder
将数据转换成了data
,我们可以直接对数据进行存储了,可以写入本地文件、数据库等。以下简单写入UserDefaults
演示(当然用NSKeyedArchiver
也可以):
if let data = try? JSONEncoder().encode(hotpot) {
let jsonString = String(decoding: data, as: UTF8.self)
//写入UserDefaults
UserDefaults.standard.set(data, forKey: "hotpotData")
print(jsonString)
}
// 从UserDefaults读取
if let data = UserDefaults.standard.data(forKey: "hotpotData") {
let jsonString = String(decoding: data, as: UTF8.self)
print(jsonString)
}
{"name":"cat","age":10,"height":1.85}
{"name":"cat","age":10,"height":1.85}
嵌套的模型
struct Hotpot: Codable{
var name: String
var age: Int
var height: Double
var cat: Cat
}
extension Hotpot {
struct Cat: Codable {
var name: String
var age: Int
}
}
let jsonString = """
{
"name": "hotpot",
"height": 1.85,
"age": 18,
"cat": {
"age": 1,
"name": "cat"
}
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
if let data = jsonData {
let result = try? decoder.decode(Hotpot.self, from: data)
print(result ?? "解析失败")
}
输出:
Hotpot(name: "hotpot", age: 18, height: 1.85, cat: SwiftProtocol.Hotpot.Cat(name: "cat", age: 1))
- 对于遵循了
Codable
协议的类型,解码器能够自动识别模型的嵌套。
包含数组
struct Hotpot: Codable{
var name: String
var age: Int
var height: Double
var cat: [Cat]
}
extension Hotpot {
struct Cat: Codable {
var name: String
var age: Int
}
}
let jsonString = """
{
"name": "hotpot",
"height": 1.85,
"age": 18,
"cat": [{
"age": 1,
"name": "cat"
},{
"age": 10,
"name": "cat1"
}]
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
if let data = jsonData{
let result = try? decoder.decode(Hotpot.self, from: data)
print(result ?? "解析失败")
}
Hotpot(name: "hotpot", age: 18, height: 1.85, cat: [SwiftProtocol.Hotpot.Cat(name: "cat", age: 1), SwiftProtocol.Hotpot.Cat(name: "cat1", age: 10)])
可以看到不需要做额外的操作。
JSON数据是数组集合
let jsonString = """
[
{
"age" : 18,
"cat" : [
{
"age" : 1,
"name" : "cat"
},
{
"age" : 10,
"name" : "cat1"
}
],
"name" : "hotpot",
"height" : 1.85
},
{
"age" : 18,
"cat" : [
{
"age" : 1,
"name" : "cat"
},
{
"age" : 10,
"name" : "cat1"
}
],
"name" : "hotpot",
"height" : 1.85
}
]"""
//调用
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
if let data = jsonData{
let result = try? decoder.decode([Hotpot].self, from: data)
print(result ?? "解析失败")
}
[SwiftProtocol.Hotpot(name: "hotpot", age: 18, height: 1.85, cat: [SwiftProtocol.Hotpot.Cat(name: "cat", age: 1), SwiftProtocol.Hotpot.Cat(name: "cat1", age: 10)]), SwiftProtocol.Hotpot(name: "hotpot", age: 18, height: 1.85, cat: [SwiftProtocol.Hotpot.Cat(name: "cat", age: 1), SwiftProtocol.Hotpot.Cat(name: "cat1", age: 10)])]
只需要解析模型对应的类型传数组[Hotpot].self
就ok了。
JSON数据中有 Optional values
一般情况下后台可能会传给我们null
值,如果上面的例子不做修改直接解析会失败。处理不当可能会crash。
一般情况下我们把可能为空的值声明为可选值解决。
struct Hotpot: Codable{
var name: String
var age: Int?
var height: Double
var cat: [Cat]
}
extension Hotpot {
struct Cat: Codable {
var name: String
var age: Int
}
}
let jsonString = """
[
{
"age" : 18,
"cat" : [
{
"age" : 1,
"name" : "cat"
},
{
"age" : 10,
"name" : "cat1"
}
],
"name" : "hotpot",
"height" : 1.85
},
{
"age" : null,
"cat" : [
{
"age" : 1,
"name" : "cat"
},
{
"age" : 10,
"name" : "cat1"
}
],
"name" : "hotpot",
"height" : 1.85
}
]
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
if let data = jsonData{
let result = try? decoder.decode([Hotpot].self, from: data)
print(result ?? "解析失败")
}
[SwiftProtocol.Hotpot(name: "hotpot", age: Optional(18), height: 1.85, cat: [SwiftProtocol.Hotpot.Cat(name: "cat", age: 1), SwiftProtocol.Hotpot.Cat(name: "cat1", age: 10)]), SwiftProtocol.Hotpot(name: "hotpot", age: nil, height: 1.85, cat: [SwiftProtocol.Hotpot.Cat(name: "cat", age: 1), SwiftProtocol.Hotpot.Cat(name: "cat1", age: 10)])]
元组类型
比如我们有一个坐标,后台服务器传给"location": [114.121344, 38.908766]
,这个时候用数组接显然不好维护。返回的数组怎么和模型对应上呢? 这个时候就需要自己写解析实现"init(from decoder: Decoder)"
struct Location: Codable {
var x: Double
var y: Double
init(from decoder: Decoder) throws {
//不解析当前的key,也就是解码时不要key值。
var contaioner = try decoder.unkeyedContainer()
//单方面把值给到x与y
self.x = try contaioner.decode(Double.self)
self.y = try contaioner.decode(Double.self)
}
}
struct RawSeverResponse: Codable {
var location: Location
}
let jsonString = """
{
"location": [114.121344, 38.908766]
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(RawSeverResponse.self, from: jsonData!)
print(result)
RawSeverResponse(location: SwiftProtocol.Location(x: 114.121344, y: 38.908766))
继承
class Hotpot: Codable {
var name: String?
}
class HotpotCat: Hotpot {
var age: Int?
}
let jsonString = """
{
"name": "hotpot",
"age": 18
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(HotpotCat.self, from: jsonData!)
print(result.name)
print(result.age)
按照猜想上面的代码应该age
和name
都能正常解析,看下输出:
Optional("hotpot")
nil
age
没有解析?那么Codable
放在子类上呢?
class Hotpot {
var name: String?
}
class HotpotCat: Hotpot, Codable {
var age: Int?
}
let jsonString = """
{
"name": "hotpot",
"age": 18
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(HotpotCat.self, from: jsonData!)
print(result.name)
print(result.age)
nil
Optional(18)
name
没有解析。
这个时候就需要我们在派生类中重写init(from decoder: Decoder)
,如果要编码还需要encode(to encoder: Encoder)
并且需要定义CodingKeys
。
class Hotpot: Codable {
var name: String?
}
class HotpotCat: Hotpot {
var age: Int?
private enum CodingKeys: String,CodingKey{
case name
case age
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
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)
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)
try super.encode(to: encoder)
}
}
let jsonString = """
{
"name": "hotpot",
"age": 18
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(HotpotCat.self, from: jsonData!)
print(result.name)
print(result.age)
Optional("hotpot")
Optional(18)
为什么派生类不能自动解析?原理后面再分析。
协议
如果有协议的参与下解析会怎么样呢?
protocol HotpotProtocol: Codable {
var name: String{ get set }
}
struct Hotpot: HotpotProtocol {
var name: String
var age: Int?
}
let jsonString = """
{
"name": "hotpot",
"age": 18
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(Hotpot.self, from: jsonData!)
print(result)
Hotpot(name: "hotpot", age: Optional(18))
可以看到能够正常解析。
key值不同
对于一些不符合规范的字段,我们一般是会给别名来解析,那么在Codable
中怎么实现呢?
比如后台返回了这样的数据,正常情况下应该返回一个数组和对应的type。
let jsonString = """
{
"01Type" : "hotpot",
"item.1" : "mouse",
"item.2" : "dog",
"item.3" : "hotpot",
"item.0" : "cat"
}
"""
//如果不需要编码,直接遵循 Decodable 就好了
struct Hotpot: Decodable {
var type: String?
let elements: [String]
enum CodingKeys: String, CaseIterable, CodingKey {
case type = "01Type"
case item0 = "item.0"
case item1 = "item.1"
case item2 = "item.2"
case item3 = "item.3"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.type = try container.decode(String.self, forKey: .type)
var element: [String] = []
//方式一:遍历 container
for item in container.allKeys {
switch item {
case .item0,.item1,.item2,.item3:
element.append(try container.decode(String.self, forKey: item))
default:
continue
}
}
//方式二:遍历CodingKeys 和 contains比较装入集合
// for item in CodingKeys.allCases {
// guard container.contains(item) else {
// continue
// }
// if item != .type {
// element.append(try container.decode(String.self, forKey: item))
// }
// }
self.elements = element
}
}
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(Hotpot.self, from: jsonData!)
print(result)
Hotpot(type: Optional("hotpot"), elements: ["cat", "mouse", "dog", "hotpot"])
源码解析
Decodable
public protocol Decodable {
init(from decoder: Decoder) throws
}
对应的Decoder
提供具体解码
public protocol Decoder {
var codingPath: [CodingKey] { get }
var userInfo: [CodingUserInfoKey : Any] { get }
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey
func unkeyedContainer() throws -> UnkeyedDecodingContainer
func singleValueContainer() throws -> SingleValueDecodingContainer
}
Encodable
public protocol Encodable {
func encode(to encoder: Encoder) throws
}
对应的Encoder
提供具体编码
public protocol Encoder {
var codingPath: [CodingKey] { get }
var userInfo: [CodingUserInfoKey : Any] { get }
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey
func unkeyedContainer() -> UnkeyedEncodingContainer
func singleValueContainer() -> SingleValueEncodingContainer
}
JSONDecoder
open class JSONDecoder {
public enum DateDecodingStrategy {
/// Defer to `Date` for decoding. This is the default strategy.
case deferredToDate
/// 距离 1970.01.01的秒数
case secondsSince1970
/// 距离 1970.01.01的毫秒数
case millisecondsSince1970
///日期编码格式
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
case iso8601
///后台自定义格式,这个时候可以创建自定义DataFormatter来解析
case formatted(DateFormatter)
///自定义格式,提供一个闭包表达式
case custom((Decoder) throws -> Date)
}
/// 二进制数据解码策略
public enum DataDecodingStrategy {
/// 默认
case deferredToData
/// base64
case base64
/// 自定义
case custom((Decoder) throws -> Data)
}
/// 不合法浮点数编码策略
public enum NonConformingFloatDecodingStrategy {
/// Throw upon encountering non-conforming values. This is the default strategy.
case `throw`
/// Decode the values from the given representation strings.
case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String)
}
/// key值编码策略
public enum KeyDecodingStrategy {
/// Use the keys specified by each type. This is the default strategy.
case useDefaultKeys
//指定去掉中间下划线,变成驼峰命名
case convertFromSnakeCase
//自定义
case custom(([CodingKey]) -> CodingKey)
}
open var nonConformingFloatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy
/// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.
open var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy
/// Contextual user-provided information for use during decoding.
open var userInfo: [CodingUserInfoKey : Any]
/// Initializes `self` with default strategies.
///默认init方法
public init()
/// Decodes a top-level value of the given type from the given JSON representation.
/// - parameter type: The type of the value to decode.
/// - parameter data: The data to decode from.
/// - returns: A value of the requested type.
/// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid JSON.
/// - throws: An error if any value throws an error during decoding.
open func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
}
DateDecodingStrategy
以何种策略解析日期格式。
let jsonString = """
{
"name" : "hotpot",
"age" : 18,
"date" : 1610390703
}
"""
struct Hotpot: Codable {
var name: String
var age: Double
var date: Date
}
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(Hotpot.self, from: jsonData!)
print(result)
deferredToDate
"date" : 1610390703
decoder.dateDecodingStrategy = .deferredToDate
Hotpot(name: "hotpot", age: 18.0, date: 2052-01-12 18:45:03 +0000)
使用默认格式无法正确推导出时间。
secondsSince1970
decoder.dateDecodingStrategy = .secondsSince1970
Hotpot(name: "hotpot", age: 18.0, date: 2021-01-11 18:45:03 +0000)
millisecondsSince1970
"date" : 1610390703000
decoder.dateDecodingStrategy = .millisecondsSince1970
Hotpot(name: "hotpot", age: 18.0, date: 2021-01-11 18:45:03 +0000)
iso8601
"date" : "2021-01-11T19:20:20Z"
decoder.dateDecodingStrategy = .iso8601
Hotpot(name: "hotpot", age: 18.0, date: 2021-01-11 19:20:20 +0000)
formatted
"date" : "2021/01/11 19:20:20"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
Hotpot(name: "hotpot", age: 18.0, date: 2021-01-11 11:20:20 +0000)
custom
custom相对比较灵活,可以做一些错误处理以及格式不固定的情况下使用。
"date" : "1610390703"
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let dateStr = try container.decode(String.self)
return Date(timeIntervalSince1970: Double(dateStr)!)
})
Hotpot(name: "hotpot", age: 18.0, date: 2021-01-11 18:45:03 +0000)
func decode
源码:
open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {//泛型函数,同时当前泛型约束为遵循了Decodable的协议
let topLevel: Any
do {
//JSONSerialization将二进序列化成json
topLevel = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
}
// __JSONDecoder 传入序列化的json 和 self.options 编码策略创建decoder对象
let decoder = __JSONDecoder(referencing: topLevel, options: self.options)
//unbox 拆 topLevel 数据 返回解码后的value
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.这里是一个泛型函数,传入的参数T
要求遵守 Decodable
协议;
2.调用 JSONSerializationg
对当前 data 进行序列话为json数据;
3.通过__JSONDecoder
传入json
数据和编码策略创建decoder
对象;
4.unbox
解码json
串,返回value
。
__JSONDecoder
私有类源码(初始化方法):
init(referencing container: Any, at codingPath: [CodingKey] = [], options: JSONDecoder._Options) {
//内部类 _JSONDecodingStorage
self.storage = _JSONDecodingStorage()
//存放要解码的数据
self.storage.push(container: container)
self.codingPath = codingPath
self.options = options
}
_JSONDecodingStorage
本身用数组存放数据,是一个容器
private struct _JSONDecodingStorage {
// MARK: Properties
/// The container stack.
/// Elements may be any one of the JSON types (NSNull, NSNumber, String, Array, [String : Any]).
private(set) var containers: [Any] = []
// MARK: - Initialization
/// Initializes `self` with no containers.
init() {}
// MARK: - Modifying the Stack
var count: Int {
return self.containers.count
}
var topContainer: Any {
precondition(!self.containers.isEmpty, "Empty container stack.")
return self.containers.last!
}
mutating func push(container: __owned Any) {
self.containers.append(container)
}
mutating func popContainer() {
precondition(!self.containers.isEmpty, "Empty container stack.")
self.containers.removeLast()
}
}
unbox
源码:
func unbox<T : Decodable>(_ value: Any, as type: T.Type) throws -> T? {
return try unbox_(value, as: type) as? T
}
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)
}
}
分析下日期格式的处理
这也就是我们设置不同的日期处理策略能够正常解析的原因。不同的策略走不同的分支解析。
_JSONStringDictionaryDecodableMarker
:
#if arch(i386) || arch(arm)
internal protocol _JSONStringDictionaryDecodableMarker {
static var elementType: Decodable.Type { get }
}
#else
private protocol _JSONStringDictionaryDecodableMarker {
static var elementType: Decodable.Type { get }
}
#endif
extension Dictionary : _JSONStringDictionaryDecodableMarker where Key == String, Value: Decodable {
static var elementType: Decodable.Type { return Value.self }
}
- 字典扩展遵循了这个协议,限制条件:
Key == String, Value: Decodable
。
_JSONStringDictionaryDecodableMarker
的unbox
:
是一个递归调用。unbox_
就是最开始的区分类型(Date
,Data
……)的方法。
看一个例子:
struct Hotpot: Codable {
var name: String
var age: Double
}
let jsonString = """
{
"name" : "hotpot",
"age" : 18
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(Hotpot.self, from: jsonData!)
print(result)
对应的SIL
代码:
- 解码对应的
key
值通过CodingKeys
去找,decodable
约束的。 -
decode
方法。 -
encode
方法。
init
方法源码中是:
return try type.init(from: self)
传入的decoder
是self
,这里的self
是JSONDecoder
。
init(from
SIL
的实现:
查看
_ JSONDecoder
源码确实实现了Decoder
协议sil
中调用的是_JSONDecoder
实现的container
方法:这里返回的是
KeyedDecodingContainer(container)
对象。KeyedDecodingContainer
内容:可以看到有对应每种类型的
decode
方法,意味着会根据类型匹配对应的方法解码。这么多方法苹果其实是用内部的一个工具生成的。具体是用源码中的Codable.swift.gyb
文件。通过Codable.swift.gyb
生成Codable.swift
源文件。Codable.swift.gyb
文件内容:集合中放了可编解码类型,
%%
代表语句的开始和结束。通过python
控制的。相当于模板文件。自己实现下
init
方法:
struct Hotpot: Codable {
var name: String
var age: Double
//_JSONDecoder,返回container
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(Double.self, forKey: .age)
}
}
可以看到对应的CodingKeys
和decode
正是系统生成调用的方法,CodingKeys
系统帮我们生成了,这也就是我们可以在CodingKeys
中操作key
不一致的原因。
那么如果继承自OC
的类呢?
HPOCModel
是一个OC
类:
@interface HPOCModel : NSObject
@property (nonatomic, assign) double height;
@end
@implementation HPOCModel
@end
class Hotpot:HPOCModel, Decodable {
var name: String
var age: Double
private enum CodingKeys: String,CodingKey{
case name
case age
case height
}
//_JSONDecoder,返回container
required 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(Double.self, forKey: .age)
super.init()
self.height = try container.decode(Double.self, forKey: .height)
}
}
let jsonString = """
{
"name" : "hotpot",
"age" : 18,
"height":1.85
}
"""
let jsonData = jsonString.data(using: .utf8)
let decoder = JSONDecoder()
let result = try decoder.decode(Hotpot.self, from: jsonData!)
print(result.height)
1.85
这个时候init
方法就要我们自己去实现了。
整个流程:
JSONEncoder
class Hotpot: Codable {
var name: String?
var age: Int?
}
class Cat: Hotpot {
var height: Double?
}
let cat = Cat()
cat.name = "cat"
cat.age = 18
cat.height = 1.85
let enCoder = JSONEncoder()
let jsonData = try enCoder.encode(cat)
let jsonStr = String(data: jsonData, encoding: .utf8)
print(jsonStr)
Optional("{\"name\":\"cat\",\"age\":18}")
源码分析:
encode
方法:
open func encode<T : Encodable>(_ value: T) throws -> Data {
let encoder = _JSONEncoder(options: self.options)
//先box包装成string
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."))
}
//序列化
let writingOptions = JSONSerialization.WritingOptions(rawValue: self.outputFormatting.rawValue).union(.fragmentsAllowed)
//返回data
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))
}
}
- 在这里真正处理数据的是
_JSONEncoder
; -
box
根据不同数据类型,把value
包装成对应的数据类型; -
JSONSerialization
来返回data数据。
_JSONEncoder
:
//实现Encoder的方法
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)
//返回KeyedEncodingContainer,最终KeyedEncodingContainer encode根据数据类型编码数据
return KeyedEncodingContainer(container)
}
-
_JSONEncoder
遵循Encoder
协议实现了container
方法,最终KeyedEncodingContainer
提供对应数据类型encode
方法编码数据。
box_
:
我们自定义数据类型会走到
value.encode
,参数self
就是_JSONEcoder
。正好的解码过程相反。最终都会都到
value is _JSONStringDictionaryEncodableMarker
_JSONStringDictionaryEncodableMarker
:在最开始的例子中,Cat
中的height
并没有被编码出来。
先看下继承和协议遵循关系:
那么
Hotpot
遵循了Codable
协议,编译器默认生成了encode
方法,在调用过程中由于Cat
中没有对应encode
方法,所以会去父类中找。所以编码结果中没有height
。SIL
验证下:
class Hotpot : Decodable & Encodable {
@_hasStorage @_hasInitialValue var name: String? { get set }
@_hasStorage @_hasInitialValue var age: Int? { get set }
@objc deinit
init()
enum CodingKeys : CodingKey {
case name
case age
@_implements(Equatable, ==(_:_:)) static func __derived_enum_equals(_ a: Hotpot.CodingKeys, _ b: Hotpot.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)
}
required init(from decoder: Decoder) throws
func encode(to encoder: Encoder) throws
}
@_inheritsConvenienceInitializers class Cat : Hotpot {
@_hasStorage @_hasInitialValue var height: Double? { get set }
@objc deinit
override init()
required init(from decoder: Decoder) throws
}
Hotpot
中有init
和encode
方法。而在Cat
中编译器覆盖了init(from decoder: Decoder)
方法,而没有覆盖encode
。这也就解释了解码能够无缝对接,编码不行的原因。
那么我们需要在子类中重写encode
方法:
class Hotpot: Codable {
var name: String?
var age: Int?
}
class Cat: Hotpot {
var height: Double?
enum CodingKeys: String,CodingKey {
case height
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(height, forKey: .height)
try super.encode(to: encoder)
}
}
let cat = Cat()
cat.name = "cat"
cat.age = 18
cat.height = 1.8
let enCoder = JSONEncoder()
let jsonData = try enCoder.encode(cat)
let jsonStr = String(data: jsonData, encoding: .utf8)
Optional("{\"name\":\"cat\",\"age\":18,\"height\":1.8}")
当然这里要注意数据的精度问题,前面用法里面已经说过了。
有一个小的点try super.encode(to: encoder)
如果用自己的编码器呢?
try super.encode(to: container.superEncoder())
Optional("{\"super\":{\"name\":\"cat\",\"age\":18},\"height\":1.8}")
编码值中多了super
。所以这里最好就传当前类的encoder
。否则多次编解码会错误解析不到数据。
对于上面编码后的值,再解码看看:
let cat2 = try JSONDecoder().decode(Cat.self, from: jsonData)
Optional("{\"name\":\"cat\",\"age\":18,\"height\":1.8}")
SwiftProtocol.Cat
(lldb) po cat2.name
▿ Optional<String>
- some : "cat"
(lldb) po cat2.age
▿ Optional<Int>
- some : 18
(lldb) po cat2.height
nil
可以看到height
没有解析出来。这和前面的结论一样,看下SIL
:
在最后确实调用了
Hotpot
的decode
方法,所以父类中的数据解析没有问题,但是height
在SIL
中只有创建没有赋值。没有创建Cat
自己本身的container
。再看下和Hotpot
的区别:可以看到区别是仅仅
Cat
中根本没有创建Container
,仅是调用了父类的decode
方法,这也是为什么没有解析出来自己的属性。(Swift源码中继承后在
vscode
中调试报错,只能编译一份Xcode源码再调试了,后面再补充吧).源码调试待补充
我们自己实现
cat
的init
方法看下sil
代码:
class Hotpot:Codable {
var name: String?
var age: Int?
}
class Cat: Hotpot {
var height: Double?
enum CodingKeys: String, CodingKey {
case height
}
required init(from decoder: Decoder) throws {
var container = try decoder.container(keyedBy: CodingKeys.self)
self.height = try container.decode(Double.self, forKey: .height)
try! super.init(from: decoder)
}
}
let jsonString = "{\"name\" : \"hotpot\",\"age\" : 18,\"height\":1.8}"
let cat2 = try JSONDecoder().decode(Cat.self, from: jsonString.data(using: .utf8)!)
print(cat2)
SwiftProtocol.Cat
(lldb) po cat2.age
▿ Optional<Int>
- some : 18
(lldb) po cat2.height
▿ Optional<Double>
- some : 1.8
这样就能正常解析了,注释掉不必要的代码看下SIL
:
-
Cat
创建了自己的container
; -
bb1
中对height
进行了解析赋值; -
bb2
分支中调用了Hotpot
的init
方法。
编解码多态中的应用
protocol Hotpot: Codable {
var age: String { get set }
var name: String { get set }
}
struct Cat: Hotpot {
var age: String
var name: String
}
struct Dog: Hotpot {
var age: String
var name: String
}
struct Animal: Codable{
var animals: [Hotpot]
var desc: String
enum CodingKeys: String, CodingKey {
case animals
case desc
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(animals, forKey: .animals)
try container.encode(desc, forKey: .desc)
}
}
上面的例子中,Animal
中animals
存储的是遵循Hotpot
协议的属性:
看到直接报错,有自定义类型就需要我们自己实现
init
和decode
方法了。并且animals
不能解析,需要各自的类型(Cat
、Dog
)自己去实现相应的init
和decode
方法。这在类型比较少的时候还能接受,类型多了的情况下呢?那么可以用一个中间层
HotpotBox
去专门做解析:
protocol Hotpot {
var age: Int { get set }
var name: String { get set }
}
struct HotpotBox: Hotpot, Codable {
var age: Int
var name: String
init(_ hotpot: Hotpot) {
self.age = hotpot.age
self.name = hotpot.name
}
}
struct Cat: Hotpot {
var age: Int
var name: String
}
struct Dog: Hotpot {
var age: Int
var name: String
}
struct Animal: Codable{
var animals: [HotpotBox]
var desc: String
}
调用
let hotpots: [Hotpot] = [Cat(age: 18, name: "cat"),Dog(age: 28, name: "dog")]
//对hotpots数组中的集合执行HotpotBox.init,也就是用hotpots初始化animals
let animal = Animal(animals: hotpots.map(HotpotBox.init), desc: "Animal")
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
let jsonData = try jsonEncoder.encode(animal)
if let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
}
输出
{
"animals" : [
{
"age" : 18,
"name" : "cat"
},
{
"age" : 28,
"name" : "dog"
}
],
"desc" : "Animal"
}
再decode
一下:
let animal1: Animal = try JSONDecoder().decode(Animal.self, from: jsonData)
print(animal1)
输出:
Animal(animals: [SwiftProtocol.HotpotBox(age: 18, name: "cat"), SwiftProtocol.HotpotBox(age: 28, name: "dog")], desc: "Animal")
可以看到这里输出的是HotpotBox
,如果要还原当前的类型信息呢?
1.可以写一个对应的unBox
来还原数据:
struct Cat: Hotpot {
var age: Int
var name: String
static func unBox(_ value: HotpotBox) -> Cat {
var cat = Cat(age: value.age, name: value.name)
return cat
}
}
[SwiftProtocol.Cat(age: 18, name: "cat"), SwiftProtocol.Cat(age: 28, name: "dog")]
2.编码过程中将类型信息编码进去。
enum HotpotType: String, Codable {
case cat
case dog
var metadata: Hotpot.Type {
switch self {
case .cat:
return Cat.self
case .dog:
return Dog.self
}
}
}
protocol Hotpot: Codable {
static var type: HotpotType { get }//计算属性,编解码过程中不会被编码进去。
var age: Int { get set }
var name: String { get set }
}
struct HotpotBox: Codable {
var hotpot: Hotpot
init(_ hotpot: Hotpot) {
self.hotpot = hotpot
}
private enum CodingKeys: String, CodingKey {
case type
case hotpot
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(HotpotType.self, forKey: .type)
self.hotpot = try type.metadata.init(from: container.superDecoder(forKey: .hotpot))
}
func encode(to encoder: Encoder) throws {
var container = try encoder.container(keyedBy: CodingKeys.self)
try container.encode(type(of: hotpot).type, forKey: .type)
try hotpot.encode(to: container.superEncoder(forKey: .hotpot))
}
}
struct Cat: Hotpot {
static var type: HotpotType = .cat
var age: Int
var name: String
}
struct Dog: Hotpot {
static var type: HotpotType {
.dog
}
var age: Int
var name: String
}
struct Animal: Codable {
var animals: [HotpotBox]
var desc: String
}
let hotpots: [Hotpot] = [Cat(age: 18, name: "cat"),Dog(age: 28, name: "dog")]
//对hotpots数组中的集合执行HotpotBox.init,也就是用hotpots初始化animals
let animal = Animal(animals: hotpots.map(HotpotBox.init), desc: "Animal")
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
let jsonData = try jsonEncoder.encode(animal)
if let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
}
let animal1: Animal = try JSONDecoder().decode(Animal.self, from: jsonData)
print(animal1)
输出
{
"animals" : [
{
"type" : "cat",
"hotpot" : {
"age" : 18,
"name" : "cat"
}
},
{
"type" : "dog",
"hotpot" : {
"age" : 28,
"name" : "dog"
}
}
],
"desc" : "Animal"
}
Animal(animals: [SwiftProtocol.HotpotBox(hotpot: SwiftProtocol.Cat(age: 18, name: "cat")), SwiftProtocol.HotpotBox(hotpot: SwiftProtocol.Dog(age: 28, name: "dog"))], desc: "Animal")
这个时候看到有无用信息,再改造下:
self.hotpot = try type.metadata.init(from: container.superDecoder(forKey: .hotpot))
改为
self.hotpot = try type.metadata.init(from: decoder)
{
"animals" : [
{
"type" : "cat",
"age" : 18,
"name" : "cat"
},
{
"type" : "dog",
"age" : 28,
"name" : "dog"
}
],
"desc" : "Animal"
}
Animal(animals: [SwiftProtocol.HotpotBox(hotpot: SwiftProtocol.Cat(age: 18, name: "cat")), SwiftProtocol.HotpotBox(hotpot: SwiftProtocol.Dog(age: 28, name: "dog"))], desc: "Animal")
如果不想要type
func encode(to encoder: Encoder) throws {
var container = try encoder.container(keyedBy: CodingKeys.self)
// try container.encode(type(of: hotpot).type, forKey: .type)
try hotpot.encode(to: encoder)
}
去掉就好了,不过在解码的时候就没有type
相关信息了。需要根据需求灵活处理
{
"animals" : [
{
"age" : 18,
"name" : "cat"
},
{
"age" : 28,
"name" : "dog"
}
],
"desc" : "Animal"
}
另外一种方式
protocol Meta: Codable {
associatedtype Element
static func metatype(for typeString: String) -> Self
var type: Decodable.Type { get }
}
struct MetaObject<M: Meta>: Codable {
let object: M.Element
init(_ object: M.Element) {
self.object = object
}
enum CodingKeys: String, CodingKey {
case metatype
case object
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let typeStr = try container.decode(String.self, forKey: .metatype)
let metatype = M.metatype(for: typeStr)
let superDecoder = try container.superDecoder(forKey: .object)
let obj = try metatype.type.init(from: superDecoder)
guard let element = obj as? M.Element else {
fatalError()
}
self.object = element
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let typeStr = String(describing: type(of: object))
try container.encode(typeStr, forKey: .metatype)
let superEncoder = container.superEncoder(forKey: .object)
let encodable = object as? Encodable
try encodable?.encode(to: superEncoder)
}
}
enum HotpotType: String, Meta {
typealias Element = Hotpot
case cat = "Cat"
case dog = "Dog"
static func metatype(for typeString: String) -> HotpotType {
guard let metatype = self.init(rawValue: typeString) else {
fatalError()
}
return metatype
}
var type: Decodable.Type {
switch self {
case .cat:
return Cat.self
case .dog:
return Dog.self
}
}
}
class Hotpot: Codable {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
class Cat: Hotpot {
var height: Double
init(name: String, age: Int, height: Double) {
self.height = height
super.init(name: name, age: age)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
height = try container.decode(Double.self, forKey: .height)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(height, forKey: .height)
try super.encode(to: encoder)
}
enum CodingKeys: String, CodingKey {
case height
}
}
class Dog: Hotpot {
var weight: Double
init(name: String, age: Int, weight: Double) {
self.weight = weight
super.init(name: name, age: age)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
weight = try container.decode(Double.self, forKey: .weight)
let superDecoder = try container.superDecoder()
try super.init(from: superDecoder)
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(weight, forKey: .weight)
let superdecoder = container.superEncoder()
try super.encode(to: superdecoder)
}
enum CodingKeys: String, CodingKey {
case weight
}
}
let hotpot: Hotpot = Cat(name: "cat", age: 18, height: 1.8)
let jsonData = try JSONEncoder().encode(MetaObject<HotpotType>(hotpot))
if let str = String(data: jsonData, encoding: .utf8) {
print(str)
}
let decode: MetaObject<HotpotType> = try JSONDecoder().decode(MetaObject<HotpotType>.self, from: jsonData)
print(decode)
{"metatype":"Cat","object":{"name":"cat","age":18,"height":1.8}}
MetaObject<HotpotType>(object: SwiftProtocol.Cat)
- 中间层
- 记录metadata
- Type
在编解码过程中遇到多态的方式,可以通过中间层去解决处理。
SwiftUI中Codable
当我们在SwiftUI
中定义如下代码会直接报错(ObservableObject
和Published
可以实现组件数据一致性,而且可以自动更新):
class Hotpot: ObservableObject, Codable {
@Published var name = "cat"
}
这实际上是
Swift
的一项基本原则,我们不能说var names: Set
,因为Set
是通用类型,必须创建Set<String>
的实例。Published
也是同理,不能自己创建Published
的实例,而是创建一个Published<String>
的实例。这时候就需要我们自己编写编解码操作了:
class Hotpot: ObservableObject, Codable {
@Published var name = "cat"
enum CodingKeys: CodingKey {
case name
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
}
}
比较
Codable
:继承多态模式下需要自己编写编解码。
HandyJSON
:内存赋值的方式进行编解码操作,依赖metadata
。如果metadata
变化后会出现问题,迁移成本比较高。
SwiftyJSON
:需要使用下标的方式取值。
ObjectMapper
:需要手动对每一个属性提供映射关系。
如果项目中数据模型继承和多态比较少建议直接用Codable
,否则就用HandyJSON
吧。相对来说Codable
效率更高。
具体的对比以后再补充吧。
参考:
https://zhuanlan.zhihu.com/p/50043306
https://www.jianshu.com/p/f4b3dce8bd6f
https://www.jianshu.com/p/1f194f09599a
https://www.jianshu.com/p/bdd9c012df15
https://www.jianshu.com/p/6db40c4c0ff9
https://www.jianshu.com/p/5dab5664a621?utm_campaign
https://www.jianshu.com/p/bf56a74323fa
https://www.jianshu.com/p/0088b62f698a
enum解析
解析框架对比
Double精度问题
NSDecimalNumber