FlatBuffers学习总结

来源:互联网 发布:三星c5网络权限设置 编辑:程序博客网 时间:2024/06/01 08:13

据说facebook使用google的黑科技flatbuffers,用来替代传统的json进行数据交换,大大提高了facebook android客户端的效率。于是我在网上查找各种资料学习了一下flatbuffers,参看资料包括GOOGLE官方文档、facebook技术博客、以及其他国内的个人博客,也写了些代码做实验,以此文作为学习总结。

什么是Google FlatBuffers

FlatBuffers是一个开源的、跨平台的、高效的、提供了C++/Java接口的序列化工具库。它是Google专门为游戏开发或其他性能敏感的应用程序需求而创建。尤其更适用于移动平台,这些平台上内存大小及带宽相比桌面系统都是受限的,而应用程序比如游戏又有更高的性能要求。它将序列化数据存储在缓存中,这些数据既可以存储在文件中,又可以通过网络原样传输,而不需要任何解析开销。

为什么要使用Google FlatBuffers

  1. 对序列化数据的访问不需要打包和拆包——它将序列化数据存储在缓存中,这些数据既可以存储在文件中,又可以通过网络原样传输,而没有任何解析开销;
  2. 内存效率和速度——访问数据时的唯一内存需求就是缓冲区,不需要额外的内存分配;
  3. 扩展性、灵活性——它支持的可选字段意味着不仅能获得很好的前向/后向兼容性(对于长生命周期的游戏来说尤其重要,因为不需要每个新版本都更新所有数据);
  4. 最小代码依赖——仅仅需要自动生成的少量代码和一个单一的头文件依赖,很容易集成到现有系统中。
  5. 强类型设计——尽可能使错误出现在编译期,而不是等到运行期才手动检查和修正;
  6. 使用简单——生成的C++代码提供了简单的访问和构造接口;而且如果需要,通过一个可选功能可以用来在运行时高效解析Schema和类JSON格式的文本;
  7. 跨平台——支持C++11、Java,而不需要任何依赖库;在最新的gcc、clang、vs2010等编译器上工作良好。

官网的基准测试结果如下图所示:

为什么不使用Protocol Buffers或者JSON

Protocol Buffers vs FlatBuffers

Protocol Buffers的确和FlatBuffers比较类似,但其主要区别在于FlatBuffers在访问数据前不需要解析/拆包这一步,而且Protocol Buffers没有可选的文本导入/导出功能(FlatBuffers可以直接根据Schema和json文本生成对应的二进制数据文件)。
Protocol Buffers使用一些特殊的数据结构来存储数据,从而在解析数据时需要花费额外的时间。比如,使用varints来存储整型数,varints是一种使用一个或多个字节来序列化整型数的方法,越小的数占用的字节数就越少。同时,代表一个数字的多个字节采用little endian的方式来存储。这就导致在解析数据时,需要判断哪几个字节是表示这个字段的,并进行其他的数学变换来还原出原始数据。
关于protocal buffers具体的数据编码方式,可以参考google开发者网站以及IBM技术博客。

与之相对,FlatBuffers可以直接根据起始位置+偏移量直接获取到数据,无解析过程,效率更高,而伴随的副作用是FlatBuffers需要占用相对更多的空间,因为Protocol Buffers的编码在一定程度上压缩了数据。

网上有人进行了性能对比试验,结果如下图:

JSON vs FlatBuffers

JSON是非常可读的,而且当和动态类型语言(如JavaScript)一起使用时非常方便。然而在静态类型语言中序列化数据时,JSON不但具有运行效率低的明显缺点,而且会让你写更多的代码来访问数据(这个与直觉相反)。

哪些项目使用了FlatBuffers

  • Cocos2d-x, the #1 open source mobile game engine, uses it to serialize all their game data.
  • Facebook uses it for client-server communication in their Android app. They have a nice article explaining how it speeds up loading their posts.
  • Fun Propulsion Labs at Google uses it extensively in all their libraries and games.

FlatBuffers原理

假设我们有一个person类,定义如下:

123456
class Person {    String name;    int friendshipStatus;    Person spouse;    List<Person>friends;}

其中的spouse和friends字段页包含了person对象,这样就形成了一个树结构。下面就是关于此对象在FlatBuffer中存储的简化图示:

从图中可以看到:

  • 每个对象都被分成两部分:中心点左边的元数据部分(或者叫vtable),和中心点右边的真实数据部分。
  • 每个字段都对应vtable中的一个槽(slot),它存储了那个字段的真实数据的偏移量。例如,John的vtable的第一个槽的值是1,表明John的名字被存储在John的中心点右边的一个字节的位置上。
  • 如果一个字段是一个对象,那么它在vtable中的偏移量会指向子对象的中心点(pivot point)。比如,John的vtable中第三个槽指向Mary的中心点。
  • 要表明某个字段现在没有数据,可以在vtable对应的槽中使用偏移量0来标注。

