一、序
文章开始,先聊一聊自己的一些经历。
客户端和服务端打交道,首先要确定协议,包括选取数据协议和约定字段。
说到消息协议,大家可能会想到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, 其他业务还是用json.
但是protobuf的使用是真的麻烦,需要编写.proto文件,下载编译软件,生成java文件,拷贝文件到项目,项目中还要引入一个不小的SDK……
kotlinx.serialization其实也提供了一个protobuf的实现,但性能难堪大用。
换了工作后,新项目中没有消息数据特别大的业务,json协议基本够用。
但是寻求一个好用的序列化方案的念头一直萦绕不去,最终,还是决定自己实现一个。
在查了各种资料,耗费了许多时日之后,终于实现了一种既高效又易用的序列化方案。
搞了许久,是骡子是马,总得拉出来溜溜吧。
项目取名Packable, 是参考Android序列化方案Parcelable取的名字。
二、用法
2.1 下载
Packable目前实现了Java、Kotlin、C++、C#、Objective-C、Go等版本。
Java和Kotlin版本代码已发布到Maven仓库,路径如下:
java:
dependencies {
implementation 'io.github.billywei01:packable-java:2.1.2'
}
kotlin:
dependencies {
implementation 'io.github.billywei01:packable-kotlin:2.1.2'
}
2.2 使用
以下以Kotlin版本的用法为例。
假设类型定义如下:
data class Person(
val name: String,
val age: Int
)
可以定义类型适配器如下:
object PersonAdapter : TypeAdapter<Person> {
override fun encode(encoder: PackEncoder, target: Person) {
encoder
.putString(0, target.name)
.putInt(1, target.age)
}
override fun decode(decoder: PackDecoder): Person {
return Person(
name = decoder.getString(0),
age = decoder.getInt(1)
)
}
}
序列化/反序列化:
private fun testEncodeObject() {
val person = Person("Tom", 20)
val encoded = PersonAdapter.encode(person)
val decoded = PersonAdapter.decode(encoded)
println("Person: ${person == decoded}")
}
private fun testEncodeObjectList() {
val list = listOf(
Person("Tom", 20),
Person("Mary", 18)
)
val encoded = PackEncoder.encodeObjectList(list, PersonAdapter)
val decoded = PackDecoder.decodeObjectList(encoded, PersonAdapter)
println("Person list: ${list == decoded}")
}
2.3 方法接口
以上是packable的序列化/反序列化的整体用法。
具体到PackEncoder/PackDecoder,又提供了哪些接口呢(支持什么类型)。
以PackEncoder为例,接口如下:
三、性能测试
测试对象:
- Packable
- Protobuf
- Gson
测试设备: Macbook Pro
测试代码:Main
测试结果:
| 数据大小(byte) | 序列化(ms) | 反序列化(ms) | |
|---|---|---|---|
| packable | 2564756 (56%) | 8 | 8 |
| protobuf | 2627081 (59%) | 16 | 17 |
| gson | 4427344 (100%) | 58 | 50 |
四、总结
Packable的设计和实现参考了Parcelable和Protobuf,但是又有所不同。
相比于Protobuf,Packable使用更方便,性能更好;
相比于Parcelable,Packable支持版本兼容,支持跨平台,可用于数据持久化和网络传输。
说到跨平台,目前Packable实现了Java、Kotlin、C++、C#、Objective-C、GO等语言。