应该什么时候使用Codable?

Jason Yu
10 min readOct 22, 2017

--

数据序列化是每个app开发者必须面对的工作。

在OC年代,为了实现数据序列化(记得Core Data和NSKeyedArchiver吗?),我们必须为一个数据结构实现NSCoding协议,写一大堆重复啰嗦的代码,或者借助一些第三方库(如Mantle/MagicalRecord)实现。后来由于JSON的流行,iOS逐步引入了JSONSerialization和JSONEncoder类来帮助我们操作。

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

HandyJSON中的部分代码,可以看出对Swift运行时有深入的研究

自从Swift4中开始从语言和系统(Foundation)层面支持Codable(Encodable&Decodable)协议,叫好的声音不断,颇有取代ObjectMapper之势;对于HandyJson来说,也有了更强的官方版本——如果原生已经支持了,何必用第三方库呢?

Codable的第一印象

即刻和绝大多数app一样每天都在和JSON数据格式打交道,尤其是网络通信时,JSON早已成为数据序列化的首选方案。WWDC17的session也不意外的用JSON作为例子对Codable的具体使用进行说明。

一切看起来非常美好:当一个数据结构的所有字段都实现了Codable(基础数据类型系统已经帮我们做好),那只需要将它本身也声明为Codable,剩下的一切都由编译器帮你完成,而不需要像ObjectMapper一样实现mapping方法来为每一个字段指定对应key。

声明为Codable即可

然而一旦涉及到自定义,就有了额外成本:

  1. 如果key和字段名不同,那么就需要自定义CodingKeys,将所有的字段列出,并手动指定需要自定义的部分。
  2. 如果数据类型和序列化后的结构并不完全一致(如增加了嵌套),那就需要重新分别实现encode和decode方法。这不禁让人回想起之前NSCoding的类似方法,由于绝大多数都是一一对应的相似代码,比较啰嗦,此时还不如ObjectMapper(<-操作符一次同时指定了encode和decode的字段)。实践中大多数数据结构都会遇到这个问题。
  3. 如果涉及到日期、浮点类型转换,一般都需要手动指定具体转换方式。
假如字段名和结构都是自定义的话,就不那么简洁了

由于通常json和数据模型结构并非完全一致,因此大多数情况仍然是ObjectMapper更加灵活,再加上团队对于ObjectMapper已经用得非常顺手,因此我们打消了迁移到Codable的念头。

似乎Codable并不总是那么好用?我们可能误解了它,本来它就并不是专门为了解析JSON而生。

从头设计一个序列化协议

假如我们没有任何的序列化工具,需要从头开始设计,应该怎么办?

编码和解码,操作的对象是数据结构。假如我们有一个名叫Person的类,那么它一般存在于两种形式中:

  1. 内存中运行时的数据结构,由语言本身决定并管理(强类型)。将iOS app内存中表示Person实例的一段字节流,拷贝到Java的运行时环境中,由于语言对数据结构的定义不同,是无法被识别的。

2. 外部存储与通信使用,如读写数据库、网络传输、文件存储等等。由于需要在不同环境间进行交换,其表现形式必须是平台无关的(弱类型,或只有一些通用基本类型,如String,Bool等,但不会存在类型叫Person)。

当客户端收集用户数据发送给服务端,必然经过从1到2的过程,称为序列化(Serialization);而从服务端接收数据转为可操作的实例结构,称为反序列化(Deserialization)。

Encoder/Decoder

首先我们需要定义一些工具来负责具体实施转换的过程,将它们称之为Encoder/Decoder。为了说明方便,这里先看Encoder需要具备些什么能力(从内存到外部数据的工作):

  1. 将一个class或者struct转换为外部结构,如果把它的每个变量名作为key,变量值作为value,那么就可以将它看成是一系列键值对的组合。如此一来,Encoder需要提供一个键值对存储容器(container),具备将键值对转存为内部专用结构的能力。这种container暂时称为keyedContainer,也是最常用的一种容器。
  2. 有些数据结构并不能看做是键值对的组合(如数组),无法按照键值对进行存储,只能通过顺序读写,与之相对的容器暂时称为unkeyedContainer。
  3. container中的某些key可能对应嵌套的其他类型或者数组,需要为其分配一个1或2中的container,作为嵌套的子容器。
  4. 还有一些数据类型(如Int),其本身就是单个值,对应一个singleValueContainer。