要了解更复杂的关于FlatBuffers字段修改的实现原理,可以查看facebook的文档中的“Mutation on FlatBuffers”部分。

简明使用步骤

  1. 编写一个用来定义你想序列化的数据的schema文件(又称IDL),数据类型可以是各种大小的int、float,或者是string、array,或者另一对象的引用,甚至是对象集合。各个数据属性都是可选的,且可以设置默认值,所以不必要为每个对象实例都去呈现这些字段。
  2. 使用FlatBuffer编译器flatc生成C++头文件或者Java类,生成的代码里额外提供了访问、构造序列化数据的辅助类。生成的代码仅仅依赖flatbuffers.h;
  3. 使用FlatBufferBuilder类构造一个二进制buffer。你可以向这个buffer里循环添加各种对象,而且很简单,就是一个单一函数调用;
  4. 保存或者发送该buffer;
  5. 当再次读取该buffer时,你可以得到这个buffer根对象的指针,然后就可以简单的就地读取数据内容。

FlatBuffers使用实战

构建flatc(FlatBuffers编译器)

从Google的flatbuffers仓库下载或克隆源代码,可以在Google的FlatBuffers构建文档中查看构建过程。如果你是Mac用户的话就可以直接按照下面的步骤来操作:

  1. 打开下载的源代码,目录是\{extract directory}\build\Xcode\FlatBuffers.xcodeproj ,用Xcode打开此文件。
  2. 点击Play按钮或者⌘ + R运行。
  3. flatc可执行文件会出现在项目的根目录下。
    现在就可以使用flatc来从给定的schema生成模型类,或者把JSON转换成FlatBuffers二进制文件了。

编写一个Schema

Schema语言(即IDL)的语法与C语言家族很类似。一个简单的例子如下:

12345678910111213141516171819202122232425262728
// example IDL filenamespace 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

Tables是在FlatBuffers中定义对象的主要方式,由名字(这里的Monster)和字段列表组成。每一个字段都有名字、类型、和一个可选的默认值(如果忽略的话,默认是0/NULL)。
每一个字段都是可选的:对每一个单独的对象个体,你都可以选择忽略一些字段。也就使你可以灵活的增加字段,而不必担心数据膨胀。这个设计也是FlatBuffers向前和向后兼容的机制。
注意:

  • 如果要在schema中添加新字段,只能在table的末尾进行添加。
  • 如果你不再使用某些字段了,你不能从schema中删除它们。你可以不再把它们写入到你的数据中,效果是一样的。

Structs

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的结构如下面的片段所示:

1234567891011121314151617181920212223
{  "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:ReposListRepo 和 User,还要定义root_type

可以直接从github下载已经编写好的Schema,地址为https://github.com/frogermcs/FlatBuffs/blob/master/flatbuffers/repos_schema.fbs。
这个Schema的部分片段如下:

123456789101112131415161718192021222324
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;

FlatBuffers数据文件

现在我们要做的就是把JSON文件转换成FlatBuffers二进制文件,并且生成能够以JAVA友好方式表达我们的数据的JAVA模型。方法是在中断中执行以下命令:
$ ./flatc -j -b repos_schema.fbs repos_json.json

如果一切运行正常,会看到一系列新生成的文件,它们分别是:

  • repos_json.bin (我们把它重命名为repos_flat.bin)
  • Repos/Repo.java
  • Repos/ReposList.java
  • Repos/User.java

Schema编译器(即flatc)的完整使用方法为:

flatc [ -c ] [ -j ] [ -b ] [ -t ] [ -o PATH ] [ -I PATH ] [ -S ] FILES…
[ – FILES…]

具体参数说明可以在Google说明文档查看。

在Android app中读数据

在Android Studio中使用FlatBuffers,需要把repos_flat.bin放到res/raw/目录下,同时把repo.javaReposList.java 和 User.java放到工程源代码的某个目录下。

FlatBuffers提供java库来直接在java中操纵这种数据格式。从这里下载jar文件(也可以自己下载的latBuffers源代码,用mvn生成这个库文件),把它放在Android工程的app/libs/目录下。

在需要读取FlatBuffers数据的地方,先把bin文件读取到一个byte数组中,然后用如下所示的代码访问各数据字段:

12345
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)

在Android app中写数据(以FlatBuffers格式传输数据)

先创建Builder:

1
FlatBufferBuilder fbb = new FlatBufferBuilder();

创建String字段:

1
int str = fbb.createString("MyMonster");

创建一个包含struct的table:

123456789
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官方文档。

0 0
原创粉丝点击