携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第0天,点击查看活动详情
图片来自:unsplash.com/photos/f...
本文作者:无帆
业界常用的几种方案
手动解码方案,如 Unbox(DEPRECATED)
Swift 早期普遍采用的方案,类似的还有 ObjectMapper
该方案需要使用者手动编写解码逻辑,使用成本比较高;目前已被 Swift 官方推出的 Codable 取代
示例:
struct User {
let name: String
let age: Int
}
extension User: Unboxable {
init(unboxer: Unboxer) throws {
self.name = try unboxer.unbox(key: "name")
self.age = try unboxer.unbox(key: "age")
}
}extension User: Unboxable {
init(unboxer: Unboxer) throws {
self.name = try unboxer.unbox(key: "name")
self.age = try unboxer.unbox(key: "age")
}
}
阿里开源的 HandyJSON
HandyJSON 目前依赖于从 Swift Runtime 源码中推断的内存规则,直接对内存进行操作。
在使用方面,不需要繁杂的定义,不需要继承自 NSObject,声明实现了协议即可
示例:
class Model: HandyJSON {
var userId: String = ""
var nickname: String = ""
required init() {}
}
let jsonObject: [String: Any] = [
"userId": "1234",
"nickname": "lilei",
]
let model = Model.deserialize(from: object)let model = Model.deserialize(from: object)
但是存在兼容和安全方面的问题,由于强依赖内存布局规则,Swift 大版本升级时可能会有稳定性问题。同时由于要在运行时通过反射解析数据结构,会对性能有一定影响
基于 Sourcery 的元编程方案
Sourcery 是一款 Swift 代码生成器,使用 SourceKitten 解析 Swift 源码,根据 Stencil 模版生成最终代码
可定制能力非常强,基本可以满足我们所有的需求
示例:
定义了 AutoCodable 协议,并且让需要被解析的数据类型遵循该协议
protocol AutoCodable: Codable {}
class Model: AutoCodable {
// sourcery: key = "userID"
var userId: String = ""
var nickname: String = ""
required init(from decoder: Decoder) throws {
try autoDecodeModel(from: decoder)
}
}}
之后通过 Sourcery 生成代码,这个过程 Sourcery 会扫描所有代码,对实现了 AutoCodable 协议的类/结构体自动生成解析代码
// AutoCodable.generated.swift
// MARK: - Model Codable
extension Model {
enum CodingKeys: String, CodingKey {
case userId = "userID"
case nickname
}
// sourcery:inline:Model.AutoCodable
public func autoDecodeModel(from decoder: Decoder) throws {
// ...
}
}}
如上所示,还可以通过代码注释(注解)来实现键值映射等自定义功能,但是需要对使用者有较强的规范要求。其次在组件化过程中需要对每个组件进行侵入/改造,内部团队可以通过工具链解决,作为跨团队通用方案可能不是太合适
Swift build-in API Codable
Swift 4.0 之后官方推出的 JSON 序列化方案,可以理解为 Unbox+Sourcery 的组合,编译器会根据数据结构定义,自动生成编解码逻辑,开发者使用特定的 Decoder/Encoder 对数据进行转化处理。
Codable 作为 Swift 官方推出的方案,使用者可以无成本的接入。不过在具体实践过程中,碰到了一些问题
- Key 值映射不友好,例如以下情况:
// swift
struct User: Codable {
var name: String
var age: Int
// ...
}
// json1
{
"name": "lilei"
}
// json2
{
"nickname": "lilei"
}
// json3
{
"nickName": "lilei"
}// json3
{
"nickName": "lilei"
}
Swift 编译器会自动帮我们生成完整的 CodingKeys,但是如果需要将 json 中的 nickname 或 nickName 解析为 User.name 时,需要重写整个 CodingKeys,包括其他无关属性如 age
容错处理能力不足、无法提供默认值
Swift 设计初衷之一就是安全性,所以对于一些类型的强校验从设计角度是合理的,不过对于实际使用者来说会增加一些使用成本
举个例子:
enum City: String, Codable {
case beijing
case shanghai
case hangzhou
}
struct User: Codable {
var name: String
var city: City?
}
// json1
{
"name": "lilei",
"city": "hangzhou"
}
// json2
{
"name": "lilei"
}
// json3
{
"name": "lilei",
"city": "shenzhen"
}
let decoder = JSONDecoder()
try {
let user = try? decoder.decode(User.self, data: jsonData3)
}
catch {
// json3 格式会进入该分支
print("decode user error")
}
try {
let user = try? decoder.decode(User.self, data: jsonData3)
}
catch {
// json3 格式会进入该分支
print("decode user error")
}
上述代码中,json1 和 json2 可以正确反序列化成 User 结构,json3 由于 “shenzhen” 无法转化成 City,导致整个 User 结构解析失败,而不是 name 解析成功,city 失败后变成 nil
- 嵌套结构解析繁琐
- JSONDecoder 只接受 data,不支持 dict,特殊场景使用时的类型转化存在性能损耗
属性装饰器,如 BetterCodable
Swift 5.0 新增的语言特性,通过该方案可以补足原生 Codable 方案一些补足之处,比如支持默认值、自定义解析兜底策略等,具体原理也比较简单,有兴趣的可自行了解
示例:
struct UserPrivilege: Codable {
@DefaultFalse var isAdmin: Bool
}
let json = #"{ "isAdmin": null }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)
print(result) // UserPrivilege(isAdmin: false)print(result) // UserPrivilege(isAdmin: false)
不过在实际编码中,需要对数据结构的属性显式描述,增加了使用成本
各个方案优缺点对比
| Codable | HandyJSON | BetterCodable | Sourcery | |
|---|---|---|---|---|
| 类型兼容 | ❌ | ✅ | ✅ | ✅ |
| 支持默认值 | ❌ | ✅ | ✅ | ✅ |
| 键值映射 | ❌ | ✅ | ❌ | ✅ |
| 接入/使用成本 | ✅ | ✅ | ❌ | ❌ |
| 安全性 | ✅ | ❌ | ✅ | ✅ |
| 性能 | ✅ | ❌ | ✅ | ✅ |
上述方案都有各自的优缺点,基于此我们希望找到更适合云音乐的方案。从使用接入和使用成本上来说,Codable 无疑是最佳选择,关键点在于如何解决存在的问题
Codable 介绍
原理浅析
先看一组数据结构定义,该数据结构遵循 Codable 协议
enum Gender: Int, Codable {
case unknown
case male
case female
}
struct User: Codable {
var name: String
var age: Int
var gender: Gender
}struct User: Codable {
var name: String
var age: Int
var gender: Gender
}
使用命令 swiftc main.swift -emit-sil | xcrun swift-demangle > main.sil 生成 SIL(Swift Intermediate Language