推薦一款好用的序列化框架

一、序

文章開始,先聊一聊自己的一些經歷。

客戶端和服務端打交道,首先要確定協議,包括選取數據協議和約定字段。
說到消息協議,大家可能會想到xml、json,或許還了解protobuf, protostuff, thrift, msgpack, avro ……

記得剛從學校出來去實習的時候,還真寫過用XML協議去請求服務數據的接口。
當然後來json大行其道,漸漸地替換掉了xml,作爲客戶端和服務端的主要數據協議。
剛開始是直接用JSONObject/JSONArray解析報文,後來Gson/FastJson/JackJson等解析框架湧現,還轉門開了評審會來評選引入哪一個。
再後來,kotlin普及,而kotlin自帶的kotlinx.serialization也能做數據解析。

而且,無論是Gson等框架還是kotlinx.serialization,其作用不單單是消息的封裝和解析了:
因爲能夠做json字符串和對象之間的轉換,也就是序列化和反序列化,那就能夠替代Serializable來存儲對象了。
可以說,無論是消息傳輸,還是對象存儲,json都相當的統治力。

但是作爲一種文本協議,其性能還是有一定的侷限,即使優化得再好,和一些實現得比較好的二進制協議框架還是有差距的。
當然,在數據量不是很大的情況下,json是夠用的。

但總有些情況下需要性能更好的二進制協議。
我們在一個業務中就碰到過這樣的情況:
這個業務的數據量比較大,數據在一定的時機纔會觸發上傳,在此之前會累積。
最開始時候我們是所有數據一起打包成json字符串,後來發現有的OOM的情況,就改爲分片打包上傳。
雖然解決了OOM的問題,但是這樣大的數據量,迫使我們尋求性能更好的方案。
這時候protobuf, protostuff, thrift, avro等走入了我們視野,最終技術負責人決定用protobuf。
protobuf也不負衆望,替換json後性能提升不少。
當然只是該場景替換用了protobuf, 其他業務

但是protobuf的使用是真的麻煩,需要編寫.proto文件,下載編譯軟件,生成java文件,拷貝文件到項目,項目中還要引入一個不小的SDK……
kotlinx.serialization其實也提供了一個protobuf的實現,但性能難堪大用。

換了工作後,新項目中沒有消息數據特別大的業務,json協議基本夠用。
但是尋求一個好用的序列化方案的念頭一直縈繞不去,最終,還是決定自己實現一個。
在查了各種資料,耗費了許多時日之後,終於實現了一種既高效又易用的序列化方案。

搞了許久,是騾子是馬,總得拉出來溜溜吧。
項目取名Packable, 是參考Android序列化方案Parcelable取的名字,實現上也參考了一些。
不過協議的設計是參考Protobuf的。
當然,只是參考,不盡相同,其中有不少改進。

二、用法

2.1 常規用法

序列化/反序列化對象時,實現如上接口,然後調用編碼/解碼方法即可。
用例如下:

static class Data implements Packable {
    String msg;
    Item[] items;

    @Override
    public void encode(PackEncoder encoder) {
        encoder.putString(0, msg)
                .putPackableArray(1, items);
    }

    public static final PackCreator<Data> CREATOR = decoder -> {
        Data data = new Data();
        data.msg = decoder.getString(0);
        data.items = decoder.getPackableArray(1, Item.CREATOR);
        return data;
    };
}

static class Item implements Packable {
    int a;
    long b;

    Item(int a, long b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public void encode(PackEncoder encoder) {
        encoder.putInt(0, a);
        encoder.putLong(1, b);
    }

    static final PackArrayCreator<Item> CREATOR = new PackArrayCreator<Item>() {
        @Override
        public Item[] newArray(int size) {
            return new Item[size];
        }

        @Override
        public Item decode(PackDecoder decoder) {
            return new Item(
                    decoder.getInt(0),
                    decoder.getLong(1)
            );
        }
    };
}

static void test() {
    Data data = new Data();
    // 序列化
    byte[] bytes = PackEncoder.marshal(data);
    // 反序列化
    Data data_2 = PackDecoder.unmarshal(bytes, Data.CREATOR);
}
  • 序列化
    1、聲明 implements Packable 接口;
    2、實現encode()方法,編碼各個字段(PackEncoder提供了各種類型的API);
    3、調用PackEncoder.marshal()方法,傳入對象, 得到字節數組。

