[译]FaceBook出品:开始用FlatBuffers替换老旧的Json吧

人们通过FaceBook关注家人朋友的动态更新,浏览他们上传的照片。我们的后端存储了组成社交媒介的数据结构。在移动手机端,我们不能拉取整个数据结构,所以只拉取某个节点和其相关的联系构成的树状结构。

下图说明了一条带图朋友圈的工作原理。John发了一条朋友圈,之后他的朋友进行了点赞和评论,左边的图是社交图,用于后端的关系展示。当Android应用查询这条朋友圈时,就可以拉取到树状结构,包括作者信息,反馈和图片信息(右图展示)。

tree structure

应用的关键在于展现和存储信息。使用SQLite数据库把全部数据持久化是不现实的,因为有很多方法从后端获取到某个节点的树状数据。其中一个可行的方法是使用Json来存储树状信息,但是UI展示数据时,需要额外的开销把Json解析成为Java对象,包括时间,我们在Android上使用Jackson Json解析器,有如下缺点:

  • 解析速度:20KB的Json数据流(一个典型的FaceBook数据包)需要35ms来解析,超出了UI帧的刷新频率16.6ms。所以,即使是加载硬盘缓存数据,也无法做到顺滑如丝的滚动效果。
  • 初始化解析:在解析开始前,Json解析器需要构造映射表,大概需要100~200ms,大幅降低了应用的启动速度。
  • 垃圾回收:在Json解析过程中会产生很多小对象,调查中发现,解析20KB的数据流时大概分配100KB的临时内存,给垃圾回收器带来巨大的压力。

我们希望有一个更好的存储格式来提高Android应用的体验。

FlatBuffers

在可行的替代格式的探索中,我们碰到了 FlatBuffers,Google的一个开源项目。FlatBuffers是协议缓冲区的一个演化,它包括了元数据,可以在不反序列化整个对象的前提下,直接取到这个对象中的某个子数据(例如上述的树状结构)。

想象一个简单的类,有四个变量:姓名,关系,配偶,朋友列表。其中配偶和朋友也是Person对象,所以构成了一个树状结构。下面简单地分析,如何使用FlatBuffers来表示一个人,John,他的配偶,Mary。

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

Show FlatBuffers

注意上述结构的几个方面:

  • 每个对象的描述分为两部分,左中心点的元数据(虚函数表),右中心点的真正数据;
  • 每个虚函数表中的格子代表一个变量,存储着真正数据的下标。例如,John的虚函数表的第一个格子有个1,指向在右中心点中,John名字的这个字节。
  • 对于对象变量,虚函数表的偏移量会指向子对象的中心点。例如,John的虚函数表的第三个格子(12)指向Mary的中心点(4)。
  • 在虚函数表中使用0表示没有其他数据可以展示了。

下面的代码片段展示了如何在这个数据结构中取得John的配偶信息:

// Root object position is normally stored at beginning of flatbuffer.
int johnPosition = FlatBufferHelper.getRootObjectPosition(flatBuffer);
int maryPosition = FlatBufferHelper.getChildObjectPosition(
   flatBuffer,
   johnPosition, // parent object position
   2 /* field number for spouse field */);
String maryName = FlatBufferHelper.getString(
   flatBuffer,
   johnPosition, // parent object position
   2 /* field number for name field */);

注意并没有涉及中间对象,节省了内存分配。更进一步,可以使用FlatBuffers数据进行存储,直接映射到内存中。这意味着我们只需要加载我们需要的部分数据,大大减少了过度的数据加载。

更重要的是,在读取变量之前不需要反序列整个对象。这减少了UI层和数据层之间的交互时间,提高了性能。

FlatBuffers的动态更新

数据是随着时间改变的,所以要更新FlatBuffers中存储的数据。因为FlatBuffers是设计为不可变的,所以没有直接的方法实现动态更新。解决方案是使用原始FlatBuffers数据进行比较。

FlatBuffer中每一块数据拥有独一无二的绝对位置,为了不需要每次都下载整个数据块,希望实现动态更新——例如关系的变更,下面举例子说明如何进行比较的:

  • John的好友状态在FlatBuffer中是虚函数表的第二个格子(6),为了改变好友状态,我们需要记录指向的数值现在是1(说明是朋友),而不是2(非朋友,但添加请求已发送)。

friend status

  • Mary的名字(”Mary”)在虚函数表的第13格子,相似地,修改Mary的名字需要把第13个格子指向新的字符串。

当查询FlatBuffer数据时,可以计算出数据的绝对位置,查看Mutation Buffer看是否有动态更新,如果没有则返回Base Buffer

扁平化Models

