Swift 中的 JSON 反序列化

220 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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 中的 nicknamenickName 解析为 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)

不过在实际编码中,需要对数据结构的属性显式描述,增加了使用成本

各个方案优缺点对比

CodableHandyJSONBetterCodableSourcery
类型兼容
支持默认值
键值映射
接入/使用成本
安全性
性能

上述方案都有各自的优缺点,基于此我们希望找到更适合云音乐的方案。从使用接入和使用成本上来说,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