一、序
文章開始,先聊一聊自己的一些經歷。
客戶端和服務端打交道,首先要確定協議,包括選取數據協議和約定字段。
說到消息協議,大家可能會想到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和手機上測試了兩組數據:
- Macbook Pro
序列化耗時 (ms) | 反序列化耗時(ms) | |
---|---|---|
packable | 9 | 8 |
protobuf | 19 | 11 |
gson | 67 | 46 |
- 榮耀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'
}