在Android中使用Protocol Buffers(下篇)
本文来自网易云社区。
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();
}
编码字符串的过程如下:
- 对字符串进行编码,比如 UTF-8 ,编码后的数据保存在另一个 ByteBuffer 中。
- 在可用空间的结尾处添加值为 0 的byte。
- 将第 1 步中创建的 ByteBuffer 作为一个字节数组添加到 FlatBufferBuilder 的 ByteBuffer 中。这里不是逐个元素,也就是字节,添加,而是将 ByteBuffer 整体一次性添加,以保证字符串中各个字节的相对顺序不会被颠倒过来,这一点与我们前面在AddressBook 中看到的稍有区别。
编码后的字符串将有如下的内存分布:
FlatBuffers编码对象
对象的编码与数组的编码有点类似。编码对象的过程为:
- 先执行 startObject(),创建 vtable并初始化,记录对象的字段个数及对象数据的起始位置,并设置nested,指示对象编码的开始。
- 然后为对象逐个添加每个字段的值。
最后执行 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;
}
结束对象编码的过程比较有意思:
- 在可用空间的结尾处添加值为 0 的int。
- 记录下当前的offset值 vtableloc,也就是 ByteBuffer中已经保存的数据的长度。
- 编码vtable。vtable用于记录对象每个字段的存储位置,在为对象添加字段时会被更新。在这里会用 vtableloc - vtable[i],找到每个对象的保存位置相对于对象起始位置的偏移,并将这个偏移量保存到ByteBuffer中。
- 记录对象所有字段的总长度,包含对象开始初值为0的int数据。
- 记录元数据的长度。这包括vtable的长度,记录 对象所有字段的总长度 的short型值,以及这个长度本身所消耗的存储空间。
- 查找是否有一个vtable与正在创建的这个一致。
- 找到了匹配的vtable,则清除创建的元数据。第 1 步中放0的那个位置的值,被更新为找到的vtable相对于对象的数据起始位置的偏移。
- 没有找到匹配的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;
}
过程大体为:
- 获得对应字段在对象中的偏移位置。
- 根据字段的偏移位置及对象的原点位置计算出对象的位置。
- 通过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。
相关阅读:
在Android中使用Protocol Buffers(上篇)
网易云新用户大礼包:https://www.163yun.com/gift
本文来自网易实践者社区,经作者韩鹏飞授权发布。
在Android中使用Protocol Buffers(下篇)的更多相关文章
- 在Android中使用Protocol Buffers(上篇)
本文来自网易云社区. 总览 先来看一下 FlatBuffers 项目已经为我们提供了什么,而我们在将 FlatBuffers 用到我们的项目中时又需要做什么的整体流程.如下图: 在使用 FlatBuf ...
- 在Android中使用Protocol Buffers(中篇)
本文来自网易云社区. FlatBuffers 编码原理 FlatBuffers的Java库只提供了如下的4个类: ./com/google/flatbuffers/Constants.java ./c ...
- 如何在 PHP 中处理 Protocol Buffers 数据
Protocol Buffers是谷歌定义的一种跨语言.跨平台.可扩展的数据传输及存储的协议,因为将字段协议分别放在传输两端,传输数据中只包含数据本身,不需要包含字段说明,所以传输数据量小,解析效率高 ...
- 在Android中使用FlatBuffers(下篇)
本文来自网易云社区. FlatBuffers编码数组 编码数组的过程如下: 先执行 startVector(),这个方法会记录数组的长度,处理元素的对齐,准备足够的空间,并设置nested,用于指示记 ...
- 【笔记】golang中使用protocol buffers的底层库直接解码二进制数据
背景 一个简单的代理程序,发现单核QPS达到2万/s左右就上不去了,40%的CPU消耗在pb的decode/encode上面. 于是我想,对于特定的场景,直接从[]byte中取出字段,而不用完全的把整 ...
- golang gin框架中使用protocol buffers和JSON两种协议
首先,我使用protobuf作为IDL,然后提供HTTP POST + JSON BODY的方式来发送请求. 能不能使用HTTTP POST + PB序列化后的二进制BODY呢? 做了一下尝试,非常简 ...
- 【新手笔记】golang中使用protocol buffers 3
主要参考了这篇帖子:https://segmentfault.com/a/1190000009277748 1.下载windows版本的PB https://github.com/protocolbu ...
- 在Android中使用FlatBuffers(中篇)
本文来自网易云社区. FlatBuffers.Protobuf及JSON对比测试 FlatBuffers相对于Protobuf的表现又如何呢?这里我们用数据说话,对比一下FlatBuffers格式.J ...
- 在Android中使用FlatBuffers(上篇)
本文来自网易云社区. 总览 先来看一下 FlatBuffers 项目已经为我们提供了什么,而我们在将 FlatBuffers 用到我们的项目中时又需要做什么的整体流程.如下图: 在使用 FlatBuf ...
随机推荐
- spring boot项目多环境配置文件设置
具体做法: 不同环境的配置设置一个配置文件,例如:dev环境下的配置配置在application-dev.properties中:prod环境下的配置配置在application-prod.prope ...
- vue backup
为了解决跨域问题 目前在项目,目录下创建了vue.config.js文件,目前是指向别外的一个端口,之前是记得在express后端进行端口指定的,先备注一下,后期跟进 npm ls | grep ax ...
- java:Maven构建项目速度太慢的解决办法,以及报错Retrieving archetypes:' has encountered a problem
如果报错信息如下: Retrieving archetypes:' has encountered a problemAn internal error occurred during:"R ...
- 分享知识-快乐自己:解决 Maven 无法下载 fastdfs-client-java 依赖。
因为fastdfs-client-java-1.27-SNAPSHOT.jar这个依赖包在maven中央仓库是没有的. 需要自己编译源码成jar本地安装到maven 的本地仓库,安装完以后就能正常引用 ...
- 实现html表头固定,表格内的信息向上滚动
效果如下: <!doctype html>Table header header two fuck 1 fuck 2 fuck 1 fuck 2 fuck 1 fuck 2 fuck 1 ...
- 使用kill命令终止进程shell脚本
因有的程序使用kill才能结束掉进程,没有关闭脚本,以我司的服务为例,服务名叫asset-server服务,只有启动脚本,自编写关闭脚本,及重启动脚本. 关闭服务脚本. vim asset-shutd ...
- SSH Secure Shell 的使用
一,安装Secure Shell ,之后会出现SSH Secure Shell client 和SSH Secure File Transfer client 两个快捷方式. 需要服务器的IP地址,用 ...
- oracle数据库规划建议
之前负责的项目有用到oracle的,oracle dba给过一些建议,自己整理了一下,写再这里做个备忘 数据库需求分析: 1. 创建的数据库名称为maildb,并且字符集为UTF8. 2. 提供可连接 ...
- 配置 VS Code 调试 PHP
配置 VS Code 调试 PHP 1.下载 xampp 集成服务器wampserver3.1.0-Apache2.4.7_PHP5.6.3-7.0.23-7.1.19_MySQL5.7.19_Mar ...
- Access中一句查询代码实现Excel数据导入导出
摘 要:用一句查询代码,写到vba中实现Excel数据导入导出,也可把引号中的SQL语句直接放到查询分析器中执行正 文: 导入数据(导入数据时第一行必须是字段名): DoCmd.RunSQL &quo ...