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

发布时间:2019-06-09 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了如何在Flutter上优雅地序列化一个对象脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。

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

对象的序列化反序列化是我们日常编码中一个非常基础的需求,尤其是对一个对象的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_runnerjson_annotation库,来自动生成FromJson/toJson函数内容。(关于使用build_runner生成代码的原理,之前兴往同学的文章已经有所提及)

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

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

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

  • Step 3 代理实现:

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

我们为什么不用这个实现

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

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

一种正经的方式

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

需要实现的功能

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

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

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

    Let's rock

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

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

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

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

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

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

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

    • ValueHolder
    这是一个数据中转存储的基类,它内部的writeXXX APIs提供展开类内部的fields的能力,而readXXX则
    用来将ValueHolder中的内容读取赋值给类的fields。
    
    readList/readMap/readSerializable函数中的type argument,我们把它作为外部想要解释数据的
    方式,比如readSerializable<T>(key: 'object'),表示外部想要把key为object的值解释为T类
    型。
    • FishSerializable
    FishSerializable是一个interfacecreator是个一个get函数,用来返回一个“创建类对象的回调”,
    writeTo函数则用来在反序列化的时候放置ValueHoder->fields的代码。
    • JsonSerializer
    它继承于FishSerializer接口,实现了encode/decode函数,并额外提供encodeToMap和
    decodeFromMap功能。JsonSerializer类似JsonCodec,直接面向使用者用来json encode/decode

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

    如何在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('rnint list test');
      test<List<int>>(<int>[]);
      print('rnobject 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>{
    
    }

    输出的结果是:

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

    可以看到,对于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]

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

    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-serializablefish-serializable-intelij-plugin都在开源计划中,相信不久就可以与大家见面,尽请期待~



    本文作者:闲鱼技术-海潴

    阅读原文

    本文为栖社区原创内容,未经允许不得转载

    脚本宝典总结

    以上是脚本宝典为你收集整理的如何在Flutter上优雅地序列化一个对象全部内容,希望文章能够帮你解决如何在Flutter上优雅地序列化一个对象所遇到的问题。

    如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

    本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
    如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。