如何在Flutter上优雅地序列化一个对象

7次阅读

共计 4051 个字符,预计需要花费 11 分钟才能阅读完成。

序列化一个对象才是正经事
对象的序列化和反序列化是我们日常编码中一个非常基础的需求,尤其是对一个对象的 json encode/decode 操作。每一个平台都会有相关的库来帮助开发者方便得进行这两个操作,比如 Java 平台上赫赫有名的 GSON,阿里巴巴开源的 fastJson 等等。
而在 flutter 上,借助官方提供的 JsonCodec,只能对 primitive/Map/List 这三种类型进行 json 的 encode/decode 操作,对于复杂类型,JsonCodec 提供了 receiver/toEncodable 两个函数让使用者手动“打包”和“解包”。
显然,JsonCodec 提供的功能看起来相当的原始,在闲鱼 app 中存在着大量复杂对象序列化需求,如果使用这个类,就会出现集体“带薪序列化”的盛况,而且还无法保证正确性。
来自官方推荐
聪明如 Google 官方,当然不会坐视不理。json_serializable 的出现就是官方给出的推荐,它借助 Dart Build System 中的 build_runner 和 json_annotation 库,来自动生成 fromJson/toJson 函数内容。(关于使用 build_runner 生成代码的原理,之前兴往同学的文章已经有所提及)
关于如何使用 json_serializable 网上已经有很多文章了,这里只简单提一些步骤:

Step 1 创建一个实体类
Step 2 生成代码:

来让 build runner 生成序列化代码。运行完成后文件夹下会出现一个 xxx.g.dart 文件,这个文件就是生成后的文件。
Step 3 代理实现:
把 fromJson 和 toJson 操作代理给上面生成出来的类
我们为什么不用这个实现
json_serializable 完美实现了需求,但它也有不满足需求的一面:

使用起来有些繁琐,多引入了一个类
很重要的一点是,大量的使用 ”as” 会给性能和最终产物大小产生不小的影响。实际上闲鱼内部的《flutter 编码规范》中,是不建议使用 ”as” 的。(对包大小的影响可以参见三笠同学的文章,同时 dart linter 也对 as 的性能影响有所描述)

一种正经的方式
基于上面的分析,很明显的,需要一种新的方式来解决我们面临的问题,我们暂且叫它,fish-serializable
需要实现的功能
我们首先来梳理一下,一个序列化库需要用到:

获取可序列化对象的所有 field 以及它们的类型信息
能够构造出一个可序列化对象,并对它里面的 fields 赋值,且类型正确
支持自定义类型
最好能够解决泛型的问题,这会让使用更加方便
最好能够轻松得在不同的序列化 / 反序列化方式中切换,例如 json 和 protobuf。

困难在哪里

flutter 禁用了 dart:mirrors,反射 API 无法使用,也就无法通过反射的方式 new 一个 instance、扫描 class 的 fields。
泛型的问题由于 dart 不进行类型擦出,可以获取,但泛型嵌套后依然无法解开。

Let’s rock
无法使用 dart:mirrors 是个“硬”问题,没有反射的支持,类的内容就是一个黑盒。于是我们在迈出第一步的时候就卡壳了 - -!
这个时候笔者脑子里闪过了很多画面,白驹过隙,乌飞兔走,啊,不是 … 是 c ++,c++ 作为一种无法使用反射的语言,它是如何实现对象的 序列化 / 反序列化 操作的呢?
一顿搜索猛如虎之后,发现大神们使用创建类对象的回调函数配合宏的方式来实现 c ++ 中类似反射这样的操作。
这个时候,笔者又想到了曾经朝夕相处的 Android(现在已经变成了 flutter),Android 中的 Parcelable 序列化协议就是一个很好的参照,它通过 writeXXXAPIs 将类的数据写入一个中间存储进行序列化,再通过 readXXXAPIs 进行反序列化,这就解决了我们上面提到的第一个问题,既如何将一个类的“黑盒子”打开。
同时,Parcelable 协议中还需要使用者提供一个叫做 CREATOR 的静态内部类,用来在反序列化的时候反射创建一个该类的对象或对象数组,对于没有反射可用的我们来说,用 c ++ 的那种回调函数的方式就可以完美解决反序列化中对象创建的问题。
于是最终我们的基本设计就是:

ValueHolder
这是一个数据中转存储的基类,它内部的 writeXXX APIs 提供展开类内部的 fields 的能力,而 readXXX 则
用来将 ValueHolder 中的内容读取赋值给类的 fields。

