数据序列化框架在 Swift 日常开发中的应用

image

到了 Swift 年代,第三方库 SwiftyJSON 和 ObjectMapper 都曾经作为 JSON 转换的中流砥柱,只是这两者还是免不了“手动指定字段和JSON字典映射关系”的工作。于是阿里想了个黑科技(HandyJSON),通过分析Swift数据结构在内存中的布局,自动分析出映射关系,进一步降低开发者使用的成本。

如今我们就有多个选择:ObjectMapper、HandyJSON、SwiftyJSON、MJExtension 等

其实我们在日常开发中,对于 JSON 数据的处理有两大需求:

  1. json 和 model 互相转换(Android Studio有 Gson format 插件,但Xcode没有类似功能)
  2. 服务端返回的 json 里可能有 null,但是 Swift 语言的空是用 nil 表示,需要空值处理(对象 Optional 类型)

框架简介

ObjectMapper

先看 ObjectMapper : Model 类必须实现 Mappable 协议,即实现 init 和 mapping 函数;适合跟 Alamofire 配合。但是 mapping 函数实现起来过于臃肿耗时,只能借助插件来快速完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import ObjectMapper

class PersonOBM: Mappable {
var username: String?
var age: Int?
var weight: Double!
var sex: Bool!
var location: String?
var three_day_forecast: [ForecastOBM]?

required init?(map: Map) {

}

func mapping(map: Map) {
username <- map["username"]
age <- map["age"]
weight <- map["weight"]
sex <- map["sex"]
location <- map["location"]
three_day_forecast <- map ["three_day_forecast"]
}
}

class ForecastOBM: Mappable {
var conditions: String?
var day: String?
var temperature: Double!

required init?(map: Map) {

}

func mapping(map: Map) {
conditions <- map["conditions"]
day <- map["day"]
temperature <- map["temperature"]
}
}

j2s 是一个 macOS app 能够将 JSON 对象转成 Swift 结构体

HandyJSON

再看 HandyJSON, 写起来比较方便,类和结构体要求继承于 HandyJSON、枚举要继承于 HandyJSONEnum。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import HandyJSON

class PersonHJ: HandyJSON {
var username: String?
var age: Int?
var weight: Double!
var sex: Bool!
var locatoin: String?
var three_day_forecast: [ForecastHJ]?

required init() {

}
}

class ForecastHJ: HandyJSON {
var conditions: String?
var day: String?
var temperature: Double!

required init() {

}
}

比ObjectMapper使用上要简单, 不用写mapping函数那么多代码了。

SwiftyJSON

SwiftyJSON:取字段值使用比较方便, 但是然并卵? SwiftyJSON 不支持转 Model,如果你只是想要解析某几个字段,那么 SwiftyJSON 是不二选择, 而且适用于 Alamofire。

1
2
3
4
5
6
7
8
9
let jsonString: String = "{\"username\":\"yuhanle\",\"age\":18,\"weight\":65.4,\"sex\":1,\"location\":\"Toronto, Canada\",\"three_day_forecast\":[{\"conditions\":\"Partly cloudy\",\"day\":\"Monday\",\"temperature\":20},{\"conditions\":\"Showers\",\"day\":\"Tuesday\",\"temperature\":22},{\"conditions\":\"Sunny\",\"day\":\"Wednesday\",\"temperature\":28}]}"

let dataFromString = jsonString.data(using: .utf8, allowLossyConversion: false)
do {
let json = try JSON(data: dataFromString!)
print(json["username"], json["weight"], json["three_day_forecast"][0]["conditions"])
} catch let error as NSError {
print ("Error: \(error.domain)")
}

MJExtension

最后看下一下 MJExtension,作为一个从 ObjC 年代就开始流程的转换框架,在如今使用的人仍然很多,但是对于 Swift 的集成却不是特别友好,官方 issue 列表中经常都会有申请支持 swift 的呼声!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import MJExtension

class PersonMJ: NSObject {
@objc var username: String?
@objc var age = 0
@objc var weight = 0.0
@objc var sex = false
@objc var location: String?
@objc var three_day_forecast: [ForecastMJ]?
}

class ForecastMJ: NSObject {
@objc var conditions: String?
@objc var day: String?
@objc var temperature = 0.0
}

尽管在支持上不是特别友好,但是在自定义 Model 的过程中,应该是最轻松的一款,但是在升级 Swift 4 之后,需要在属性前添加 @objc 才可以正常使用,否则转换失败。具体情况可参考:Swift 4 字典转模型失败

另外还有一种情况,就是关于整型属性,需要给定初试值,也就是说,MJExtension 无法序列化/反序列化整型。解决办法很简单, 就是赋个默认值, 即将Optional整型变为整型就可以。

1
2
3
4
5
class People: NSObject {
var name: String?
var age: Int? // 请注意:MJExtension不能解析Optional Int类型
var age = 0 // 正确
}

运行耗时

我们准备了一小段 JSON 数据,循环解析 10000 次,来分析几大框架的运行耗时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"username": "yuhanle",
"age": 18,
"weight": 65.4,
"sex": 1,
"location": "Toronto, Canada",
"three_day_forecast": [
{
"conditions": "Partly cloudy",
"day": "Monday",
"temperature": 20
},
{
"conditions": "Showers",
"day": "Tuesday",
"temperature": 22
},
{
"conditions": "Sunny",
"day": "Wednesday",
"temperature": 28
}
]
}

以 HandyJSON 为例,在开始处理数据和结束时,记录时间差,时间差的结果每次会有波动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let maxCount = 10000
func testHandyJSON(json: String) -> Void {
var start = CFAbsoluteTimeGetCurrent()

var people: PersonHJ = PersonHJ()
for _ in 0..<maxCount {
people = PersonHJ.deserialize(from: json)!
}

var executionTime = CFAbsoluteTimeGetCurrent() - start
print("HandyJSON deserialize time totals: ", executionTime)

start = CFAbsoluteTimeGetCurrent()

var res = ""
for _ in 0..<maxCount {
res = people.toJSONString()!
}

executionTime = CFAbsoluteTimeGetCurrent() - start
print(res)
print("HandyJSON toJSONString time totals: ", executionTime)
}

最终记录得到的结果对比图

测试项 JSON -> MODEL MODEL -> JSON
HandyJSON 3.0839329957962 4.97446703910828
ObjeceMapper 1.40153098106384 1.2123589515686
SwiftJSON 不支持 不支持
MJExtension 0.417935013771057 0.418874025344849

image

结果有点出乎意料,HandyJSON 的黑魔法纵然很强大,这也导致了耗时的问题,相比较而言,不太友好的 MJExtension 速度最快。

总结

对于应用开发来说,JSON 数据序列化和反序列号的操作必不可少,上述分析的效率和性能问题,也应该多考虑,选择合适的框架很重要,学习和踩坑也是并存的。

另外,Swift 支持 Codable 协议,对这个需求的处理也有很大的支持!

参考链接

请我喝杯咖啡吧~