据说facebook使用google的黑科技flatbuffers,用来替代传统的json进行数据交换,大大提高了facebook android客户端的效率。于是我在网上查找各种资料学习了一下flatbuffers,参看资料包括GOOGLE官方文档、facebook技术博客、以及其他国内的个人博客,也写了些代码做实验,以此文作为学习总结。
FlatBuffers是一个开源的、跨平台的、高效的、提供了C++/Java接口的序列化工具库。它是Google专门为游戏开发或其他性能敏感的应用程序需求而创建。尤其更适用于移动平台,这些平台上内存大小及带宽相比桌面系统都是受限的,而应用程序比如游戏又有更高的性能要求。它将序列化数据存储在缓存中,这些数据既可以存储在文件中,又可以通过网络原样传输,而不需要任何解析开销。
官网的基准测试结果如下图所示:
Protocol Buffers的确和FlatBuffers比较类似,但其主要区别在于FlatBuffers在访问数据前不需要解析/拆包这一步,而且Protocol Buffers没有可选的文本导入/导出功能(FlatBuffers可以直接根据Schema和json文本生成对应的二进制数据文件)。
Protocol Buffers使用一些特殊的数据结构来存储数据,从而在解析数据时需要花费额外的时间。比如,使用varints来存储整型数,varints是一种使用一个或多个字节来序列化整型数的方法,越小的数占用的字节数就越少。同时,代表一个数字的多个字节采用little endian的方式来存储。这就导致在解析数据时,需要判断哪几个字节是表示这个字段的,并进行其他的数学变换来还原出原始数据。
关于protocal buffers具体的数据编码方式,可以参考google开发者网站以及IBM技术博客。
与之相对,FlatBuffers可以直接根据起始位置+偏移量直接获取到数据,无解析过程,效率更高,而伴随的副作用是FlatBuffers需要占用相对更多的空间,因为Protocol Buffers的编码在一定程度上压缩了数据。
网上有人进行了性能对比试验,结果如下图:
JSON是非常可读的,而且当和动态类型语言(如JavaScript)一起使用时非常方便。然而在静态类型语言中序列化数据时,JSON不但具有运行效率低的明显缺点,而且会让你写更多的代码来访问数据(这个与直觉相反)。
假设我们有一个person类,定义如下:
1 2 3 4 5 6 |
class Person { String name; int friendshipStatus; Person spouse; List |
其中的spouse和friends字段页包含了person对象,这样就形成了一个树结构。下面就是关于此对象在FlatBuffer中存储的简化图示:
从图中可以看到:
要了解更复杂的关于FlatBuffers字段修改的实现原理,可以查看facebook的文档中的“Mutation on FlatBuffers”部分。
从Google的flatbuffers仓库下载或克隆源代码,可以在Google的FlatBuffers构建文档中查看构建过程。如果你是Mac用户的话就可以直接按照下面的步骤来操作:
\{extract directory}\build\Xcode\FlatBuffers.xcodeproj
,用Xcode打开此文件。Play
按钮或者⌘ + R
运行。Schema语言(即IDL)的语法与C语言家族很类似。一个简单的例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// example IDL file namespace MyGame; attribute "priority"; enum Color : byte { Red = 1, Green, Blue } union Any { Monster, Weapon, Pickup } struct Vec3 { x:float; y:float; z:float; } table Monster { pos:Vec3; mana:short = 150; hp:short = 100; name:string; friendly:bool = false (deprecated, priority: 1); inventory:[ubyte]; color:Color = Blue; test:Any; } root_type Monster; |
(Weapon 和 Pickup没有在这个例子中列出。)
Tables是在FlatBuffers中定义对象的主要方式,由名字(这里的Monster)和字段列表组成。每一个字段都有名字、类型、和一个可选的默认值(如果忽略的话,默认是0/NULL)。
每一个字段都是可选的:对每一个单独的对象个体,你都可以选择忽略一些字段。也就使你可以灵活的增加字段,而不必担心数据膨胀。这个设计也是FlatBuffers向前和向后兼容的机制。
注意:
Structs与Table类似,只不过没有字段是可选的了(所以没有默认值了),并且字段不能增加或被废弃(deprecated)。Struct只能包含标量或者其他struct。如果你非常确定任何变化都不会发生,那么就可以使用struct。Struct使用的内存比table少,并且读取时比table更快(它们通常被以in-line的方式存储在它们的父对象中,并且不适用virtual table)。
内建的标量类型:
- 8 bit: byte ubyte bool
- 16 bit: short ushort
- 32 bit: int uint float
- 64 bit: long ulong double
内建的非标量类型:
- 关于任何其他类型的Vector
- string,只能存储UTF-8或者7-bit ASCII。如果需要存储其他编码的文本,或者通用二进制数据,请使用vector([byte]或者[ubyte])。
- 对其他table、struct、enum或者union的引用。
关于编写Schema的更多信息,可以参考Google文档:Writing a aschema。
在实验中,我直接使用网上的示例文件。
要处理的数据对应的JSON文件的下载地址为:https://github.com/frogermcs/FlatBuffs/blob/master/flatbuffers/repos_json.json
这个JSON的结构如下面的片段所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
{ "repos": [ { "id": 27149168, "name": "acai", "full_name": "google/acai", "owner": { "login": "google", "id": 1342004, ... "type": "Organization", "site_admin": false }, "private": false, "html_url": "https://github.com/google/acai", "description": "Testing library for JUnit4 and Guice.", ... "watchers": 21, "default_branch": "master" }, ... ] } |
为了使用FlatBuffers处理这个JSON数据,我们需要编写对应的Schema,在这个例子中需要创建3个Table:ReposList
, Repo
和 User
,还要定义root_type
。
可以直接从github下载已经编写好的Schema,地址为https://github.com/frogermcs/FlatBuffs/blob/master/flatbuffers/repos_schema.fbs。
这个Schema的部分片段如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
table ReposList { repos : [Repo]; } table Repo { id : long; name : string; full_name : string; owner : User; //... labels_url : string (deprecated); releases_url : string (deprecated); } table User { login : string; id : long; avatar_url : string; gravatar_id : string; //... site_admin : bool; } root_type ReposList; |
现在我们要做的就是把JSON文件转换成FlatBuffers二进制文件,并且生成能够以JAVA友好方式表达我们的数据的JAVA模型。方法是在中断中执行以下命令:$ ./flatc -j -b repos_schema.fbs repos_json.json
如果一切运行正常,会看到一系列新生成的文件,它们分别是:
Schema编译器(即flatc)的完整使用方法为:
flatc [ -c ] [ -j ] [ -b ] [ -t ] [ -o PATH ] [ -I PATH ] [ -S ] FILES…
[ – FILES…]
具体参数说明可以在Google说明文档查看。
在Android Studio中使用FlatBuffers,需要把repos_flat.bin放到res/raw/目录下,同时把repo.java
, ReposList.java
和 User.java
放到工程源代码的某个目录下。
FlatBuffers提供java库来直接在java中操纵这种数据格式。从这里下载jar文件(也可以自己下载的latBuffers源代码,用mvn生成这个库文件),把它放在Android工程的app/libs/目录下。
在需要读取FlatBuffers数据的地方,先把bin文件读取到一个byte数组中,然后用如下所示的代码访问各数据字段:
1 2 3 4 5 |
ByteBuffer bb = ByteBuffer.wrap(dataByteArray); ReposList rootRoposList = ReposList.getRootAsReposList(bb); Log.i("TAG", "reposLength = " + rootRoposList.reposLength()); Repo repo1 = rootRoposList.repos(0); Log.i("TAG", "id: " + repo1.id() + "\nname: " + repo1.name() + "\nforks: " + repo1.forks()); |
第一行代码中传入的参数dataByteArray就是从bin文件读出的二进制byte数组。
注意:
Java不支持无符号标量。这意味着你在schema中使用的任何无符号类型实际上都会被表达成为一个有符号的值。这表明,所有的bit都仍然在,但可能代表一个负数。比如,要把一个byte b
作为一个无符号数来读取,可以这样做:(short)(b & 0xFF)
。
先创建Builder:
1
|
FlatBufferBuilder fbb = new FlatBufferBuilder();
|
创建String字段:
1
|
int str = fbb.createString("MyMonster");
|
创建一个包含struct的table:
1 2 3 4 5 6 7 8 9 |
Monster.startMonster(fbb); Monster.addPos(fbb, Vec3.createVec3(fbb, 1.0f, 2.0f, 3.0f, 3.0, (byte)4, (short)5, (byte)6)); Monster.addHp(fbb, (short)80); Monster.addName(fbb, str); Monster.addInventory(fbb, inv); Monster.addTest_type(fbb, (byte)1); Monster.addTest(fbb, mon2); Monster.addTest4(fbb, test4s); int mon = Monster.endMonster(fbb); |
最后,需要终止这个buffer:
1
|
Monster.finishMonsterBuffer(fbb, mon);
|
这个buffer现在已经可以被传输了。它被包含在ByteBuffer中,可以通过fbb.dataBuffer()来获取。很重要的一点是,buffer中有效的数据不是从偏移量0开始的,而是从fbb.dataBuffer().position()开始的,在fbb.dataBuffer().capacity()结束。
更详细的java使用方法,可以参见google官方文档。