概述
本文正式开始分析SwiftyJSON。
原生的JSONSerialization已经实现了高效的Data
和Any
相互转化,所差的只是一种灵活方便的方式,进行错误处理和对Any
对象进行转型。SwiftyJSON的方案是,设计一个JSON结构体,该结构体有一个type
属性来对应JSON的6种数据类型,每种类型都有一个相应的Swift属性与之绑定。通过递归的对解析后的Any
类型对象进行解包,判断出每一层的数据类型并将确定完类型的对象赋值到JSON结构体的object
属性中。然后通过Swift的Subscript
特性,以下标的方式让用户获取JSON结构体中嵌套数据。最后通过Swift的RawRepresentable
特性,来获取最终的Swift对象。
代码充分利用了Swift “面向协议编程” 和其他语言特性,可以说非常的Swifty,下面我们来一一分析和学习。
首先是第一个部分,JSON结构体的设计和实现,在此之前先简单的回顾一下JSON的定义和苹果的JSONSerialization
类
关于JSON
JSON全称JavaScript Object Notation, 是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。
JSON有6种数据格式:
-
object
,格式为{ string: value, string: value }
,如{ "code": 0, "version": "2.5"}
用以表示一个无序的“名称/值”对的集合,对应于Swift中的Dictionary
-
array
,格式为[ value, value ]
,如[ "foo", "bar" ]
用以表示一个有序的值的集合,对应于Swift中的Array
-
string
,格式为" string "
,如"foo"
用以表示一个字符串,对应于Swift中的String
-
number
,如123
用以表示一个数字,对应于Swift中的数值类型,如Int
Double
UInt
等 -
true
和false
用以表示布尔值,对应Swift中的Bool
-
null
用以表示空,对应Swift中的nil
JSON的最外层必须是object
或array
,内部可以以多层object
和array
嵌套的形式来表示复杂的数据结构
关于JSONSerialization
JSONSerialization是苹果用于序列化/反序列化JSON数据的类,其可以实现JSON的二进制数据Data
和Any
对象的相互转化。由于Data
并不一定能反序列化为Any
对象,所以需要进行错误处理。由于得到的是Any
类型,所以当实际使用的时候,还必须进行多次的转型和判断。
do {
let jsonObject: Any = try JSONSerialization.jsonObject(with: data, options: [])
} catch {
print("解析失败")
}
SwiftyJSON中的JSON结构体的设计
从文章开始的分析中可以得知,该JSON结构体需要有Any
类型的object
属性来储存从Data
反序列化后的结果,一个枚举type
属性来表示该object
的类型,以及一个NSError
类型的error
属性来进行错误处理。
其流程为:
- 假设要传输的JSON为
{ “user” : “ysj”}
,我们从网络接收到的数据将是一个17字节的Data
; - 通过
JSONSerialization
反序列化后,如果没有错误发生,将得到一个Any
对象,此时我们并不知道它里面具体是什么; - 把
Any
对象递归的解包之后,就得到了unwrapedObject
,即字典["user": "ysj"]
; - 根据
unwrapedObject
的类型,对结构体的type
和rowValue
赋值,以方便后续的使用 - 以计算属性
object
对type
和rowValue
进行封装,方便外部对JSON结构体数据的使用
关于Struct
和Class
的选择
在Swift中,结构体不仅可以定义方法实现逻辑,还可以通过遵守、实现协议来获得更多的特性。而且对于不需要获得OC特性的类型来说,也不再需要声明为NSObject的子类,可以说,不能再以OC中的结构体和类的概念来思考自定义类型。
Swift中的Struct
和Class
的最大区别有两个,可以根据这两大区别来灵活选择使用哪一种
-
Struct
是值类型,Class
是引用类型。也就是说,在进行赋值的时候:Struct
是深复制,其结果是实例的一个全新的副本;而Class
是浅复制,其结果是原实例的一个新的引用。当一个实例传递到多处使用时,尤其是在多线程的条件下,由于Struct
的值类型特点,我们可以不用担心非预期的数据修改、多线程数据竞争 -
Struct
只是简单的数据/方法集合体,虽然可以通过方法和协议获得强大的能力,但并不支持继承、多态、引用计数等功能。
知识点三:自定义类型时,不要不加思索的使用
Class
。如果在传递时更倾向于生成一个新的副本,并且本身不需要面向对象的特性,应使用Struct
来获得更多的线程安全和内存安全。
JSON结构体的属性
关于Swift中的属性
Swift有两种属性:
- 存储属性(Stored Properties)
就是我们平常使用的普通属性,它直接用来存储数据。根据Swift的初始化规则,每个非
optional
的属性都必须进行初始化,所以一般直接在声明的属性以字面值、构造器或闭包的方式赋值。
比较特殊的是以lazy
关键字修饰的“懒加载属性”,这种属性不会和实例一起完成初始化,而是在第一次被使用的时候再初始化。其好处一是可以节省资源、加快实例化的速度;二是可能某些属性需要对象被实例化后才能确定(官方教材的说法,我没想出来应用场景,欢迎同学们分享你的idea)。
- 计算属性(Computed Properties)
其数据是依靠其他的属性通过一定的逻辑处理来确定的。通过在属性后加大括号并在里面添加
set
get
方法来实现。如果属性需要被声明为只读,可以只添加get
方法而不添加set
方法,此时的get { }
可以省略,直接return
数据即可。
关于JSON结构体的属性
- 私有的枚举
Type
类型的属性_type
,来对应6种JSON数据格式,考虑到存在非法格式的可能性,另增加了unknown
类型。
- 私有的与
Type
相对应6个属性rawValue
,用于存储进行过类型转换后的最终结果
- 私有的
NSError?
类型的_error
属性用于储存在解析过程中出现的错误 - 暴露给外部使用的
object
计算属性,其setter方法将对给定的Any
对象解包后根据类型对type
和rawValue
赋值 ,其getter方法将根据类型type
返回相应的rawValue
- 暴露给外部使用的
type
和error
属性。外部可能需要结构体的Type
和Error
信息,但是不应赋予其写的权限。因此声明两个只读的计算属性type
和error
,直接返回_type
和_error
的值
知识点四:根据实际需要灵活使用存储属性(包括懒加载)和计算属性。如果有私有存储属性需要给外部提供只读接口,可以把私有属性命名为
_name
的形式,并增加一个以name
命名的只读计算属性,直接返回_name
的值
JSON结构体的初始化方法
初始化
从输入考虑,有如下可能的情况:二进制数据Data
,JSON字符串String
,任意类型Any
。虽然有多种可能的输入,但是最终的逻辑是一样的,即通过一个解析后的Any
对象,生成一个JSON结构体。我们可以把最终生成JSON结构体的逻辑封装成一个指定初始化方法(Designated Initializer),其他的所有初始化方法则是对输入进行相应的处理后,再代理给指定初始化方法。(严格来说,指定初始化方法是作用于Class
,相对于Convenience Initializers来说的,但是其思想同样可以用于Struct
)
这样的设计思路有几个好处,一是把初始化的逻辑拆分成了两大块,降低了复杂度;二是提高了代码的复用性和扩展能力
具体的初始化方法分析如下:
- 私有的指定初始化方法,通过参数
jsonObject
创建JOSN结构体实例并初始化object
属性 - 二进制数据
Data
。调用系统的JSONSerialization
对Data
进行解析,将解析后的jsonObject
对象代理给指定初始化方法进行初始化;如果有错误产生,那么通过指定初始化方法构造一个空JSON,并把错误保存到属性error
- JSON字符串
String
。通过字符串创建Data
,再代理给「情况2」进行初始化,如果创建失败,则通过指定初始化方法构造一个空JSON - 任意类型
Any
。因为此Any
不一定是JSONSerialization
解析后的Any
,还有可能是Data
类型。所以进行一次判断,如果是Data
则代理给「情况2」进行初始化,否则代理给指定初始化方法进行初始化
知识点五: 对初始化方法的设计,应在分析可能的参数和初始化逻辑的基础上,设计出指定初始化方法,形成一个初始化代理链,以降低初始化方法的复杂度,提高复用和扩展性
合并
除了初始化外,还有一种需要创建JSON结构体实例的情况是,合并两个JSON结构体。方法代码截图如下:
方法中使用了
mutating
关键字,其作用是使得方法可以修改调用该方法的实例本身,即self
。
联想一下Swift数组排序的两个方法sort()
和sorted()
,sort()
的方法声明mutating func sort()
,sorted()
的方法声明func sorted() -> [Self.Iterator.Element]
。前者是直接修改数组,后者是返回一个新的数组。同时苹果对这种方法的命名规则也是很值得学习的,以被动语态表示这是一个经过处理后的新实例。
设想一下使用场景,相比返回一个新的实例,直接修改原来的实例可以不用声明一个新的变量或重新赋值,无疑更适合我们的情况。
知识点六:设计操作实例的方法时,除了返回一个新的实例,也可以通过
mutating
关键字直接修改实例本身,命名时通过方法的被动/主动语态区分两者。
合并逻辑:
- 如果两个结构体的
type
相同:
- 如果是除字典和数据外的类型,直接替换为新的数据
- 如果是数组,把两个数组的内容合并
- 如果是字典,遍历字典,如果
key
相同,递归的调用合并方法把对应的value
进行合并,并附加一个标志typecheck
- 如果两个结构体的
type
不同:
- 如果
typecheck
为真说明是第一次调用,也就是说两个结构体的最外层结构是不相同的,那么无法合并,抛出定义好的NSError
错误 - 如果
typecheck
为假说明是遍历字典时递归调用的,也就是说两个结构体的最外层结构是一样,只不过里层的结构不同,那么把里面的数据直接替换为新的数据