Android protobuf 编码详解
请支持原创~~
系列博文:
Android protobuf 原理以及ProtoOutputStream、ProtoInputStream 使用(最全)
Android protobuf 生成java 文件详解
Android protobuf 生成c++ 文件详解
android protobuf 在ProtoOutputStream和ProtoInputStream 详解
Android protobuf 编码详解
基于版本:Android R
0. 前言
上一篇 Android protobuf 原理 中简单分析了proto buf 的优缺点和实现原理,以及使用。对于 *.proto 文件的详细编译、生成原理以单独的博文呈现,这一篇主要对proto buf 进行编码原理的剖析。
This document describes the binary wire format for protocol buffer messages. You don't need to understand this to use protocol buffers in your applications, but it can be very useful to know how different protocol buffer formats affect the size of your encoded messages.
引用官方的一段话,理论上在app 开发的时候并不需要关心编码,但可以有助于理解编码后的消息以什么样的形式组成。
1. 组成规则 key-value
protocol buffer message 就是一系列的键值对。使用field number 为key,并根据wire type 确定value 存放的形式。

不同wire type 会导致value 以不同的方式存储,下面会详细说明。这里需要记住一点,field number 和wire type 会组成key,后期解码也会根据这个key 确认对应的value 占用空间和value 值。
2. key 的构成
key = (field_number << 3) | wire_type;
field_number 为该field 在message 中声明的unique value。
例如,一个key 的值为:
000 1000
可以获得:
- field_number 为 1;
- wire_type 为0,即varint 类型的value
3. wire types
3.1 varint
一般varint 的方式针对 field 类型 int32, int64, uint32, uint64, sint32, sint64, bool, enum 的value。
对于不带符号的数,算法类似:
public void writeRawVarint64(long val) {while (true) {if ((val & ~0x7FL) == 0) {writeRawByte((byte)val);return;} else {writeRawByte((byte)((val & 0x7F) | 0x80));val >>>= 7;}}}
假设value 为uint64,调用该函数,每次取 7 bits 的数,如果还能继续取,则第 8 位置1。
换言之,varint 的数可能是一个或者多个byte组成,如果byte 的第 8 位为1,则表示后面一个byte 也是属于这个value。
例如,150
二进制:1001 0110
那么变成message value应该为:1001 0110 0000 0001 (十六进制为:96 01)
由 1 个byte 的数变成 2 个 byte:
- 第一个byte 为 1001 0110,最高位1 表示后面的 1 个byte 为value 的高7位;
- 第二个byte 为 0000 0001,最高位0 表示value 到此,后面的7个bits 为value 的高7位;
varint 其实对于bit 位比较小的数是节省开销的,例如,
double value = 1;
正常情况下value 存储是按照 8 个bytes,而如果按照varint 则只需要存1 个bytes。
对于带符号的数,首先是需要经过zigzag 的转换,再按照上面的算法存放。
zigzag 算法类似:
(n << 1) ^ (n >> 31) //对于sint32
(n << 1) ^ (n >> 63) //对于sint64
这样做目的是将负数,转换为正数,减少空间开销。例如,
sint32 value = -1;
如果按照完整存储的话,一直到31位都是1,所以需要4个字节的空间,而如果按照zigzag 方式,-1 将会被转换为1,只需要 1 个字节的空间。
经过zigzag 可以转换:

