FlatBuffers 简介
FlatBuffers 是 Google 开源的一个跨平台的, 高效的, 提供了 C++/Java 接口的序列化工具库, 它是 Google 专门为游戏开发或其他性能敏感的应用程序需求而创建. 尤其适用移动, 嵌入式平台, 这些平台在内存大小及带宽相比桌面系统都是受限的, 而应用程序比如游戏又有更高的性能要求. 它将序列化数据存储在缓存中, 这些数据既可以存储在文件中, 又可以通过网络原样传输, 而不需要任何解析开销. 以下是项目地址: 代码托管主页: https://github.com/google/flatbuffers; 项目介绍主页:
FlatBuffers 优势
相比传统的 JSON 和 Protocol Buffers 等序列化工具, FlatBuffers 具有如下的一些优点:
不需要解析 / 拆包就可以访问序列化数据: FlatBuffers 与其他库不同之处就在于它使用二进制缓冲文件来表示层次数据, 这样它们就可以被直接访问而不需解析与拆包, 同时还支持数据结构进化(前进, 后退兼容性).
内存高效速度快 : 访问数据时只需要访问内存中的缓冲区. 它不需要多余的内存分配 (至少在 C++ 是这样, 其他语言中可能会有变动). FlatBuffers 还适合配合 mmap 或数据流使用, 只需要缓冲区的一部分存储在内存中. 访问时速度接近原结构访问, 只有一点延迟(一种虚函数表 vtable), 是为了允许格式升级以 及可选字段. FlatBuffers 适合那些花费了大量时间和空间(内存分配) 来访问和构建序列化数据的项目, 比如游戏以及其他对表现敏感的应用. 可以参考 FlatBuffers 基准.
灵活 : 由于具有可选字段, 你不但有很强的升级和回退兼容性(对于历史悠久的游戏尤其重要, 不用为了每个版本升级所有数据), 在选择要存储哪些数据以及设计数据结构时也很自由.
轻量的 code footprint:FlatBuffers 只需要很少量的生成代码, 以及一个表示最小依赖的很小的头文件, 很容易集成.
强类型: 当编译时报错时, 不需要自己写重复的容易出错的运行时检查, 它可以自动生成有用的代码.
使用方便: 生成的 C++ 代码允许精简访问与构建代码, 还有可选的用于实现图表解析, 类似 JSON 的运行时字符串展示等功能的方法.(后者比 JSON 解析库更快, 内存效率更高).
代码跨平台且没有依赖: C++ 代码可以运行在任何近代的 gcc/clang 和 VS2010 上, 同时还有用于测试和范例的构建文件(Android 中. mk 文件, 其他平台是 cmake 文件).
VS Protocol Buffers 和 JSON
Protocol Buffers 的确和 FlatBuffers 比较类似, 但其主要区别在于 FlatBuffers 在访问数据前不需要解析 / 拆包这一步, 而且 Protocol Buffers 既没有可选的文本导入 / 导出功能, 也没有 Schemas 语法特性(比如 union).
JSON 是一种轻量级的数据交换格式, JSON 可以将 JavaScript 对象中表示的一组数据转换为字符串, 然后就可以在函数之间轻松地传递这个字符串, 或者在异步应用程序中将字符串从 web 客户机传递给服务器端程序. JSON 和动态类型语言 (如 JavaScript) 一起使用时非常方便. 然而在静态类型语言中序列化数据时, JSON 不但具有运行效率低的明显缺点, 而且会让你写更多的代码来访问数据.
FlatBuffers 究竟有多提高
解析速度: 解析一个 20KB 的 JSON 流需要 35ms, 超过了 UI 刷新间隔也就是 16.6ms. 如果解析 JSON 的话, 我们就在滑动时就会因为要从磁盘加载缓存而导致掉帧(视觉上的卡顿).
解析器初始化 : 一个 JSON 解析器需要先构建字段映射再进行解析, 这会花 100ms 到 200ms, 很明显的拖缓 App 启动时间.
垃圾回收 在解析 JSON 时创建了很多小对象, 在我们的试验中, 解析 20kb 的 JSON 流时, 要分配大约 100kb 的瞬时存储, 对 Java 内存回收造成很大压力.
FlatBuffers 实战
FlatBuffers 运作流程
首先来看一下 FlatBuffers 项目为开发者提供了哪些内容, 可以从官网下载源码, 其目录结构如下图:
如果要将 FlatBuffers 用到我们的项目中, 又需要哪些流程呢? 可以参考下面的流程图:
FlatBuffers 用法
就像 Parcel 和 Serializable 的序列化一样, FlatBuffers 的是使用方式上也比最传统的 JSON 序列化要复杂的多. 在实际上面开发中, 为了降低开发的难度, 提高开发效率, 我们会将源码编译成可植入的第三方库. 下面以 Java 环境为例, 来介绍 FlatBuffers 的简单使用方法. 读者可以到对应的 maven 仓库下载 https://mvnrepository.com/ .
现在, 假如我们拿到的 JSON 文件的格式是下面这样的:
- {
- "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"
- },
- ...
- ]
- }
注: 可以通过下面的链接来获取更完整的 JSON 对象
模式文件
我们需要准备一个 model 文件, 它定义了我们想要序列化 / 反序列化 的数据结构, 这个模式将被 flatc 用于创建 Java 模型以及从 JSON 到 FlatBuffer 二进制文件的转换.
现在, 我们所要做的所有事情就是创建 3 个表: ReposList,Repo 和 User, 并定义 root_type. 例如:
- 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 文件
接下来, 我们所需要做的就是将 repos_json.JSON 转换为 FlatBuffers 二进制文件, 并产生 Java 模型, 其可以以 Java 友好的方式表示我们的数据, 下面是转换的命令:
$ ./flatc -j -b repos_schema.fbs repos_json.JSON
如果没有任何报错, 将会生成如下 4 个文件:
- repos_json.bin (将被重命名为 repos_flat.bin)
- Repos/Repo.java
- Repos/ReposList.java
- Repos/User.java
测试
接下来, 我们可以使用 FlatBuffers 提供的 Java 库来处理在 Java 中直接处理这种数据格式, 此处使用需要使用到 .
- public class MainActivity extends AppCompatActivity {
- @Bind(R.id.tvFlat)
- TextView tvFlat;
- @Bind(R.id.tvJson)
- TextView tvJson;
- private RawDataReader rawDataReader;
- private ReposListJson reposListJson;
- private ReposList reposListFlat;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- ButterKnife.bind(this);
- rawDataReader = new RawDataReader(this);
- }
- @OnClick(R.id.btnJson)
- public void onJsonClick() {
- rawDataReader.loadJsonString(R.raw.repos_json).subscribe(new SimpleObserver<String>() {
- @Override
- public void onNext(String reposStr) {
- parseReposListJson(reposStr);
- }
- });
- }
- private void parseReposListJson(String reposStr) {
- long startTime = System.currentTimeMillis();
- reposListJson = new Gson().fromJson(reposStr, ReposListJson.class);
- for (int i = 0; i <reposListJson.repos.size(); i++) {
- RepoJson repo = reposListJson.repos.get(i);
- Log.d("FlatBuffers", "Repo #" + i + ", id:" + repo.id);
- }
- long endTime = System.currentTimeMillis() - startTime;
- tvJson.setText("Elements:" + reposListJson.repos.size() + ": load time:" + endTime + "ms");
- }
- @OnClick(R.id.btnFlatBuffers)
- public void onFlatBuffersClick() {
- rawDataReader.loadBytes(R.raw.repos_flat).subscribe(new SimpleObserver<byte[]>() {
- @Override
- public void onNext(byte[] bytes) {
- loadFlatBuffer(bytes);
- }
- });
- }
- private void loadFlatBuffer(byte[] bytes) {
- long startTime = System.currentTimeMillis();
- ByteBuffer bb = ByteBuffer.wrap(bytes);
- reposListFlat = frogermcs.io.flatbuffs.model.flat.ReposList.getRootAsReposList(bb);
- for (int i = 0; i < reposListFlat.reposLength(); i++) {
- Repo repos = reposListFlat.repos(i);
- Log.d("FlatBuffers", "Repo #" + i + ", id:" + repos.id());
- }
- long endTime = System.currentTimeMillis() - startTime;
- tvFlat.setText("Elements:" + reposListFlat.reposLength() + ": load time:" + endTime + "ms");
- }
- }
在上面的示例代码中, 有两个方法是比较核心的, 需要我们注意.
parseReposListJson(String reposStr) : 初始化 Gson 解析器并将 JSON 字符串转换为 Java 对象.
loadFlatBuffer(byte[] bytes) 将 bytes(即 repos_flat.bin 文件)转为 Java 对象.
耗时测试
下面我们来测试下 FlatBuffers 和传统的 JSON 在数据解析上的耗时, 我们以 4mb 的 JSON 文件为例.
如图, 可以发现 FlatBuffers 花了 1-5ms,JSON 花了大约 2000ms. 并且 FlatBuffers 期间 Android App 中没有 GC, 而在使用 JSON 时发生了很多次 GC, 测试的源码可以通过以下地址下载: FlatBuffers 耗时测试
参考: 在 Android 中使用 FlatBuffers FlatBuffers 官方 doc http://google.github.io/flatbuffers/
来源: https://juejin.im/post/5bfd5112f265da611c26abec