本文来自网易云社区

FlatBuffers编码数组

编码数组的过程如下:

先执行 startVector(),这个方法会记录数组的长度,处理元素的对齐,准备足够的空间,并设置nested,用于指示记录的开始。 然后逐个添加元素。 最后 执行 endVector(),将nested复位,并记录数组的长度。

  1. public void startVector(int elem_size, int num_elems, int alignment) {
  2. notNested();
  3. vector_num_elems = num_elems;
  4. prep(SIZEOF_INT, elem_size * num_elems);
  5. prep(alignment, elem_size * num_elems); // Just in case alignment > int.
  6. nested = true;
  7. }
  8. public int endVector() {
  9. if (!nested)
  10. throw new AssertionError("FlatBuffers: endVector called without startVector");
  11. nested = false;
  12. putInt(vector_num_elems);
  13. return offset();
  14. }

我们前面的AddressBook例子中有如下这样的生成代码:

  1. public static int createPersonVector(FlatBufferBuilder builder, int[] data) {
  2. builder.startVector(4, data.length, 4);
  3. for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]);
  4. return builder.endVector();
  5. }

编码后的数组将有如下的内存分布:

其中的Vector Length为4字节的int型值。

FlatBuffers编码字符串

FlatBufferBuilder 创建字符串的过程如下:

  1. public int createString(CharSequence s) {
  2. int length = s.length();
  3. int estimatedDstCapacity = (int) (length * encoder.maxBytesPerChar());
  4. if (dst == null || dst.capacity() < estimatedDstCapacity) {
  5. dst = ByteBuffer.allocate(Math.max(128, estimatedDstCapacity));
  6. }
  7. dst.clear(); CharBuffer src = s instanceof CharBuffer ? (CharBuffer) s :
  8. CharBuffer.wrap(s);
  9. CoderResult result = encoder.encode(src, dst, true);
  10. if (result.isError()) {
  11. try {
  12. result.throwException();
  13. } catch (CharacterCodingException x) {
  14. throw new Error(x);
  15. }
  16. }
  17. dst.flip();
  18. return createString(dst);
  19. }
  20. public int createString(ByteBuffer s) {
  21. int length = s.remaining();
  22. addByte((byte)0);
  23. startVector(1, length, 1);
  24. bb.position(space -= length);
  25. bb.put(s);
  26. return endVector();
  27. }
  28. public int createByteVector(byte[] arr) {
  29. int length = arr.length;
  30. startVector(1, length, 1);
  31. bb.position(space -= length);
  32. bb.put(arr);
  33. return endVector();
  34. }

编码字符串的过程如下:

  1. 对字符串进行编码,比如 UTF-8 ,编码后的数据保存在另一个 ByteBuffer 中。
  2. 在可用空间的结尾处添加值为 0 的byte。
  3. 将第 1 步中创建的 ByteBuffer 作为一个字节数组添加到 FlatBufferBuilder 的 ByteBuffer 中。这里不是逐个元素,也就是字节,添加,而是将 ByteBuffer 整体一次性添加,以保证字符串中各个字节的相对顺序不会被颠倒过来,这一点与我们前面在AddressBook 中看到的稍有区别。

编码后的字符串将有如下的内存分布:

FlatBuffers编码对象

