接着上一篇文章的例子,本篇主要研究protobuf如何从字节流中解析并生java对象的。之前的文章也介绍过如何从文件中读取出一个对象的:
Message testMessage = Message.parseFrom(new FileInputStream("testmessage.txt"));
通过阅读parseFrom这个方法的源码,将它的流程简要概括如下:
- 从InputStream中新建CodedInputStream对象
- 从CodedInputStream读取下一个tag,即field_num和wire type的组合
- tag告知了wire type, 也就知道后面的字节是什么类型,不同类型有不同的读取逻辑,同时tag还告知了field_num,因此读取成功后赋值给对应的字段。
- 然后循环2-3步,直到末尾结束返回Message对象
实际源码解析
以下列出了解析流程中的最核心的代码(java):
boolean done = false; while (!done) { int tag = input.readTag(); switch (tag) { case 0: // 到达末尾,跳出循环 done = true; break; default: { // 如果遇到未知的字段,在java的代码里则存储到unknownFields,可以供调用者使用,其它语言就不一定了,具体可以参考其它语言的实现 if (!parseUnknownFieldProto3( input, unknownFields, extensionRegistry, tag)) { done = true; } break; } case 8: { // tag为8 即为 00001000, field_num是1, wire type是0,即代表后面的字节是整型数字a的内容 a_ = input.readInt32(); // 读取后面的整型内容,具体怎么读取的后面会介绍 break; } case 18: { // tag为18 即为 00010010, field_num是2, wire type是2,即代表后面的字节是字符串query的内容 String s = input.readStringRequireUtf8(); query_ = s; break; } } }
那现在来看看读取整型变量的主要源码,即上面代码中input.readInt32()的逻辑
long result = 0; for (int shift = 0; shift < 64; shift += 7) { // 每次移动7位,为什么是7位,因为一个字节是8位,去掉首bit就只有7位有效了 final byte b = readRawByte(); result |= (long) (b & 0x7F) << shift; // 取出字节的后7位并往左移7位,上一篇文章介绍过,整型是倒过来存储的 if ((b & 0x80) == 0) { // 如果当前字节的首bit是0,则意味着后面的字节不属于这个整型的一部分了 return result; } }
看起来和上一篇文章说的逻辑一样,接下来再继续看看读取字符串的源码,即上面代码中input.readStringRequireUtf8()
final int size = readRawVarint32(); // 后面的这一个字节内容代表这个字符串是几位 final byte[] bytes; final int oldPos = pos; final int tempPos; if (size <= (bufferSize - oldPos) && size > 0) { // Fast path: We already have the bytes in a contiguous buffer, so // just copy directly from it. bytes = buffer; pos = oldPos + size; tempPos = oldPos; } else if (size == 0) { return ""; } else if (size <= bufferSize) { refillBuffer(size); bytes = buffer; tempPos = 0; pos = tempPos + size; } else { // Slow path: Build a byte array first then copy it. bytes = readRawBytesSlowPath(size); tempPos = 0; } // TODO(martinrb): We could save a pass by validating while decoding. if (!Utf8.isValidUtf8(bytes, tempPos, tempPos + size)) { throw InvalidProtocolBufferException.invalidUtf8(); } // 上面的一长串都是和读取的内容会不会超过预设值的bufferSize, 默认是4096, 真正取出字符串的是下面这一句 return new String(bytes, tempPos, size, UTF_8); // String的构造函数,从bytes的tempPos开始后面的size个字节,并以UTF_8编码
总结和思考
通过上面的说明,我们对protobuf是如何读取字节流并解码成数据对象的过程有了一定的了解,看起来对我们日常使用protobuf没什么好处,但了解其内部原理确实能帮忙解惑不少protobuf的一些功能。
举一个简单的例子,protobuf是支持更新.protobuf文件中的对象的结构的,而且能够兼容使用旧文件的代码, 怎么做到的呢?当然前提条件是要分配一个新的field_num, 有了上面的分析不难想到,使用旧的.protobuf的代码会忽略这个属性, 因为新的field_num会生成一个新的tag,在switch的case中找不多这个新的tag,自然就忽略它并把它加入到unknownFields。同样的道理,如果你指修改了字段的类型而不修改field_num, 解码的时候就无法正确赋值了。
欢迎关注我的个人的博客www.zhijianliu.cn, 虚心求教,有错误还请指正轻拍,谢谢
版权声明:本文出自志健的原创文章,未经博主允许不得转载