实现了以上能力,一个功能完整的Encoder就可以正常工作了。

Swift中Encoder协议的定义

任何实现Encoder协议的具体类都需要具备完成以上工作的能力,如JSONEncoder(事实上JSONEncoder并没有实现Encoder,而是它内部的另一个类_JSONEncoder实现的)和PropertyListEncoder。

Encodable/Decodable

我们已经定义了Encoder,那么被编码的数据结构本身需要描述些什么呢?

由于Encoder并不了解被编码数据的内部结构,数据结构本身需要根据自身情况,在encode(encoder: Encoder)方法中:

  1. 选择相对应的keyedContainer/unkeyedContainer/singleValueContainer
  2. 枚举数组中的所需元素/结构中所需键值对(可以有选择地编码部分数据,丢弃可再生的数据),将每个元素作为参数,传给container所提供的“顺序存储”和“键值对存储”的方法
  3. 如果存在嵌套类型的情况,向encoder申请nestedContainer,再执行2
依次指定每个字段的映射和嵌套关系

由于一般简单情况下,逐个encode自身所有的字段就能满足需求,因此只要加上Codable声明,编译器就有能力帮我们完成这项重复劳动。

优点

  1. Encodable数据结构负责指定,如何按照自身结构依次调用encoder的相应容器来编码数据。本身并不关心encode完成后的具体数据结构。
  2. Encoder工具类提供一系列标准化container,以及当encodable数据结构调用时,如何编码转化为具体的外部存储结构。它并不关心encodable数据的原始结构。

可以看到两个协议分工明确,互不干扰。

这样一来,Codable协议的优点就很明显了:它并没有与某个具体的序列化格式相耦合,而只是规定了数据结构在序列化时的一般情况,与编码后的具体情况无关。当一个数据结构实现了Codable协议,涉及到该类型的代码就已经全部完成了。

如果有一天想把JSON格式换成XML,只需要将负责序列化的Encoder换掉,而不需要改动数据结构本身。而对于ObjectMapper,因为它在内部将以上两件事一起做了,如果我们还想把数据输出为XML,就需要全部重头再来。

又一次,增加的这一层抽象帮助了我们。

具体使用场景

在本地持久化数据时,即刻尝试过直接存储JSON,也考虑过用Realm,无论选择哪种,指定每个字段映射的key和value绕不过去的工作。如果每个class都需要同时支持JSON和数据库类型,由于不同的第三方库的要求不同,而“描述数据结构”这件事本身却差不多,将会有大量的重复代码。

幸运的是,SQLite.swift在最新的版本中加入了Codable支持,也就是说,只要数据结构支持Codable,在集成SQLite.swift库之后就已经具备直接写入SQLite的能力,完全省去了再手动指定字段/类型转换的代码——一边实现Codable的数据结构已经描述了自己的内部结构,另一边SQLite.swift则实现了Encoder/Decoder协议,封装了从container的到SQLite内部数据的转换细节,因此整个过程变得异常简单。

SQLite.swift中实现了Encoder和Container协议

可以想象,如果将来还要转存为XML,接入另一个支持Codable协议的库就行了,同样不需要对现有数据结构做出修改。

回过头来看开头的问题,此时会发现其实ObjectMapper和Codable并非孰优孰劣,而是各擅胜场:

  1. ObjectMapper专为JSON序列化设计,相对Codable集成度更高,提供更多语法糖,最大程度简化数据结构与JSON之间相互转换的工作量。缺点是可定制性较低,如果我们还想转成XML或者写入数据库,ObjectMapper无法帮上忙。
  2. Codable作为语言和系统层提供的协议,提供更加一般化、基础化的数据序列化规则,并且包含了完整的类型保护和错误处理机制。语法虽较ObjectMapper较为啰嗦,但是为某一个协议提供语法糖本来就并非一个语言所应该关注的,我们完全可以自己定义,或者由第三方库来帮助我们完成。

针对的情况不同,没法简单的说谁更好。目前来说,如果app从内到外全部使用JSON,那么ObjectMapper用起来更方便(可能大多数Swift app也已经习惯了);如果存在多种序列化格式,则Codable更通用一些,加上适当的语法糖一样可以变得非常易用。相信会有更多的第三方库和SQLite.swift一样,加入对Codable的支持。

--

--