FlatBuffer可以作为应用的内存格式来处理数据存储和网络请求。它消除了从后端数据转化成前端UI展示的额外开销,也让我们转向于更为简洁的扁平化Models的架构,减少了UI层和数据层之间的复杂性。

使用Json作为数据格式时,为了用户体验,通常会在反序列化的时候做一些缓存,UI层和数据层之间添加了应用和网络的逻辑。
Json Architecture

iOS和桌面应用采用这种三层架构很普遍,在Android上就有缺点了:

  • 内存缓存意味着需要在内存中保存UI上的数据,很多Android设备都对应用有一个最大内存使用上限48MB或者更少。当开销增加时会触发垃圾回收机制,引起卡顿;
  • 应用逻辑需要处理内存缓存,UI,存储,特别是UI和存储相关的使用多线程处理,将会是个巨大的难题;
  • UI层的数据来源可能是缓存数据,网络数据,本地数据等等,当切换数据源时可能会引起UI层的过度绘制。

使用扁平化Models,UI层和数据层的交互就更为简单了,像下面图示:
Flat Models

  • UI层位于最高一层,使用标准Android的Cursor,在大多数Android应用中是通过路径进行存储的,保证了及时响应。
  • 应用和网络逻辑被数据层屏蔽了,在后台线程中运行逻辑处理并反映到数据层,使用标准的Android content provider通知,可以使UI层进行重绘。
  • 更好地分离了UI层与应用逻辑层,UI层仅仅需要对数据层的变更做出反应,应用逻辑层只需要把数据写到数据层中。UI层与应用逻辑层各自在工作线程做处理,互不干扰。
结论

FlatBuffers减少了UI层和数据层之间数据转换的花销,同样驱动了应用的架构升级。动态更新策略可以随时了解后端数据的更新,而本地状态统统存储在一个小小的数据结构中,大大减少了额外的花销。

我们用六个月时间完成了大部分Facebook的Android应用使用FlatBuffers作为存储格式,发现:
- 加载本地缓存的时间从35ms下降到4ms;
- 临时内存分配减少大约75%;
- 冷启动时间减少10~15%
- 存储空间减少了15%

很兴奋使用一个新的数据结构,让用户在看到朋友的动态如此迅速,感谢FlatBuffers

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用说明: 1.易语言模块和使用例子在Release目录下 2.为了节省打包大小,删除了Visual Studio的配置,重新编的话 选择Release x86即可 3.官方库若有更新可直接替换include文件夹,重新编即可(官方git: https://github.com/Tencent/rapidjson/)(官方文档: http://rapidjson.org/zh-cn/) 封装日志: 1.0.0.9版-2019.5.6 1) 升级 rapidjson库到官方最新版本(2019.4.15) 1.0.0.8版-2018.11.22 1)  修复 gstrlen函数 pop顺序错误问题. 2)  修复 win10环境下【SAX解析】路径深度到达3时,路径未以0结尾问题. 1.0.0.7版-2018.11.17 1)  修复 NumConversion.h中 StrToInt64函数 换异常问题。(所有取长整数值,若类型是文本型,自动换时会调用该函数) 2)  升级 rapidjson库到官方最新版本(2018.10.8) 1.0.0.6版-2018.10.8 1)  修复 rapidjson_dll_ec.e RJ生成W.创建对象和RJ生成W.创建数组 键名为空时,生成异常问题 2)  优化 取数值时,若为文本型,则强为对应数值返回. 3)  添加 通配_取xx值配置 系列 (作用:取值,需要提供一个默认值,若节点存在则返回节点值,不存在则添加默认值) 4)  添加 通配_置xx值 系列 (作用:可多路径生成json) 5)  添加 pointer_erase_path 函数 (作用:删除某个节点) 6)  添加 pointer_is_exist 函数 (作用: 查询节点是否存在) 7)  添加 几个性能优化过的辅助函数,实现在rapidjson_dll_ec.e(辅助功能) 8)  封装 zlib部分解压缩功能,实现在auxiliary.cpp 9)  更新 易语言模块和使用例子 1.0.0.5版-2018.9.26 1)  添加SAX解析方式,实现在sax.cpp 2)  同步更新使用例子(rapidjson.e) 1.0.0.4版-2018.9.9 1)  修复解析时传入空指针导致奔溃问题 2)  修复一些隐患 3)  增加object_get_key函数(取对象成员键名) 4)  增加double_to_string函数(双精度到文本 Grisu2算法),实现在auxiliary.cpp 5)  同步更新易语言模块和使用例子 1.0.0.3版-2018.8.30 1)  修复object_get_int和get_path_type返回错误问题(测试的时候加了个取字符串长度的代码,忘记删掉了- -)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值