readList/readMap/readSerializable 函数中的 type argument,我们把它作为外部想要解释数据的
方式,比如 readSerializable<T>(key: ‘object’),表示外部想要把 key 为 object 的值解释为 T 类
型。
FishSerializable
FishSerializable 是一个 interface,creator 是个一个 get 函数,用来返回一个“创建类对象的回调”,
writeTo 函数则用来在反序列化的时候放置 ValueHoder->fields 的代码。
JsonSerializer
它继承于 FishSerializer 接口,实现了 encode/decode 函数,并额外提供 encodeToMap 和
decodeFromMap 功能。JsonSerializer 类似 JsonCodec,直接面向使用者用来 json encode/decode
以上,我们已经基本做好了一个 flutter 上支持对象序列化 / 反序列化操作的库的基本架构设计,对象的序列化过程可以简化为:

由于 ValueHolder 中间存储的存在,我们可以很方便得切换 序列化 / 反序列器,比如现有的 JsonSerializer 用来实现 json 的 encode/decode,如果有类似 protobuf 的需求,我们则可以使用 ProtoBufSerializer 来将 ValueHolder 中的内容转换成我们需要的格式。
困难是不存在的
有了基本的结构设计之后,实现的过程并非一帆风顺。
如何匹配类型?
为了能支持泛型容器的解析,我们需要类似下面这样的逻辑:
List<SerializableObject> list
= holder.readList<SerializableObject>(key: ‘list’);

List<E> readList<E>({String key}){
List<dynamic> list = _read(key);
}

E _flattenList<E>(List<dynamic> list){
list?.map<E>((dynamic item){
// 比较 E 是否属于某个类型,然后进行对应类型的转换
});
}
在 Java 中,可以使用 Class#isAssignableFrom,而在 flutter 中,我们没有发现类似功能的 API 提供。而且,如果做下面这个测试,你还会发现一些很有意思的细节:
void main() {
print(‘int test’);
test<int>(1);
print(‘\r\nint list test’);
test<List<int>>(<int>[]);
print(‘\r\nobject test’);
test<A<int>>(A<int>());
}

void test<T>(T t){
print(T);
print(t.runtimeType);
print(T == t.runtimeType);
print(identical(T, t.runtimeType));
}

class A<T>{

}
输出的结果是:

可以看到,对于 List 这样的容器类型,函数的 type argument 与 instance 的 runtimeType 无法比较,当然如果使用 t is T,是可以返回正确的值的,但需要构造大量的对象。所以基本上,我们无法进行类型匹配然后做类型转换。
如何解析泛型嵌套?
接下去就是如何分解泛型容器嵌套的问题,考虑如下场景:
Map<String, List<int>> listMap;

listMap = holder.readMap<String, List<int>>(key: ‘listMap’);
readMap 中得到的 value type 是一个 List<int>,而我们没有 API 去切割这个 type argument。所以我们采用了一种比较“笨”也相对实用的方式。我们使用字符串切割了 type argument,比如:
List<int> => <String>[List<int>, List, int]
然后在内部展开 List 或 Map 的时候,使用字符串匹配的方式匹配类型,在目前的使用中,完美得支持了标准 List 和 Map 容器互相嵌套。但目前无法支持标准 List 和 Map 之外的其他容器类型。
What’s more
IDE 插件辅助
写过 Android 的 Parcelable 的同学应该有种很深刻的体会,Parcelable 协议中有大量的“机械”代码需要写,类似设计的 fish-serializable 也一样。
为了不被老板和使用库的同学打死,同时开发了 fish-serializable-intelij-plugin 来自动生成这些“机械”代码。
与 json_serializable 的对比

fish-serializable 在使用上配合 IDE 插件,减少了大量的 ”as” 操作符的使用,同时在步骤上也更加简短方便。
相比于 json_annotation 生成的代码,fish-serializable 生成的代码也更具可读性,方便手动修改一些代码实现。

fish-serializable 可以通过手动接管 序列化 / 反序列化 过程的方式完美兼容 json_annotation 等其他方案。

目前闲鱼 app 中已经开始大量使用。
开源计划
fish-serializable 和 fish-serializable-intelij-plugin 都在开源计划中,相信不久就可以与大家见面,尽请期待~

本文作者:闲鱼技术 - 海潴阅读原文
本文为云栖社区原创内容,未经允许不得转载。

正文完
 0