博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
在Android中使用FlatBuffers(下篇)
阅读量:5375 次
发布时间:2019-06-15

本文共 8908 字,大约阅读时间需要 29 分钟。

本文来自。

 

FlatBuffers编码数组

 

编码数组的过程如下:

 

 

 

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

 

public void startVector(int elem_size, int num_elems, int alignment) {        notNested();        vector_num_elems = num_elems;        prep(SIZEOF_INT, elem_size * num_elems);        prep(alignment, elem_size * num_elems); // Just in case alignment > int.        nested = true;    }    public int endVector() {        if (!nested)            throw new AssertionError("FlatBuffers: endVector called without startVector");        nested = false;        putInt(vector_num_elems);        return offset();    }

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

 

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

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

 

 

 

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

 

FlatBuffers编码字符串

 

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

 

public int createString(CharSequence s) {        int length = s.length();        int estimatedDstCapacity = (int) (length * encoder.maxBytesPerChar());        if (dst == null || dst.capacity() < estimatedDstCapacity) {            dst = ByteBuffer.allocate(Math.max(128, estimatedDstCapacity));        }        dst.clear();   CharBuffer src = s instanceof CharBuffer ? (CharBuffer) s :            CharBuffer.wrap(s);        CoderResult result = encoder.encode(src, dst, true);        if (result.isError()) {            try {                result.throwException();            } catch (CharacterCodingException x) {                throw new Error(x);            }        }        dst.flip();        return createString(dst);    }    public int createString(ByteBuffer s) {        int length = s.remaining();        addByte((byte)0);        startVector(1, length, 1);        bb.position(space -= length);        bb.put(s);        return endVector();    }    public int createByteVector(byte[] arr) {        int length = arr.length;        startVector(1, length, 1);        bb.position(space -= length);        bb.put(arr);        return endVector();    }

编码字符串的过程如下:

 

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

 

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

 

 

FlatBuffers编码对象

 

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

 

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

    public void startObject(int numfields) {     notNested();     if (vtable == null || vtable.length < numfields) vtable = new int[numfields];     vtable_in_use = numfields;     Arrays.fill(vtable, 0, vtable_in_use, 0);     nested = true;     object_start = offset(); } public int endObject() {     if (vtable == null || !nested)         throw new AssertionError("FlatBuffers: endObject called without startObject");     addInt(0);     int vtableloc = offset();     // Write out the current vtable.     for (int i = vtable_in_use - 1; i >= 0 ; i--) {         // Offset relative to the start of the table.         short off = (short)(vtable[i] != 0 ? vtableloc - vtable[i] : 0);         addShort(off);     }     final int standard_fields = 2; // The fields below:     addShort((short)(vtableloc - object_start));     addShort((short)((vtable_in_use + standard_fields) * SIZEOF_SHORT));     // Search for an existing vtable that matches the current one.     int existing_vtable = 0;     outer_loop:     for (int i = 0; i < num_vtables; i++) {         int vt1 = bb.capacity() - vtables[i];         int vt2 = space;         short len = bb.getShort(vt1);         if (len == bb.getShort(vt2)) {             for (int j = SIZEOF_SHORT; j < len; j += SIZEOF_SHORT) {                 if (bb.getShort(vt1 + j) != bb.getShort(vt2 + j)) {                     continue outer_loop;                 }             }             existing_vtable = vtables[i];             break outer_loop;         }     }     if (existing_vtable != 0) {         // Found a match:         // Remove the current vtable.         space = bb.capacity() - vtableloc;         // Point table to existing vtable.         bb.putInt(space, existing_vtable - vtableloc);     } else {         // No match:         // Add the location of the current vtable to the list of vtables.         if (num_vtables == vtables.length) vtables = Arrays.copyOf(vtables, num_vtables * 2);         vtables[num_vtables++] = offset();         // Point table to current vtable.         bb.putInt(bb.capacity() - vtableloc, offset() - vtableloc);     }     nested = false;     return vtableloc; }

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

  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() 结束编码:

 

public int offset() {        return bb.capacity() - space;    }    public void addOffset(int off) {        prep(SIZEOF_INT, 0);  // Ensure alignment is already done.        assert off <= offset();        off = offset() - off + SIZEOF_INT;        putInt(off);    }    public void finish(int root_table) {        prep(minalign, SIZEOF_INT);        addOffset(root_table);        bb.position(space);        finished = true;    }    public void finish(int root_table, String file_identifier) {        prep(minalign, SIZEOF_INT + FILE_IDENTIFIER_LENGTH);        if (file_identifier.length() != FILE_IDENTIFIER_LENGTH)            throw new AssertionError("FlatBuffers: file identifier must be length " +                                     FILE_IDENTIFIER_LENGTH);        for (int i = FILE_IDENTIFIER_LENGTH - 1; i >= 0; i--) {            addByte((byte)file_identifier.charAt(i));        }        finish(root_table);    }

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

 

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

 

 

FlatBuffers 解码原理

 

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

 

public static PhoneNumber getRootAsPhoneNumber(ByteBuffer _bb) {        return getRootAsPhoneNumber(_bb, new PhoneNumber());    }    public static PhoneNumber getRootAsPhoneNumber(ByteBuffer _bb, PhoneNumber obj) {        _bb.order(ByteOrder.LITTLE_ENDIAN);        return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb));    }    public void __init(int _i, ByteBuffer _bb) {        bb_pos = _i;        bb = _bb;    }    public PhoneNumber __assign(int _i, ByteBuffer _bb) {        __init(_i, _bb);        return this;    }

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

 

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

 

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

过程大体为:

 

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

 

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

 

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

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

 

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

protected String __string(int offset) {    CharsetDecoder decoder = UTF8_DECODER.get();    decoder.reset();    offset += bb.getInt(offset);    ByteBuffer src = bb.duplicate().order(ByteOrder.LITTLE_ENDIAN);    int length = src.getInt(offset);    src.position(offset + SIZEOF_INT);    src.limit(offset + SIZEOF_INT + length);    int required = (int)((float)length * decoder.maxCharsPerByte());    CharBuffer dst = CHAR_BUFFER.get();    if (dst == null || dst.capacity() < required) {      dst = CharBuffer.allocate(required);      CHAR_BUFFER.set(dst);    }    dst.clear();    try {      CoderResult cr = decoder.decode(src, dst, true);      if (!cr.isUnderflow()) {        cr.throwException();      }    } catch (CharacterCodingException x) {      throw new Error(x);    }    return dst.flip().toString();  }

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

 

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

 

FlatBuffers的原理大体如此。

 

Done。

 

 

相关阅读:

网易云新用户大礼包:

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

 

转载于:https://www.cnblogs.com/163yun/p/9487336.html

你可能感兴趣的文章
php环境搭建脚本
查看>>
MES架构
查看>>
hdu 2767(tarjan)
查看>>
sklearn之分类模型混淆矩阵和分类报告
查看>>
MySQL各存储引擎
查看>>
项目--简单导出CSV文件
查看>>
Oracle session相关数据字典(一)
查看>>
BZOJ 3399 [Usaco2009 Mar]Sand Castle城堡(贪心)
查看>>
获取元素属性get_attribute
查看>>
在Flex中用Validator检测数字、字符串、Email.
查看>>
[leetcode]4Sum
查看>>
POJ1062 昂贵的聘礼
查看>>
【零基础学习iOS开发】【02-C语言】08-基本运算
查看>>
Java 将指定字符串连接到此字符串的结尾 concat()
查看>>
Hibernate Criterion
查看>>
Python知识
查看>>
我们为什么要搞长沙.NET技术社区(三)
查看>>
杭电acm Cake
查看>>
js函数中this的指向
查看>>
c++ 引用方式传递数组
查看>>