  • 反序列化
    1、創建一個靜態對象,該對象爲PackCreator的實例;
    2、實現decode()方法,解碼各個字段,賦值給對象;
    3、調用PackDecoder.unmarshal(), 傳入字節數組以及PackCreator實例,得到對象。

如果需要反序列化一個對象數組, 需要創建PackArrayCreator的實例。
PackArrayCreator繼承於PackCreator,多了一個newArray方法,簡單地創建對應類型對象數組返回即可。

用過Parcelable的朋友應該對這個寫法很熟悉。
不同之處在於,Packable的put/get需要填index,加index是因爲需要支持增減字段時能正確讀取;
而Parcelable是直接依次寫入value, 讀和寫的字段需完全一致,所以用於內存的數據交換可以,但不建議做持久化。

2.2 直接編碼

上面的舉例只是範例之一,具體使用過程中,可以靈活運用。
1、PackCreator不一定要在需要反序列化的類中創建,在其他地方也可以,可任意命名。
2、如果只需要序列化(發送方),則只實現Packable即可,不需要實現PackCreator,反之亦然。
3、如果沒有類定義,或者不方便改寫類,也可以直接編碼/解碼。

static void test2() {
    String msg = "message";
    int a = 100;
    int b = 200;

    PackEncoder encoder = new PackEncoder();
    encoder.putString(0, msg)
                .putInt(1, a)
                .putInt(2, b);
    byte[] bytes = encoder.getBytes();

    PackDecoder decoder = PackDecoder.newInstance(bytes);
    String dMsg = decoder.getString(0);
    int dA = decoder.getInt(1);
    int dB = decoder.getInt(2);
    decoder.recycle();
}

2.3 自定義編碼

比方說下面這樣一個類:

class Info  {
    public long id;
    public String name;
    public Rectangle rect;
}

Rectangle是JDK的一個類),有四個字段:

class Rectangle {
  int x, y, width, height;
}

當然,有很多方案去實現(讓Rectangle實現Packable不在其中,因爲不能修改JDK)。
packable提供的一種高效(執行效率)的方法:

public static class Info implements Packable {
    public long id;
    public String name;
    public Rectangle rect;

    @Override
    public void encode(PackEncoder encoder) {
        encoder.putLong(0, id)
                .putString(1, name);
        // 返回PackEncoder的buffer
        EncodeBuffer buf = encoder.putCustom(2, 16);     // 4個int, 佔16字節
        buf.writeInt(rect.x);
        buf.writeInt(rect.y);
        buf.writeInt(rect.width);
        buf.writeInt(rect.height);
    }

    public static final PackCreator<Info> CREATOR = decoder -> {
        Info info = new Info();
        info.id = decoder.getLong(0);
        info.name = decoder.getString(1);
        DecodeBuffer buf = decoder.getCustom(2);
        if (buf != null) {
            info.rect = new Rectangle(
                    buf.readInt(),
                    buf.readInt(),
                    buf.readInt(),
                    buf.readInt());
        }
        return info;
    };
}

通常情況下,大對象嵌套一些固定字段的小對象還是挺常見的。
用此方法,可以減少遞歸層次,以及減少index的解析,能提升不少效率,

2.4 類型支持

以上是packable的序列化/反序列化的整體用法。
具體到PackEncoder/PackDecoder,又提供了哪些接口呢(支持什麼類型)?
以PackEncoder爲例,部分接口如下:

三、性能測試

除了protobuf之外,還選擇了gson來做下比較。

空間方面,序列化後數據大小如下:

數據大小(byte)
packable 2537191 (57%)
protobuf 2614001 (59%)
gson 4407901 (100%)

耗時方面,分別在PC和手機上測試了兩組數據:

  1. Macbook Pro
序列化耗時 (ms) 反序列化耗時(ms)
packable 9 8
protobuf 19 11
gson 67 46
  1. 榮耀20S
序列化耗時 (ms) 反序列化耗時(ms)
packable 32 21
protobuf 81 38
gson 190 128

四、總結

Packable的設計和實現參考了Parcelable和Protobuf,但是又有所不同。
相比於Protobuf,Packable使用更方便,性能更好;
相比於Parcelable,Packable支持版本兼容,支持跨平臺,可用於數據持久化和網絡傳輸。
說到跨平臺,目前Packable實現了Java、C++,C#,Objective-C, GO等語言。

Java平臺,目前已發佈到maven倉庫,可以直接引入,開箱即用。

dependencies {
    implementation 'io.github.billywei001:packable:1.0.1'
}

源碼地址:https://github.com/BillyWei001/Packable

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章