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

1,567 阅读4分钟

一、序

文章开始,先聊一聊自己的一些经历。

客户端和服务端打交道,首先要确定协议,包括选取数据协议和约定字段。
说到消息协议,大家可能会想到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)
packable2564756 (56%)88
protobuf2627081 (59%)1617
gson4427344 (100%)5850

四、总结

Packable的设计和实现参考了Parcelable和Protobuf,但是又有所不同。
相比于Protobuf,Packable使用更方便,性能更好;
相比于Parcelable,Packable支持版本兼容,支持跨平台,可用于数据持久化和网络传输。
说到跨平台,目前Packable实现了Java、Kotlin、C++、C#、Objective-C、GO等语言。

源码地址:github.com/BillyWei001…