3.2 64-bit
一般64-bit 类型针对field 类型 fixed64, sfixed64, double类型的value
算法类似:
public void writeRawFixed64(long val) {writeRawByte((byte)(val));writeRawByte((byte)(val >> 8));writeRawByte((byte)(val >> 16));writeRawByte((byte)(val >> 24));writeRawByte((byte)(val >> 32));writeRawByte((byte)(val >> 40));writeRawByte((byte)(val >> 48));writeRawByte((byte)(val >> 56));}
这种比较简单,将value 以8个字节完全存放
3.3 32-bit
一般针对field 类型 fixed32, sfixed32, float 的value
算法类似:
public void writeRawFixed32(int val) {writeRawByte((byte)(val));writeRawByte((byte)(val >> 8));writeRawByte((byte)(val >> 16));writeRawByte((byte)(val >> 24));}
这种比较简单,将value 以4个字节完全存放
3.4 Length-delimited
这里相对于上面几个类型比较复杂点,一般针对field 类型string, bytes, embedded messages, packed repeated fields 的value。
例如,
message Test2 {optional string b = 2;
}
对于string 类型的field,value 可能是不定长的,采用wire_type 为1,即length-delimited 方式存放value,与上面几种处理方式不同的是length-delimited 在key 之后多了一个length,即存储组成为:
key + length + string
例如,上面例子中b 对应的value 为"testing" 字符串,则字节流应该是:
12 07 74 65 73 74 69 6e 67
- 红色部分为key,(2 << 3) | 2
- 绿色部分为length
- 蓝色部分为testing 的UTF8 值
对于嵌套的message 也是同样用length-delimited 方式存放,例如,
message Test3 {optional Test1 c = 3;
}message Test1 {optional int a = 1;
}
若a 的value 为150,则c 的值空间排列为:
1a 03 08 96 01
- 红色部分为key,(3 << 3) | 2
- 绿色部分为length
- 蓝色部分为 Test1 中a 的value组成:08 为a 的key,96 01 为value(150)
4. Android 中的编码
android 在framework 中提供了一套类似的算法,目前针对wire type 是start group 和end group暂时不支持。
4.1 wire types
wire type 定义在PtotoStream.java 中:
frameworks/base/core/java/android/util/proto/ProtoStream.java
public static final int WIRE_TYPE_VARINT = 0;public static final int WIRE_TYPE_FIXED64 = 1;public static final int WIRE_TYPE_LENGTH_DELIMITED = 2;public static final int WIRE_TYPE_START_GROUP = 3;public static final int WIRE_TYPE_END_GROUP = 4;public static final int WIRE_TYPE_FIXED32 = 5;
4.2 field id
对于android frameworks 中,message 中的field id 都是通过protoc-gen-java-stream 编译称单独的java,例如windowmanagerservice.proto 在编译成独立的java后,其中的ActivityRecordProto.java:
package com.android.server.wm;/** @hide */
// message ActivityRecordProto
public final class ActivityRecordProto {// optional string name = 1;public static final long NAME = 0x0000010900000001L;// optional .com.android.server.wm.WindowTokenProto window_token = 2;public static final long WINDOW_TOKEN = 0x0000010b00000002L;// optional bool last_surface_showing = 3;public static final long LAST_SURFACE_SHOWING = 0x0000010800000003L;// optional bool is_waiting_for_transition_start = 4;public static final long IS_WAITING_FOR_TRANSITION_START = 0x0000010800000004L;...
低32 位表示在message 中声明的 field number,高32 位表示该field value 的类型和是否有repeated属性。
同样是在ProtoStream.java 中了数据类型:
public static final int FIELD_TYPE_SHIFT = 32;public static final long FIELD_TYPE_DOUBLE = 1L << FIELD_TYPE_SHIFT;public static final long FIELD_TYPE_FLOAT = 2L << FIELD_TYPE_SHIFT;public static final long FIELD_TYPE_INT64 = 3L << FIELD_TYPE_SHIFT;...
同样在ProtoStream.java 中定义了是否为repeated 或 packed:
public static final int FIELD_COUNT_SHIFT = 40;public static final long FIELD_COUNT_SINGLE = 1L << FIELD_COUNT_SHIFT;public static final long FIELD_COUNT_REPEATED = 2L << FIELD_COUNT_SHIFT;public static final long FIELD_COUNT_PACKED = 5L << FIELD_COUNT_SHIFT;
解码时,对于一个编译后生成的 field 最终可以根据低32 位得知field 的id,通过高32 位获取存放的值的类型和repeated 属性。
编码过程和注意点,详细可以查看:android protobuf 在ProtoOutputStream和ProtoInputStream 中详解
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
