1 概述
FlatBuffers(官方地址:FlatBuffers: FlatBuffers)最初是由Google创建用于游戏开发或者其它性能敏感的应用程序需求上的一个可跨语言、跨平台、高效的开源序列化工具库。目前开源的游戏戏引擎Cocos2d-x便是使用FlatBuffers来序列化所有的游戏数据;Facebook在Android应用程序中也是使用了FlatBuffers进行客户端与服务端的通信;谷歌的趣味推进实验室在所有的图书馆和游戏中也广泛使用FlatBuffers。FlatBuffers使用时只要使用.fbs文件一次定义好数据的结构化方式,然后借助工具便可以针对C++、C#、C、Go、Java、Kotlin、JavaScript、Lobster、Lua、TypeScript、PHP、Python、Rust和Swift等语言使用命令生成特殊的源代码将数据结构或对象进行序列化和反序列化。
2 特点
1 无需解析/解包即可访问序列化数据
它在一个平面二进制缓冲区中表示分层数据,这样在不进行解析/解包的情况下仍然可以直接访问分层数据,而无需解析/解包,同时还支持数据结构前后扩展兼容。
2内存占用小且运行效率高
访问数据所需的唯一内存是缓冲区,所以也是非常适用于流式读写,在进行序列化和反序列化过程中不会额外占用其他内存空间,读取数据的速度几乎跟直接从内存读取原始数据一样,仅仅多出一个相对寻址的操作。
3 灵活
支持字段可选读写,意味着数据结构在不同的版本中支持向后兼容扩展。
4 体积小
集成简单,代码量小
5 支持强类型约束
尽可能让错误发生在编译期,而不是等到运行期间才检查和修正。
6 使用简单
生成的C++代码提供了简单的访问和构造接口,且如果有必要可以进行Schema和Json的兼容解析。
7 可跨平台
支持C++代码可在gcc、clang、VS2010、AndroidStudio等编译器上工作。
3 与 Json 和 Protocol Buffers 对比
Json是一种可读性和通用性都非常强的轻量级的数据交换格式,并且是行业公认的标准,与JavaScript等的动态类型语言一起使用时非常方便,只要将数据转换成字符串,便可以在函数之间轻松地传递字符串进行数据传递。然而当从像C++、Java等的静态类型语言序列化数据时,Json就会表现出运行效率低下的明显缺点,而且还需要编写更多代码来访问数据。
Protocol Buffers和FlatBuffers是比较相似,它们都是出自Google,Protocol Buffers从空间方面考虑较有优势,非常适用于节省流量场景的数据传递,而FlatBuffers在序列化过程中更强调性能。FlatBuffers的数据都在对应的二进制缓冲区中,它不需要在访问数据之前去将数据完全解析成对象,省去对象的分配内存空间就会降低解析过程中的内存消耗。
4 下载
FlatBuffers的下载地址:https://github.com/google/flatbuffers/releases,选择你当前系统的包下载后,然后通过命令中输入命令查看其版本:
5 schema语法
我们同样使用上篇《Android序列化 之 Protocol Buffers》文章中的addressbook作为实例讲解,新建Scheme文件,文件名为addressbook.fbs,其内容如下所示:
namespace com.zyx.myapplication.fbs.person;
enum PhoneType : int {
MOBILE = 0,
HOME = 1,
WORK = 2,
}
table PhoneNumber {
number:string;
type:com.zyx.myapplication.fbs.person.PhoneType = HOME;
}
namespace com.zyx.myapplication.fbs;
table Person {
name:string;
id:int;
email:string;
phones:[com.zyx.myapplication.fbs.person.PhoneNumber];
}
table AddressBook {
people:[Person];
}
root_type AddressBook;
如果你本来就存在Protocol Buffers的addressbook.proto文件,也可以使用命令转化成addressbook.fbs(转化后的内容跟上述有点区别)。
5.1 namespace
namespace关键字用于指定包名,支持用.分割的嵌套形式的包名。
5.2 table
table关键字是FlatBuffers中定义对象的主要方式。内部每个字段由名称、类型和可选的默认值组成,如果未指定默认值,则值类型的字段默认为0,其它类型默认为null。每个字段都是可选的,所以我们可以灵活地添加字段而不必担心数据膨胀,这种设计也是FlatBuffers的向前和向后兼容的机制。但要注意:
- 只能在table的字段列表最后添加新的字段,如果一定想要任意顺序插入新字段,就需要手动给每个字段分配id。
- 不能删除不再使用的字段,实在无用的字段可以停止给它们写入数据。此外还可以给它们标记为 deprecated,表示不推荐使用。
- 可以更改字段的名称或table名称,但是不推荐,因为这样会对原来代码造成破坏。
5.3 enum
enum关键字用于定义枚举类型,和常规的枚举不同的是它可以给枚举定义类型,枚举在最后生成Java代码后是一个类。
5.4 struct
struct 关键字用于定义结构,结构中的所有字段是必填的,没有默认值。并且字段不能添加或废弃。一般struct 用于数据结构不会发生改变的场景,它相对table使用更少的内存。
5.5 root_type
root_type关键字用于声明了序列化数据的根表。
5.6 支持的类型
FlatBuffers 支持的标量类型有以下几种:
8 bit: byte (int8), ubyte (uint8), bool
16 bit: short (int16), ushort (uint16)
32 bit: int (int32), uint (uint32), float (float32)
64 bit: long (int64), ulong (uint64), double (float64)
括号中的类型名是别名,例如uint8可以用来代替ubyte,int32可以用来代替int而不影响代码生成。
FlatBuffers 支持的 非标量 类型有以下几种:
任何类型的数组。不过不支持嵌套数组,可以用 table 内定义数组的方式来取代嵌套数组。
String只能保存UTF-8或7位ASCII。对于其他文本编码或一般二进制数据,需要使用[byte]或[ubyte] 来替代。
table、structs、enums、unions
一旦一个类型声明了,尽量不要改变它的类型,一旦改变了,很可能就会出现错误。
5.7 更多
更多语法可直接前往官网查看:
6 编译.fbs文件
FlatBuffers提供多种开发语言的API,以Java为例,只要运行如下命令即可对.fbs进行编译并生成对应的.java文件:
flatc –j –o $DST_DIR $SRC_DIR/XXX.fbs
7 Android中使用
7.1 将生成的.java文件放入到Android项目中
将自动生成的所有.java文件拷贝到Android工程目录下:
7.2 依赖flatbuffers-java
在Gradle中添加 flatbuffers-java依赖(其版本必须要和下载的FlatBuffers版本一致):
implementation 'com.google.flatbuffers:flatbuffers-java:2.0.0'
7.3 序列化
private byte[] serialize() {
// 创建ByteBuffer数组,默认长度是1024
FlatBufferBuilder builder = new FlatBufferBuilder();
int nameOffset = builder.createString("子云心");
int id = 1001;
int emailOffset = builder.createString("abc@test.com");
int[] phoneNumbersOffset = new int[2];
// 方式一,添加phoneNumber
int numberOffset1 = builder.createString("0000-1234567");
int type1 = PhoneType.WORK;
PhoneNumber.startPhoneNumber(builder);
PhoneNumber.addNumber(builder, numberOffset1);
PhoneNumber.addType(builder, type1);
phoneNumbersOffset[0] = PhoneNumber.endPhoneNumber(builder);
// 方式二,添加phoneNumber
int numberOffset2 = builder.createString("1111-1234567");
int type2 = PhoneType.MOBILE;
phoneNumbersOffset[1] = PhoneNumber.createPhoneNumber(builder, numberOffset2, type2);
// 创建Person
int[] personsOffset = new int[1];
int phonesOffset = Person.createPhonesVector(builder, phoneNumbersOffset);
personsOffset[0] = Person.createPerson(builder, nameOffset, id, emailOffset, phonesOffset);
// 创建AddressBook
int peopleOffset = AddressBook.createPeopleVector(builder, personsOffset);
int rootTable = AddressBook.createAddressBook(builder, peopleOffset);
// 结束编码
builder.finish(rootTable);
byte[] data = builder.sizedByteArray();
return data;
}
注意:如果在调用builder.createString处发生如下异常:
java.lang.NoSuchMethodError: No virtual method position(I)Ljava/nio/ByteBuffer; in class Ljava/nio/ByteBuffer; or its super classes (declaration of 'java.nio.ByteBuffer' appears in /system/framework/core-oj.jar)
at com.google.flatbuffers.FlatBufferBuilder.createString(FlatBufferBuilder.java:605)
……
这是因为flatbuffers-java的2.0.0使用的JDK版本兼容问题,可以尝试将工程中JDK版本升至11 或者 重新下载1.12.0版本的flatc.exe再重新生成代码和将flatbuffers-java降级至1.12.0。
7.4 反序列化
private void deserializer(byte[] data) {
ByteBuffer byteBuffer = ByteBuffer.wrap(data);
AddressBook addressBook = AddressBook.getRootAsAddressBook(byteBuffer);
Person person = addressBook.people(0);
String name = person.name();
int id = person.id();
String email = person.email();
PhoneNumber phoneNumber1 = person.phones(0);
int type1 = phoneNumber1.type();
String number1 = phoneNumber1.number();
PhoneNumber phoneNumber2 = person.phones(1);
int type2 = phoneNumber2.type();
String number2 = phoneNumber2.number();
}
8 基本原理
FlatBuffers在使用时通过工具将 .fbs 文件中定义的 table生成对应的继承于Table的Java类(struct生成继承于Struct的类),该类中每个字段的类型及长度会根据table中的声明的字段生成,每个字段在数据中的位置也会被确定下来。
FlatBuffers编码原理主要体现在FlatBufferBuilder类中,在进行序列化时会先创建一个FlatBufferBuilder类对象,默认构造函数会创建一个长度为1024的ByteBuffer,ByteBuffer将数据按小端序进行序列化成一个Byte数组,该Byte数组无需进行解码,在反序列化时主要再将Byte数组转成ByteBuffer,Table提供了针对Byte数组的操作,生成的Java类负责对这些数据进行解释。
在ByteBuffer中,每个对象在数组中被分为两部分。元数据部vTable:负责存放索引。真实数据部objectData:存放实际的值。以Table为例来讲:
vTable负责存放索引,用于给 objectData 提供描述信息,里面所有元素都是16位的偏移值offset:
元素1:vTableSize, 用于记录整个vTable本身大小
元素2:objectDatatSize, 用于记录objectData的大小
元素3、4、5...N:fieldOffset,用于按照Schema中定义的字段顺序记录相对于table起始地址的偏移地址。例如有字段a、b、c,那么元素3、4、5分别指a、b、c相对于table起始地址的偏移地址。如果有某个元素为0,说明该元素没有真实值,后续获取到的将是默认值。
objectData 负责存放实际的值,它包含一个32位的offset 用于指定vTable 和 用于存放实际值的 instance。
9 总结
FlatBuffers是Google开发的一种跨语言跨平台的序列化机制,优势方面体现在其序列化过程的高性能。FlatBuffers会创建一个ByteBuffer将对象数据按小端序序列化后缓存在一个Byte数组中,对象在数组中被分元数据部分和真实数据两部分,分别用于存放索引和存放实际值。在反序列化时将Byte数组转成ByteBuffer再利用自动生成的Java类再将数据进行解释。FlatBuffers不需要在访问数据之前去将数据完全解析成对象,省去对象的分配内存空间就会降低解析过程中的内存消耗。