对象的编码与数组的编码有点类似。编码对象的过程为:

  1. 先执行 startObject(),创建 vtable并初始化,记录对象的字段个数及对象数据的起始位置,并设置nested,指示对象编码的开始。
  2. 然后为对象逐个添加每个字段的值。
  3. 最后执行 endObject() 结束对象的编码。

    1. public void startObject(int numfields) {
    2. notNested();
    3. if (vtable == null || vtable.length < numfields) vtable = new int[numfields];
    4. vtable_in_use = numfields;
    5. Arrays.fill(vtable, 0, vtable_in_use, 0);
    6. nested = true;
    7. object_start = offset();
    8. }
    9. public int endObject() {
    10. if (vtable == null || !nested)
    11. throw new AssertionError("FlatBuffers: endObject called without startObject");
    12. addInt(0);
    13. int vtableloc = offset();
    14. // Write out the current vtable.
    15. for (int i = vtable_in_use - 1; i >= 0 ; i--) {
    16. // Offset relative to the start of the table.
    17. short off = (short)(vtable[i] != 0 ? vtableloc - vtable[i] : 0);
    18. addShort(off);
    19. }
    20. final int standard_fields = 2; // The fields below:
    21. addShort((short)(vtableloc - object_start));
    22. addShort((short)((vtable_in_use + standard_fields) * SIZEOF_SHORT));
    23. // Search for an existing vtable that matches the current one.
    24. int existing_vtable = 0;
    25. outer_loop:
    26. for (int i = 0; i < num_vtables; i++) {
    27. int vt1 = bb.capacity() - vtables[i];
    28. int vt2 = space;
    29. short len = bb.getShort(vt1);
    30. if (len == bb.getShort(vt2)) {
    31. for (int j = SIZEOF_SHORT; j < len; j += SIZEOF_SHORT) {
    32. if (bb.getShort(vt1 + j) != bb.getShort(vt2 + j)) {
    33. continue outer_loop;
    34. }
    35. }
    36. existing_vtable = vtables[i];
    37. break outer_loop;
    38. }
    39. }
    40. if (existing_vtable != 0) {
    41. // Found a match:
    42. // Remove the current vtable.
    43. space = bb.capacity() - vtableloc;
    44. // Point table to existing vtable.
    45. bb.putInt(space, existing_vtable - vtableloc);
    46. } else {
    47. // No match:
    48. // Add the location of the current vtable to the list of vtables.
    49. if (num_vtables == vtables.length) vtables = Arrays.copyOf(vtables, num_vtables * 2);
    50. vtables[num_vtables++] = offset();
    51. // Point table to current vtable.
    52. bb.putInt(bb.capacity() - vtableloc, offset() - vtableloc);
    53. }
    54. nested = false;
    55. return vtableloc;
    56. }

    结束对象编码的过程比较有意思:

  4. 在可用空间的结尾处添加值为 0 的int。
  5. 记录下当前的offset值 vtableloc,也就是 ByteBuffer中已经保存的数据的长度。
  6. 编码vtable。vtable用于记录对象每个字段的存储位置,在为对象添加字段时会被更新。在这里会用 vtableloc - vtable[i],找到每个对象的保存位置相对于对象起始位置的偏移,并将这个偏移量保存到ByteBuffer中。
  7. 记录对象所有字段的总长度,包含对象开始初值为0的int数据。
  8. 记录元数据的长度。这包括vtable的长度,记录 对象所有字段的总长度 的short型值,以及这个长度本身所消耗的存储空间。
  9. 查找是否有一个vtable与正在创建的这个一致。
  10. 找到了匹配的vtable,则清除创建的元数据。第 1 步中放0的那个位置的值,被更新为找到的vtable相对于对象的数据起始位置的偏移。
  11. 没有找到匹配的vtable。记下vtable的位置,第 1 步中放0的那个位置的值,被更新为新创建的vtable相对于对象的数据起始位置的偏移。

就像C++中的vtable,这里的vtable也是针对类创建的,而不是对象。

编码后的对象有如下的内存分布:

图中值为0的那个位置的值实际不是0,它指向vtable,图中是指向在创建对象时创建的vtable,但它也可以相同类已经存在的vtable。

结束编码

编码数据之后,需要执行 FlatBufferBuilder 的 finish() 结束编码:

  1. public int offset() {
  2. return bb.capacity() - space;
  3. }
  4. public void addOffset(int off) {
  5. prep(SIZEOF_INT, 0); // Ensure alignment is already done.
  6. assert off <= offset();
  7. off = offset() - off + SIZEOF_INT;
  8. putInt(off);
  9. }
  10. public void finish(int root_table) {
  11. prep(minalign, SIZEOF_INT);
  12. addOffset(root_table);
  13. bb.position(space);
  14. finished = true;
  15. }
  16. public void finish(int root_table, String file_identifier) {
  17. prep(minalign, SIZEOF_INT + FILE_IDENTIFIER_LENGTH);
  18. if (file_identifier.length() != FILE_IDENTIFIER_LENGTH)
  19. throw new AssertionError("FlatBuffers: file identifier must be length " +
  20. FILE_IDENTIFIER_LENGTH);
  21. for (int i = FILE_IDENTIFIER_LENGTH - 1; i >= 0; i--) {
  22. addByte((byte)file_identifier.charAt(i));
  23. }
  24. finish(root_table);
  25. }

这个方法主要是记录根对象的位置。给 finish() 传入的的根对象的位置是相对于ByteBuffer结尾处的偏移,但是在 addOffset() 中,这个偏移会被转换为相对于整个数据块开始处的偏移。计算off值时,最后加的SIZEOF_INT是要给后面放入的off留出空间。

