[关闭]
@pockry 2016-11-16T15:01:30.000000Z 字数 10728 阅读 3170

HandyJSON:Swift中的JSON转Model工具库

移动


背景

JSON是移动端开发常用的应用层数据交换协议。最常见的场景便是,客户端向服务端发起网络请求,服务端返回JSON文本,然后客户端解析这个JSON文本,再把对应数据展现到页面上。

但在编程的时候,处理JSON是一件麻烦事。在不引入任何轮子的情况下,我们通常需要先把JSON转为Dictionary,然后还要记住每个数据对应的Key,用这个Key在Dictionary中取出对应的Value来使用。这个过程我们会犯各种错误:

为了解决这些问题,很多处理JSON的开源库应运而生。在Swift中,这些开源库主要朝着两个方向努力:

对于1,使用最广、评价最好的库非 SwiftyJSON 莫属,它很能代表这个方向的核心。它本质上仍然是根据JSON结构去取值,使用起来顺手、清晰。但也正因如此,这种做法没能妥善解决上述的几个问题,因为Key、路径、类型仍然需要开发者去指定;

对于2,我个人觉得这是更合理的方式。由于Model类的存在,JSON的解析和使用都受到了定义的约束,只要客户端和服务端约定好了这个Model类,客户端定义后,在业务中使用数据时就可以享受到语法检查、属性预览、属性补全等好处,而且一旦数据定义变更,编译器会强制所有用到的地方都改过来才能编译通过,非常安全。这个方向上,开源库们做的工作,主要就是把JSON文本反序列化到Model类上了。这一类JSON库有 ObjectMapper、JSONNeverDie、HandyJSON 等。而 HandyJSON 是其中使用最舒服的一个库,本文将介绍用 HandyJSON 来进行Model和JSON间的互相转换。

为什么用HandyJSON

在Swift中把JSON反序列化到Model类,在HandyJSON出现以前,主要使用两种方式:

这两者都有显而易见的缺点。前者要求Model继承自NSObject,非常不优雅,且直接否定了用struct来定义Model的方式;后者的Mapping函数要求开发者自定义,在其中指明每个属性对应的JSON字段名,代码侵入大,且仍然容易发生拼写错误、维护困难等问题。

而HandyJSON另辟蹊径,采用Swift反射+内存赋值的方式来构造Model实例,规避了上述两个方案遇到的问题。

把JSON转换为Model

简单类型

