44

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

 5 years ago
source link: https://mp.weixin.qq.com/s/W5kQBe0vlnkG7VwIVAmnRA?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

bY3q6jN.png!web

序列化一个对象才是正经事

bY3q6jN.png!web

对象的 序列化 反序列化 是我们日常编码中一个非常基础的需求,尤其是对一个对象的json encode/decode操作。每一个平台都会有相关的库来帮助开发者方便得进行这两个操作,比如Java平台上赫赫有名的 GSON ,阿里巴巴开源的 fastJson 等等。

而在Flutter上,借助官方提供的 JsonCodec ,只能对 primitive / Map / List 这三种类型进行json的encode/decode操作,对于复杂类型, JsonCodec 提供了receiver/toEncodable两个函数让使用者手动“打包”和“解包”。

显然, JsonCodec 提供的功能看起来相当的原始,在闲鱼app中存在着大量复杂对象序列化需求,如果使用这个类,就会出现集体“带薪序列化”的盛况,而且还无法保证正确性。

bY3q6jN.png!web

官方推荐

bY3q6jN.png!web

机智如Google官方,当然不会坐视不理。 json_ serializable 的出现就是官方给出的推荐,它借助Dart Build System中的*build runner 和json_annotation库,来自动生成fromJson/to Json 函数内容。(关于使用 build_runner*生成代码的原理,之前 兴往同学的文章 已经有所提及)

关于如何使用 json_serializable 网上已经有很多文章了,这里只简单提一些步骤:

  • Step 1 创建一个实体类。

  • Step 2 生成代码:

build runner 生成序列化代码。运行完成后文件夹下会出现一个 xxx.g.dart 文件,这个文件就是生成后的文件。

  • Step 3 代理实现:

fromJsontoJson 操作代理给上面生成出来的类。

bY3q6jN.png!web

我们为什么不用它实现?

bY3q6jN.png!web

json_serializable 完美实现了需求,但它也有不满足需求的一面:

  • 使用起来有些繁琐,多引入了一个类

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

bY3q6jN.png!web

一种正经的方式

bY3q6jN.png!web

基于上面的分析,很明显的,需要一种新的方式来解决我们面临的问题,我们暂且叫它 fish-serializable

1 需要实现的功能

我们首先来梳理一下,一个序列化库需要用到:

  1. 获取可序列化对象的所有field以及它们的类型信息

  2. 能够构造出一个可序列化对象,并对它里面的fields赋值,且类型正确

  3. 支持自定义类型

  4. 最好能够解决泛型的问题,这会让使用更加方便

  5. 最好能够轻松得在不同的序列化/反序列化方式中切换,例如json和protobuf。

2 困难在哪

  1. flutter禁用了 dart:mirrors ,反射API无法使用,也就无法通过反射的方式new一个instance、扫描class的fields。

  2. 泛型的问题由于dart不进行类型擦出,可以获取,但泛型嵌套后依然无法解开。

3 Let's rock

无法使用 dart:mirrors 是个“硬”问题,没有反射的支持,类的内容就是一个黑盒。于是我们在迈出第一步的时候就卡壳了- -!

这个时候笔者脑子里闪过了很多画面,白驹过隙,乌飞兔走,啊,不是...是c++,c++作为一种无法使用反射的语言,它是如何实现对象的 序列化/反序列化 操作的呢?

一顿搜索猛如虎之后,发现大神们使用 创建类对象的回调函数 配合 的方式来实现c++中类似反射这样的操作。

这个时候,笔者又想到了曾经朝夕相处的Android(现在已经变成了flutter),Android中的 Parcelable 序列化协议就是一个很好的参照,它通过writeXXX APIs将类的数据写入一个中间存储进行序列化,再通过readXXX APIs进行反序列化,这就解决了我们上面提到的第一个问题,既如何将一个类的“黑盒子”打开。

同时,Parcelable协议中还需要使用者提供一个叫做 Creator 的静态内部类,用来在反序列化的时候反射创建一个该类的对象或对象数组,对于没有反射可用的我们来说,用c++的那种回调函数的方式就可以完美解决反序列化中对象创建的问题。

于是最终我们的基本设计就是:

BBRB32j.jpg!web

  • ValueHolder

  • FishSerializable

  • JsonSerializer

以上,我们已经基本做好了一个flutter上支持对象序列化/反序列化操作的库的基本架构设计,对象的序列化过程可以简化为:

Iby2emV.jpg!web

由于ValueHolder中间存储的存在,我们可以很方便得切换 序列化/反序列器,比如现有的JsonSerializer 用来实现json的encode/decode,如果有类似protobuf的需求,我们则可以使用ProtoBufSerializer 来将 ValueHolder 中的内容转换成我们需要的格式。

bY3q6jN.png!web

困难是不存在的

bY3q6jN.png!web

1 如何匹配类型

为了能支持泛型容器的解析,我们需要类似下面这样的逻辑:

在Java中,可以使用Class#isAssignableFrom,而在flutter中,我们没有发现类似功能的API提供。而且,如果做下面这个测试,你还会发现一些很有意思的细节:

输出的结果是:

fAZFr2u.jpg!web

可以看到,对于List这样的容器类型,函数的type argument与instance的runtimeType无法比较,当然如果使用 t is T ,是可以返回正确的值的,但需要构造大量的对象。所以基本上,我们无法进行类型匹配然后做类型转换。

2 如何解析泛型嵌套

接下去就是如何分解泛型容器嵌套的问题,考虑如下场景:

readMap中得到的value type是一个 List<int> ,而我们没有API去切割这个type argument。 所以我们采用了一种比较“笨”也相对实用的方式。我们使用字符串切割了type argument,比如:

然后在内部展开List或Map的时候,使用字符串匹配的方式匹配类型,在目前的使用中,完美得支持了标准 ListMap 容器互相嵌套。但目前无法支持标准 ListMap之 外的其他容器类型。

bY3q6jN.png!web

What's more

bY3q6jN.png!web

1 IDE插件辅助

写过Android的Parcelable的同学应该有种很深刻的体会,Parcelable协议中有大量的“机械”代码需要写,类似设计的 fish-serializable 也一样。

为了不被老板和使用库的同学打死,同时开发了 fish-serializable-intelij-plugin 来自动生成这些“机械”代码。

2 与json_serializable的对比

  • fish-serializable 在使用上配合IDE插件,减少了大量的 "as" 操作符的使用,同时在步骤上也更加简短方便。

  • 相比于  json_annotation 生成的代码,  fish-serializable 生成的代码也更具可读性,方便手动修改一些代码实现。

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

目前闲鱼app中已经开始大量使用。

3 开源计划

fish-serializable和 fish-serializable-intelij-plugin 都在开源计划中,相信不久就可以与大家见面,尽请持续关注我们的公众号~

qANJN3Z.gif

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK