本文记录了我们团队将一个 10 万行 Swift 项目从 HandyJSON 迁移到 SmartCodable 的完整过程,包括迁移动机、踩过的坑、API 对照表,以及迁移后的效果对比。如果你的项目还在用 HandyJSON,希望这篇文章能帮你做出判断。
一、为什么要迁移
HandyJSON 的定时炸弹
HandyJSON 是国内 iOS 社区广泛使用的 JSON 解析库。它的优点很明显------API 简洁,支持 Any 类型,支持继承,几乎不需要额外的模板代码。我们团队用了两年多,一直没出什么问题。
直到 Swift 5.5 引入结构化并发之后,问题开始浮现。
HandyJSON 的核心实现依赖 Swift 运行时的内存布局反射------直接读取 struct/class 的内存 metadata,计算属性偏移量,然后写入值。这个机制有两个致命问题:
- 不是官方支持的 API 。Swift 的内存布局在不同版本之间没有 ABI 稳定性承诺。Apple 每次更新 Swift 版本,都有可能改变 metadata 的结构,导致 HandyJSON 静默地写错内存位置。这不会崩溃,而是静默返回错误的数据------更危险。
- 与 Swift 并发模型冲突。Swift 5.5+ 的并发检查越来越严格,HandyJSON 的运行时反射无法被标记为 Sendable,在启用严格并发检查的项目中会产生大量警告。
我们在一次 Xcode 15 升级后遇到了一个诡异的 Bug:某个嵌套模型的属性偶尔解析为零值。排查了两天才发现是 HandyJSON 的内存偏移计算在新版 Swift 编译器下出了问题。这次事件让我们决定迁移。
为什么选择 SmartCodable
我们评估了三个方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
手写 init(from:) |
零依赖,完全可控 | 样板代码爆炸,100 个 Model 就是地狱 |
| CodableWrapper / BetterCodable | 轻量,只用属性包装器 | 只解决默认值问题,不解决类型转换、Any 支持 |
| SmartCodable | 功能对齐 HandyJSON,基于原生 Codable | 学习成本低,API 设计与 HandyJSON 相似 |
SmartCodable 胜出的原因很简单:它是唯一一个在功能上能完全替代 HandyJSON 的方案,同时又基于 Apple 原生 Codable 协议,没有运行时安全隐患。
二、迁移前的准备
评估工作量
我们先做了一次全局搜索,统计 HandyJSON 的使用范围:
bash
# 统计引用 HandyJSON 的文件数
grep -rl "HandyJSON" --include="*.swift" . | wc -l
# 统计 deserialize 调用次数
grep -rn "deserialize(from:" --include="*.swift" . | wc -l
# 统计 mapping 方法使用次数
grep -rn "mapping(mapper:" --include="*.swift" . | wc -l
# 统计 Any 类型属性
grep -rn "var.*: Any" --include="*.swift" . | wc -l
我们的项目情况:
- 约 200 个 Model 文件
- 80+ 处
deserialize调用 - 15 处
mapping(mapper:)自定义映射 - 8 处
Any类型属性 - 3 处继承关系
制定迁移策略
根据评估结果,我们制定了分步迁移策略:
- 第一步:全局替换协议名(工作量最大但最简单)
- 第二步 :处理
mapping方法(需要逐个改写) - 第三步 :处理
Any类型属性(加@SmartAny) - 第四步 :处理继承关系(加
@SmartSubclass) - 第五步 :处理枚举(
HandyJSONEnum→SmartCaseDefaultable) - 第六步 :处理序列化(
toJSON()→toDictionary()) - 第七步:全量测试
三、逐步迁移
第一步:替换协议名(5 分钟)
这是最简单的一步,全局搜索替换即可:
arduino
import HandyJSON → import SmartCodable
HandyJSON → SmartCodable (作为协议名使用的地方)
SmartCodable 的 deserialize(from:) API 与 HandyJSON 完全一致,所以替换协议名后,所有反序列化代码不需要改动。
csharp
// HandyJSON(替换前)
guard let model = Model.deserialize(from: dict) else { return }
// SmartCodable(替换后)------ 调用方式完全一样
guard let model = Model.deserialize(from: dict) else { return }
唯一的小差异 :HandyJSON 解码数组时返回 [Model]?,有些地方写了 as? [Model] 强转。SmartCodable 不需要这个强转,但保留也不会报错,可以后续清理。
csharp
// HandyJSON 写法
guard let models = [Model].deserialize(from: arr) as? [Model] else { return }
// SmartCodable 写法(as? [Model] 可以删掉,不删也没问题)
guard let models = [Model].deserialize(from: arr) else { return }
第二步:改写自定义映射(30 分钟)
这是工作量最大的一步。HandyJSON 用 mapping(mapper:) 方法,SmartCodable 用 mappingForKey(),语法不同:
HandyJSON:
swift
struct Model: HandyJSON {
var nickName: String = ""
var userAge: Int = 0
var ignoreField: String = ""
mutating func mapping(mapper: HelpingMapper) {
mapper <<< self.nickName <-- ["nick_name", "realName"]
mapper <<< self.userAge <-- "user_age"
mapper >>> self.ignoreField // 忽略该字段
}
}
SmartCodable:
swift
struct Model: SmartCodable {
var nickName: String = ""
var userAge: Int = 0
@SmartIgnored
var ignoreField: String = ""
static func mappingForKey() -> [SmartKeyTransformer]? {
[
CodingKeys.nickName <--- ["nick_name", "realName"],
CodingKeys.userAge <--- "user_age"
]
}
}
对照表:
| HandyJSON | SmartCodable | 说明 |
|---|---|---|
mapper <<< self.prop <-- "key" |
CodingKeys.prop <--- "key" |
单字段映射 |
mapper <<< self.prop <-- ["k1", "k2"] |
CodingKeys.prop <--- ["k1", "k2"] |
多候选映射 |
mapper >>> self.prop |
@SmartIgnored var prop |
忽略字段 |
踩坑提醒 :SmartCodable 的 mappingForKey() 是 static func,不是 mutating func。如果你的 mapping 中有依赖 self 的逻辑,需要调整。
第三步:处理 Any 类型(10 分钟)
HandyJSON 天然支持 Any 类型,SmartCodable 需要加 @SmartAny 属性包装器:
less
// HandyJSON
struct Model: HandyJSON {
var extra: [String: Any] = [:]
var tags: [Any] = []
var value: Any?
}
// SmartCodable
struct Model: SmartCodable {
@SmartAny var extra: [String: Any] = [:]
@SmartAny var tags: [Any] = []
@SmartAny var value: Any?
}
全局搜索 var.*: Any、var.*: [Any]、var.*: [String: Any],逐个加上 @SmartAny 即可。
第四步:处理继承(5 分钟)
HandyJSON 自动处理继承,SmartCodable 需要在子类上加 @SmartSubclass:
kotlin
// HandyJSON ------ 什么都不用加
class BaseModel: HandyJSON {
var name: String = ""
required init() {}
}
class SubModel: BaseModel {
var age: Int = 0
}
// SmartCodable ------ 子类加 @SmartSubclass
class BaseModel: SmartCodable {
var name: String = ""
required init() {}
}
@SmartSubclass
class SubModel: BaseModel {
var age: Int = 0
}
注意 :
@SmartSubclass是 Swift 宏,需要 Swift 5.9+ 和 Xcode 15+。如果你的项目还在用低版本,可以参考 低版本继承方案。
第五步:处理枚举(5 分钟)
arduino
// HandyJSON
enum Sex: String, HandyJSONEnum {
case man
case woman
}
// SmartCodable
enum Sex: String, SmartCaseDefaultable {
case man
case woman
}
全局替换 HandyJSONEnum → SmartCaseDefaultable。
第六步:处理序列化(10 分钟)
序列化的 API 名称有变化:
| HandyJSON | SmartCodable |
|---|---|
model.toJSON() |
model.toDictionary() |
model.toJSONString() |
model.toJSONString() |
models.toJSON() |
models.toArray() |
models.toJSONString() |
models.toJSONString() |
全局搜索 .toJSON() 替换为 .toDictionary()(注意排除 toJSONString)。数组序列化搜索替换即可。
第七步:全量测试
移除 HandyJSON 依赖,编译通过后进行全量测试。
我们的测试策略:
- 先开启 SmartSentinel 日志,跑一遍主流程:
ini
SmartSentinel.debugMode = .verbose
- 观察日志中是否有异常的类型转换或缺失字段
- 重点验证有
mapping的模型、有Any类型的模型、有继承的模型 - 回归测试核心业务流程
四、迁移后的收获
解析异常不再是黑盒
HandyJSON 解析失败时,你只知道"解析返回了 nil",但不知道哪个字段出了问题。SmartCodable 的 SmartSentinel 日志系统会精确告诉你:
vbnet
================================ [Smart Sentinel] ================================
UserModel 👈🏻 👀
╆━ UserModel
┆┄ age : Expected Int, got String --- auto-converted
┆┄ email : Key not found --- using default ""
====================================================================================
我们在迁移后第一周就通过 Sentinel 日志发现了 3 个后端接口返回类型不一致的问题------这些问题在 HandyJSON 时代被静默吞掉了。
告别运行时崩溃的恐惧
HandyJSON 的每次 Swift 版本升级都是一次赌博。SmartCodable 基于原生 Codable,Swift 版本升级时完全不需要担心底层兼容性。
类型转换更智能
SmartCodable 内置的类型转换比 HandyJSON 更全面:
ini
// 后端返回 String 类型的 "123",Model 声明为 Int
var age: Int = 0
// HandyJSON: age = 0(转换失败,静默用默认值)
// SmartCodable: age = 123(自动转换成功)
编译速度
移除 HandyJSON 后,项目的 clean build 时间减少了约 8%(HandyJSON 的运行时反射代码量较大)。
五、迁移清单(Checklist)
供你在实际迁移时对照使用:
- 全局替换
import HandyJSON→import SmartCodable - 全局替换协议名
HandyJSON→SmartCodable(注意只替换作为协议使用的) - 改写所有
mapping(mapper:)→mappingForKey()+@SmartIgnored - 所有
Any/[Any]/[String: Any]属性加@SmartAny - 所有子类加
@SmartSubclass - 全局替换
HandyJSONEnum→SmartCaseDefaultable - 全局替换
.toJSON()→.toDictionary()(排除toJSONString) - 数组序列化
.toJSON()→.toArray() - 移除 HandyJSON 依赖(Podfile / Package.swift)
- 编译通过
- 开启
SmartSentinel.debugMode = .verbose,跑主流程 - 全量回归测试
- 关闭 Sentinel(
SmartSentinel.debugMode = .none) - 上线观察
六、总结
整个迁移过程比我们预想的顺利很多。200 个 Model 的项目,两个人花了一天半 完成迁移和测试。其中 80% 的工作量是全局替换(第一步),真正需要手动处理的只有 mapping 改写和 @SmartAny 标注。
如果你的项目还在用 HandyJSON,我的建议是:不要等到被 Swift 版本升级逼着迁移,主动迁移的成本远低于被动修 Bug。
SmartCodable 的 API 设计明显考虑了 HandyJSON 用户的迁移体验------deserialize、didFinishMapping、designatedPath 这些核心 API 完全一致,迁移门槛很低。