整个编码后的数据有如下的内存分布:

FlatBuffers 解码原理

这里我们通过一个生成的比较简单的类 PhoneNumber 来了解FlatBuffers的解码。

  1. public static PhoneNumber getRootAsPhoneNumber(ByteBuffer _bb) {
  2. return getRootAsPhoneNumber(_bb, new PhoneNumber());
  3. }
  4. public static PhoneNumber getRootAsPhoneNumber(ByteBuffer _bb, PhoneNumber obj) {
  5. _bb.order(ByteOrder.LITTLE_ENDIAN);
  6. return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb));
  7. }
  8. public void __init(int _i, ByteBuffer _bb) {
  9. bb_pos = _i;
  10. bb = _bb;
  11. }
  12. public PhoneNumber __assign(int _i, ByteBuffer _bb) {
  13. __init(_i, _bb);
  14. return this;
  15. }

创建对象的时候,会初始化 bb 为保存有对象数据的ByteBuffer,bb_pos 为对象数据在ByteBuffer中的偏移。在 getRootAsPhoneNumber() 中会从 ByteBuffer的position处获取根对象的偏移,并加上position,以计算出对象在ByteBuffer中的位置。

通过生成的PhoneNumber类中的number()、type()两个方法来看, FlatBuffers 中是怎么访问成员的:

  1. public String number() {
  2. int o = __offset(4);
  3. return o != 0 ? __string(o + bb_pos) : null;
  4. }
  5. public int type() {
  6. int o = __offset(6);
  7. return o != 0 ? bb.getInt(o + bb_pos) : 0;
  8. }

过程大体为:

  1. 获得对应字段在对象中的偏移位置。
  2. 根据字段的偏移位置及对象的原点位置计算出对象的位置。
  3. 通过ByteBuffer等提供的一些方法得到字段的值。

计算字段相对于对象原点位置的偏移的方法 __offset(4) 在com.google.flatbuffers.Table中定义:

  1. protected int __offset(int vtable_offset) {
  2. int vtable = bb_pos - bb.getInt(bb_pos);
  3. return vtable_offset < bb.getShort(vtable) ? bb.getShort(vtable + vtable_offset) : 0;
  4. }

在这个方法中,先是根据对象的原点处保存的vtable的偏移得到vtable的位置,然后在从vtable中获取对象字段相对于对象原点位置的偏移。

得到字符串字段的过程如下:

  1. protected String __string(int offset) {
  2. CharsetDecoder decoder = UTF8_DECODER.get();
  3. decoder.reset();
  4. offset += bb.getInt(offset);
  5. ByteBuffer src = bb.duplicate().order(ByteOrder.LITTLE_ENDIAN);
  6. int length = src.getInt(offset);
  7. src.position(offset + SIZEOF_INT);
  8. src.limit(offset + SIZEOF_INT + length);
  9. int required = (int)((float)length * decoder.maxCharsPerByte());
  10. CharBuffer dst = CHAR_BUFFER.get();
  11. if (dst == null || dst.capacity() < required) {
  12. dst = CharBuffer.allocate(required);
  13. CHAR_BUFFER.set(dst);
  14. }
  15. dst.clear();
  16. try {
  17. CoderResult cr = decoder.decode(src, dst, true);
  18. if (!cr.isUnderflow()) {
  19. cr.throwException();
  20. }
  21. } catch (CharacterCodingException x) {
  22. throw new Error(x);
  23. }
  24. return dst.flip().toString();
  25. }

了解了前面字符串编码的过程之后,相信也不难了解这里解码字符串的过程,这里完全是那个过程的相反过程。

如我们所见,FlatBuffers编码后的数据其实无需解码,只要通过生成的Java类对这些数据进行解释就可以了。

FlatBuffers的原理大体如此。

Done。

相关阅读:

在Android中使用FlatBuffers(上篇)

在Android中使用FlatBuffers(中篇)

在Android中使用FlatBuffers(下篇)

网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易云社区,经作者韩鹏飞授权发布。