某个Model类想支持通过HandyJSON来反序列化,只需要在定义时,实现HandyJSON协议,这个协议只要求实现一个空的`init()函数。

比如我们和服务端约定了一个Animal数据,里面有name/id/num字段,那么我们这样定义Animal类:

  1. class Animal: HandyJSON {
  2. var name: String?
  3. var id: String?
  4. var num: Int?
  5. required init() {}
  6. }

然后假设我们从服务端拿到这样一个JSON文本:

  1. let jsonString = "{\"name\":\"cat\",\"id\":\"12345\",\"num\":180}"

引入HandyJSON以后,我们就可以这样来做反序列化了:

  1. if let animal = JSONDeserializer<Animal>.deserializeFrom(json: jsonString) {
  2. print(animal.name)
  3. print(animal.id)
  4. print(animal.num)
  5. }

简单吧~

支持Struct

如果Model的定义是struct,由于Swift中struct提供了默认构造函数,所以就不需要再实现空的init()函数了。但需要注意,如果你为strcut指定了别的构造函数,那么就需要保留一个空的实现。

  1. struct Animal: HandyJSON {
  2. var name: String?
  3. var id: String?
  4. var num: Int?
  5. }
  6. let jsonString = "{\"name\":\"cat\",\"id\":\"12345\",\"num\":180}"
  7. if let animal = JSONDeserializer<Animal>.deserializeFrom(json: jsonString) {
  8. print(animal)
  9. }

比较复杂的类型

HandyJSON支持在类定义里使用各种形式的基本属性,包括可选(?),隐式解包可选(!),数组(Array),字典(Dictionary),Objective-C基本类型(NSString、NSNumber),各种类型的嵌套([Int]?、[String]?、[Int]!、...)等等。比如下面这个看起来比较复杂的类型:

  1. class Cat: HandyJSON {
  2. var id: Int64!
  3. var name: String!
  4. var friend: [String]?
  5. var weight: Double?
  6. var alive: Bool = true
  7. var color: NSString?
  8. required init() {}
  9. }

一样轻松转换:

  1. let jsonString = "{\"id\":1234567,\"name\":\"Kitty\",\"friend\":[\"Tom\",\"Jack\",\"Lily\",\"Black\"],\"weight\":15.34,\"alive\":false,\"color\":\"white\"}"
  2. if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString) {
  3. print(cat.xxx)
  4. }

嵌套的Model类

如果Model类中的某个属性是另一个自定义的Model类,那么只要那个Model类也实现了HandyJSON协议,就一样可以转换:

  1. class Component: HandyJSON {
  2. var aInt: Int?
  3. var aString: String?
  4. required init() {}
  5. }
  6. class Composition: HandyJSON {
  7. var aInt: Int?
  8. var comp1: Component?
  9. var comp2: Component?
  10. required init() {}
  11. }
  12. let jsonString = "{\"num\":12345,\"comp1\":{\"aInt\":1,\"aString\":\"aaaaa\"},\"comp2\":{\"aInt\":2,\"aString\":\"bbbbb\"}}"
  13. if let composition = JSONDeserializer<Composition>.deserializeFrom(json: jsonString) {
  14. print(composition)
  15. }

指定反序列化JSON中某个节点

有时候服务端返回给我们的JSON文本包含了大量的状态信息,和Model无关,比如statusCode,debugMessage等,或者有用的数据是在某个节点以下,那么我们可以指定反序列化哪个节点:

  1. class Cat: HandyJSON {
  2. var id: Int64!
  3. var name: String!
  4. required init() {}
  5. }
  6. // 服务端返回了这个JSON,我们想解析的只有data里的cat
  7. let jsonString = "{\"code\":200,\"msg\":\"success\",\"data\":{\"cat\":{\"id\":12345,\"name\":\"Kitty\"}}}"
  8. // 那么,我们指定解析 "data.cat",通过点来表达路径
  9. if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString, designatedPath: "data.cat") {
  10. print(cat.name)
  11. }

有继承关系的Model类

如果某个Model类继承自另一个Model类,只需要这个父Model类实现HandyJSON协议就可以:

  1. class Animal: HandyJSON {
  2. var id: Int?
  3. var color: String?
  4. required init() {}
  5. }
  6. class Cat: Animal {
  7. var name: String?
  8. required init() {}
  9. }
  10. let jsonString = "{\"id\":12345,\"color\":\"black\",\"name\":\"cat\"}"
  11. if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString) {
  12. print(cat)
  13. }

自定义解析方式

HandyJSON还提供了一个扩展能力,就是允许自行定义Model类某个字段的解析Key、解析方式。我们经常会有这样的需求:

HandyJSON协议提供了一个可选的mapping()函数,我们可以在其中指定某个字段用什么Key、或者用什么方法从JSON中解析出它的值。如我们有一个Model类和一个服务端返回的JSON串:

  1. class Cat: HandyJSON {
  2. var id: Int64!
  3. var name: String!
  4. var parent: (String, String)?
  5. required init() {}
  6. }
  7. let jsonString = "{\"cat_id\":12345,\"name\":\"Kitty\",\"parent\":\"Tom/Lily\"}"

可以看到,Cat类的id属性和JSON文本中的Key是对应不上的;而对于parent这个属性来说,它是一个元组,做不到从JSON中的"Tom/Lily"解析出来。所以我们要定义一个Mapping函数来做这两个支持:

  1. class Cat: HandyJSON {
  2. var id: Int64!
  3. var name: String!
  4. var parent: (String, String)?
  5. required init() {}
  6. func mapping(mapper: HelpingMapper) {
  7. // 指定 id 字段用 "cat_id" 去解析
  8. mapper.specify(property: &id, name: "cat_id")
  9. // 指定 parent 字段用这个方法去解析
  10. mapper.specify(property: &parent) { (rawString) -> (String, String) in
  11. let parentNames = rawString.characters.split{$0 == "/"}.map(String.init)
  12. return (parentNames[0], parentNames[1])
  13. }
  14. }
  15. }

就这样,HandyJSON完美地帮我们进行了JSON到Model类的转换。真是太Handy了。

把Model转换为JSON文本

HandyJSON还提供了把Model类序列化为JSON文本的能力,简直无情。

基本类型

如果只需要进行序列化,那么在定义Model类时,不需要做任何特殊的改动。任何一个类的实例,直接调用HandyJSON的序列化方法去序列化,就能得到JSON字符串了。

  1. class Animal {
  2. var name: String?
  3. var height: Int?
  4. init(name: String, height: Int) {
  5. self.name = name
  6. self.height = height
  7. }
  8. }
  9. let cat = Animal(name: "cat", height: 30)
  10. // 序列化为简单JSON文本
  11. if let jsonStr = JSONSerializer.serialize(model: cat).toJSON() {
  12. print("simple json string: ", jsonStr)
  13. }
  14. // 序列化为格式化的JSON文本
  15. if let prettifyJSON = JSONSerializer.serialize(model: cat).toPrettifyJSON() {
  16. print("prettify json string: ", prettifyJSON)
  17. }
  18. // 序列化为简单字典
  19. if let dict = JSONSerializer.serialize(model: cat).toSimpleDictionary() {
  20. print("dictionary: ", dict)
  21. }

复杂类型

即使Model类中有别的Model类啥的,都一样支持。

  1. enum Gender {
  2. case Male
  3. case Female
  4. }
  5. struct Subject {
  6. var id: Int64?
  7. var name: String?
  8. init(id: Int64, name: String) {
  9. self.id = id
  10. self.name = name
  11. }
  12. }
  13. class Student {
  14. var name: String?
  15. var gender: Gender?
  16. var subjects: [Subject]?
  17. }
  18. let student = Student()
  19. student.name = "Jack"
  20. student.gender = .Female
  21. student.subjects = [Subject(id: 1, name: "math"), Subject(id: 2, name: "English"), Subject(id: 3, name: "Philosophy")]
  22. if let jsonStr = JSONSerializer.serialize(model: student).toJSON() {
  23. print("simple json string: ", jsonStr)
  24. }
  25. if let prettifyJSON = JSONSerializer.serialize(model: student).toPrettifyJSON() {
  26. print("prettify json string: ", prettifyJSON)
  27. }
  28. if let dict = JSONSerializer.serialize(model: student).toSimpleDictionary() {
  29. print("dictionary: ", dict)
  30. }

总结

有了HandyJSON的支持,现在我们可以开心地在Swift中使用JSON了。这个库支持了Swift 2.2+, Swift 3.0+。如果大家有什么需求或者建议,可以去 https://github.com/alibaba/handyjson 提issue.

为何开发HandyJSON

我所在iOS团队是从去年11月份切Swift的。我们服务端和客户端数据交互格式一直用的是JSON,而当时Swift中处理JSON名气比较大的库貌似只有SwiftyJSON,工程切到Swift后,我们也用了这个库。用上之后,需求是满足了,但是对一些复杂的Model,代码写得看起来非常糟糕,因为每次取值都需要 json["akey"]["bkey"]["ckey"].value 形式,写的时候对着文档没觉得啥问题,但过后在脱离文档的情况下,通篇都是字符串表达的key,很难从代码中感觉出Model结构。所以我们都会把一段sample数据写在注释里。但仍然比较凌乱,另外key写错了debug起来也费劲,一个大小写问题有时候debug半天。

于是我们进化了一下,先写好Model,然后Model类中写convert函数,也用上了KVC遍历key赋值。写起来舒服多了,但还是麻烦,而且要求每一个类都继承自NSObject。不久后,我们认识了ObjectMapper库,二话不说,就换了上去。世界顿时干净多了。

但还是感觉差了一点,因为ObjectMapper需要自己指明映射关系。通常JSON中key和Model中字段名都是一致的,每次都要额外写一坨东西,总觉得多余,字段有改动的时候也费劲。新来刚接触Swift的同事,也表示不太舒服,因为他们之前使用的JSON反序列化库,无论Java中还是Objective-C中,都是自然使用Model字段名去取值的。

所以就想着研究一下,Swift中能不能做到这种效果。

HandyJSON的设计思路

Swift中存在的限制

无论是Java或者Objective-C中的JSON反序列化库,通常都是,在运行时获取Model的字段名集合,遍历该集合,拿Key去JSON中取值并完成赋值。这些步骤,Java依赖反射机制可以实现,Objective-C通过class_copyPropertyList方法加上KVC机制,也能轻松实现。而Swift会卡在最后一步: 无法赋值。

Swift的反射是只读的,就是说,我们能在运行时获取一个Model实例的所有字段、字段值,但却无法给它赋值。事实上,我们拿到的value是原值的一个只读拷贝,即使获取到这个拷贝的地址写入新值,也是无效的。

  1. class Animal {
  2. var name: String?
  3. }
  4. Mirror(reflecting: Animal()).children.forEach { (child) in
  5. print(child.label ?? "", child.value) // working correctly
  6. child.value = "cat" // error,不能直接赋值
  7. }

而且迄今,苹果官网文档上对实现反射机制的Mirror类仍然是这么描述: Mirrors are used by playgrounds and the debugger,态度非常含糊,似乎不太鼓励,但生产中很多类库都用上了。只能说,苹果不会轻易撤下这个能力,但期待它对这个能力做出改进(比如支持运行时赋值),是希望渺茫的。

如何绕过限制

最简单的方式,就是在Swift中定义Model时继承NSObject,让这个Model的实例存在于objc运行时中,上述的class_copyPropertyList方法和KVC就能用上了。目前看见的Swift中不需要指明映射关系的JSON库,都是这种方式。

然后就是以ObjectMapper为代表的库,通过运算符重载,在指定映射关系时完成赋值。走这一类实现的库也非常多了。

但我想做到的是,既支持运行在Swift运行时的纯Swift类,又不需要显示指定每一个字段的映射关系。那么,不能走反射赋值,那就直接写入内存吧。

具体实现

Swift中,一个类实例的内存布局是有规律的:

这方面基本没有官方的资料参考,上述规律一些是从网上其他大神的总结中收集,一些从Clang的一些说明文档中挖掘,还有一些是自己在playground里试出来的。开始心里没什么底,但把HandyJSON实现出来使用这么久了,还没出过状况,可以认为是靠谱的。

有法子计算内存布局,剩下的事情就比较简单了。对一个实例:

获取类实例的起始指针

Swift中,获取struct实例起始指针和获取class实例起始指针的方法是不一样的,和语言版本也相关。在Swift3中:

  1. // 获取struct实例起始指针
  2. mutating func headPointerOfStruct() -> UnsafeMutablePointer<Byte> {
  3. return withUnsafeMutablePointer(to: &self) {
  4. return UnsafeMutableRawPointer($0).bindMemory(to: Byte.self, capacity: MemoryLayout<Self>.stride)
  5. }
  6. }
  7. // 获取class实例起始指针
  8. mutating func headPointerOfClass() -> UnsafeMutablePointer<Byte> {
  9. let opaquePointer = Unmanaged.passUnretained(self as AnyObject).toOpaque()
  10. let mutableTypedPointer = opaquePointer.bindMemory(to: Byte.self, capacity: MemoryLayout<Self>.stride)
  11. return UnsafeMutablePointer<Byte>(mutableTypedPointer)
  12. }

通过Mirror获取字段名、类型

  1. Mirror(reflecting: Animal()).children.forEach { (child) in
  2. print(child.label ?? "") // 获取字段名
  3. print(type(of: child.value)) // 获取字段类型
  4. }

计算Model的每个属性字段占位大小

Swift3暴露了两个接口用于计算类型占位大小:MemoryLayout.size(ofValue: T)和MemoryLayout.size。这两者都没办法直接用,因为:

  1. protocol HandyJSON {
  2. }
  3. extension HandyJSON {
  4. static func size() -> Int {
  5. return MemoryLayout<Self>.size
  6. }
  7. }

于是,对于每一个实现HandyJSON协议的Model类T,直接调用 T.size() 就能获取到T的size了。

内存对齐的影响

类实例的属性并不是直接按照各自占位大小依次往下排列的,不然事情就简单了。和C/C++一样,Swift中实例内存布局也考虑了内存对齐。翻阅了Swift的docs和LLVM的一些资料,MemoryLayout提供了一个接口:MemoryLayout.alignment,对齐的规则为,每个字段的起始地址必须为alignment值的整数倍。细节的出处我已经忘记。当时进行了一些复杂类型的测试后,认定它符合事实。所以HandyJSON中计算下一个字段起始地址的函数为:

  1. // Returns the offset to the next integer that is greater than
  2. // or equal to Value and is a multiple of Align. Align must be
  3. // non-zero.
  4. static func offsetToAlignment(value: Int, align: Int) -> Int {
  5. let m = value % align
  6. return m == 0 ? 0 : (align - m)
  7. }

其他情况

基本类型按照上述方法处理就可以了,还有可选类型、数组类型、字典类型,通过遍历、递归解析等方式,处理方法也类似。如数组:

  1. extension Array: ArrayTypeProtocol {
  2. static func getWrappedType() -> Any.Type {
  3. return Element.self
  4. }
  5. static func castArrayType(arr: [Any]) -> Array<Element> {
  6. return arr.map({ (p) -> Element in
  7. return p as! Element
  8. })
  9. }
  10. }

获取到Array泛型实参类型,然后构造出该类型的一个数组,完成赋值就可以了。

结语

主要流程就是这样了,也比较简单,剩下处理继承、组合等情况,只是实现问题,就不再赘述了。总觉得自己对Swift指针这一套设施理解还不是很到位,也许有更好的用法,比如说,完全不需要空的init()函数就可以初始化出一个类的实例。有同学在这方面有更深入理解,有什么意见或者建议的,欢迎交流~

参考

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注