天天看點

如何在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_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

需要實作的功能

我們首先來梳理一下,一個序列化庫需要用到:

  1. 擷取可序列化對象的所有field以及它們的類型資訊
  2. 能夠構造出一個可序列化對象,并對它裡面的fields指派,且類型正确
  3. 支援自定義類型
  4. 最好能夠解決泛型的問題,這會讓使用更加友善
  5. 最好能夠輕松得在不同的序列化/反序列化方式中切換,例如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序列化協定就是一個很好的參照,它通過

writeXXX

APIs将類的資料寫入一個中間存儲進行序列化,再通過

readXXX

APIs進行反序列化,這就解決了我們上面提到的第一個問題,既如何将一個類的“黑盒子”打開。

同時,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是一個interface,creator是個一個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('\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>{

}           

輸出的結果是:

如何在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]           

然後在内部展開

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

都在開源計劃中,相信不久就可以與大家見面,盡請期待~