在Android中使用FlatBuffers(下篇)的更多相关文章

  1. 在Android中使用FlatBuffers(中篇)

    本文来自网易云社区. FlatBuffers.Protobuf及JSON对比测试 FlatBuffers相对于Protobuf的表现又如何呢?这里我们用数据说话,对比一下FlatBuffers格式.J ...

  2. 在Android中使用FlatBuffers(上篇)

    本文来自网易云社区. 总览 先来看一下 FlatBuffers 项目已经为我们提供了什么,而我们在将 FlatBuffers 用到我们的项目中时又需要做什么的整体流程.如下图: 在使用 FlatBuf ...

  3. 在Android中使用Protocol Buffers(下篇)

    本文来自网易云社区. FlatBuffers编码数组 编码数组的过程如下: 先执行 startVector(),这个方法会记录数组的长度,处理元素的对齐,准备足够的空间,并设置nested,用于指示记 ...

  4. [转] Android中的设计模式-备忘录模式

    转自Android中的设计模式-备忘录模式 定义 备忘录设计模式的定义就是把对象的状态记录和管理委托给外界处理,用以维持自己的封闭性. 比较官方的定义 备忘录模式(Memento Pattern)又叫 ...

  5. Android中measure过程、WRAP_CONTENT详解以及 xml布局文件解析流程浅析

    转自:http://www.uml.org.cn/mobiledev/201211221.asp 今天,我着重讲解下如下三个内容: measure过程 WRAP_CONTENT.MATCH_PAREN ...

  6. 在Android中使用Protocol Buffers(上篇)

    本文来自网易云社区. 总览 先来看一下 FlatBuffers 项目已经为我们提供了什么,而我们在将 FlatBuffers 用到我们的项目中时又需要做什么的整体流程.如下图: 在使用 FlatBuf ...

  7. Android 中View的绘制机制源代码分析 一

    尊重原创: http://blog.csdn.net/yuanzeyao/article/details/46765113 差点儿相同半年没有写博客了,一是由于工作比較忙,二是认为没有什么内容值得写, ...

  8. Android中的LinearLayout布局

    LinearLayout : 线性布局 在一般情况下,当有很多控件需要在一个界面列出来时,我们就可以使用线性布局(LinearLayout)了,  线性布局是按照垂直方向(vertical)或水平方向 ...

  9. Android中BroadcastReceiver的两种注册方式(静态和动态)详解

    今天我们一起来探讨下安卓中BroadcastReceiver组件以及详细分析下它的两种注册方式. BroadcastReceiver也就是"广播接收者"的意思,顾名思义,它就是用来 ...

随机推荐

  1. Kafka- Kafka架构功能

    Kafka是一个高吞吐量的分布式消息系统,一个分布式的发布-订阅消息系统.Kafka是一种快速,可拓展的,设计内在就是分布式的,分区的可复制的提交日志服务. Apache Kafka与传统消息系统相比 ...

  2. vim乱码的解决

    解决vim文件乱码,打开文件乱码,菜单,提示信息乱码: 有四个跟字符编码方式有关的选项,encoding.fileencoding.fileencodings.termencoding 在linux中 ...

  3. js string.format 方法

    String.prototype.format = function(args) { var result = this; if (arguments.length > 0) { if (arg ...

  4. 将jsp页面内容保存到excel(转)

    在此,强调一下搜索时关键词的重要性,这样一下子可以定位到文章,否则处于盲人摸象,毫无目的尴尬境地.本篇就是通过export jsp to excel找到的. 原文地址:How to Export We ...

  5. 为什么jdk1.8不支持sql.append,该如何解决

    StringBuilder sql = new StringBuilder("SELECT ID,COMMAND,DESCRIPTION,CONTENT FROM message where ...

  6. POJ-1564 dfs

    #include"cstring" #include"cstdio" +; int nux[maxn]; int nua[maxn];//解的集合 int t; ...

  7. bzoj 4504: K个串 可持久化线段树+堆

    题目: Description 兔子们在玩k个串的游戏.首先,它们拿出了一个长度为n的数字序列,选出其中的一 个连续子串,然后统计其子串中所有数字之和(注意这里重复出现的数字只被统计一次). 兔子们想 ...

  8. Poj1012_Joseph

    一.Description The Joseph's problem is notoriously known. For those who are not familiar with the ori ...

  9. [转]unity3d中创建双面材质

    在其它三维软件中设置好的双面材质导入到unity3d中就失去了效果,不过我们可以通过自定义材质来在unity3d中实现双面材质的效果.步骤如下:1.在资源库中新建一新shader:代码如下: Shad ...

  10. MongoDB优化之二:常见优化方法

    四个方面进行 cpu/io 方面的优化处理: 1.集群架构上进行读写分离.所有查询优先考虑在从库上读取,写操作在主库上执行.避免主库混合读写压力过大,也减少主库上读写记录的锁冲突. connectio ...