Android中使用Protocol Buffer
Protocol Buffer 是啥
谷歌官方定义:Protocol Buffer 是一种轻便高效的结构化存储格式,使用于结构化数据序列化,适合做数据存储或者RPC数据交换格式。与语言无关,与平台无关,可扩展的序列化结构数据格式
比 json xml 更加高效
优点:
- 序列化之后的体积比json和xml都小
- 支持快平台,多语言
- 消息格式升级和兼容性都不错
- 序列化和反序列化速度很快
使用Protocol Buffer
1. 搭建Protocol Buffer环境
下载 https://github.com/protocolbuffers/protobuf/releases
Mac下安装Homebrew1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//安装依赖
brew install autoconf automack libtool curl
//运行字画生成脚本
cd xxx/protobuffer-3.xx
./autogen.sh
//运行配置脚本
./configure
//编译为编译的依赖包
make
//检查依赖包的完整性
make check
//安装 protocol buffer
make install
//检查是否安装完成
protoc --version
在AndroidStudio中使用插件的方式安装
Google提供了插件的方式,配置build.gradle
https://github.com/google/protobuf-gradle-plugin
在工程的build.gradle中1
2
3
4
5
6
7
8buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
}
}
在app的build.gradle中(每个版本的配置都不一样,这里是3.8.0)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22apply plugin: 'com.google.protobuf'
dependencies {
// You need to depend on the lite runtime library, not protobuf-java
//lite是轻量版
compile 'com.google.protobuf:protobuf-javalite:3.8.0'
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.8.0'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
2. 定义.Proto文件(Android中在main文件夹下面创建proto文件夹在创建.proto文件) ,编译.Proto文件
构建消息
消息至少由一个字段组合而成
字段 = 字段修饰符 + 字段类型 + 字段名 + 标识号
- 字段修饰符:设置该字段解析时的规则
- 字段理性:基本类型 枚举类型 消息对象
- 字段名:字段名称
- 表示号:二进制格式唯一标识每个字段
字段默认值
Proto2支持声明默认值,Proto3使用以下规则
- 对于string类型,默认值为空字符串
- 对于byte类型,默认值为一个空byte数组
- 对于bool类型,默认为false
- 对于数值类型,默认值为0
- 对于枚举类型,默认值为第一项,即值为0的那个枚举值
- 对于引用其他message类型,默认值和对应的语言相关
标识号TAG
- 对应同一个message中的字段,每个字段的Tag是必须唯一的数字
- Tag主要用于说明字段在二进制文件中的对应关系,一旦指定字段为对应的Tag,不应该在后序进行变更
- 对于Tag的分配。1~15只用一个byte进行编码(因此应该留给那些常用的字段),16~2047用两个byte进行编码,最大支持到536870911,但是中间有一段(19000~19999)是protobuf内部使用的
- 可以通过reserved关键字来预留Tag和字段名,还有一种场景是如果某个字段已经被废弃,不希望后序被使用,也可以使用reserved关键字声明
.proto文件 例子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27//指定是 不写默认proto23的语法 不写默认proto2
syntax = "proto3";
package tutorial;
//对应环境下的处理方式 这里是java中
option java_package = "com.sample.model";
option java_outer_classname= "AddressProto";
message Person{
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
message Address {
//使用repeated关键字修饰 表示该字段可以重复多次赋值,包括0次,不指定表示0次或者1次,重复的值的顺序会被保留,相当于动态化的数组(list)[packed=true]开启高效编码
repeated Person person = 1;
}
编译消息 (命令行方式)1
2
3
4
5
6
7
8在终端输入下面命令
protoc -I=#SRC_DIR -xxx_out=$DST_DIR $SRC_DIR/xxx.proto
//比如:
protoc -I=/User/doc/Android/prorobuffer/app/src/proto -- java_out=/User/doc
/Users/doc/Android/protobuffer/app/src/main/proto/address.proto
//在/User/doc下面生成对应的java文件
- SRC_DIR 编译.proto的文件目录
- –xxx_out 设置生成的代码类型 java: –java_out c++: -cpp_out
- DST_DIR 代码生成目录 xxx.proto 的路径
在AndroidStudio中,配置好之后直接build即可
生成文件在 build/generated/source/proto中
3. Android中使用Protocol Buffer
在项目中使用序列化和反序列化1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26//创建对象
AddressProte.Person.Builder builder = AddressProto.Person.newBuilder();
AddressProto.Person person = builder.setEmain("xxx").build();
//序列化
byte[] bytes = person.toByteArray();
//反序列化
try{
AddressProto.Person person1 = AddressProto.Person.parseFron(bytes);
}catch(InvalidPriticilBufferException e){
e.prientStackTrace()
}
//序列化
ByteArrayOutputStream output = new ByteArrayOutputStream();
try(){
person.witeTo(output);
byte[] byte1 = output.toByteArray();
}catch(IOException e){
e.prientStackTrace()
}
//反序列化
try(){
AddressProto.Person person1 = AddressProto.Person.parseFrom(new ByteArrayInputStream(byte));
}catch(IOException e){
e.prientStackTrace()
}
Protocol Buffer 原理
- 编码机制 Base 128 Varints
- 消息结构 key-value 键值对组成
Varints是一种可变字节序列化整形的方法
- 每个Byte的最高为(msb)是标志位,如果该位是1,表示该Byte后面还有其他的Byte,如果该位为0,表示Byte是最后一个Byte
- 每个Byte的第7位是用来村存数值的位
- Varints方法用Little-Endian(小端)字节序
一个多位整数,按照其存储地址的最高或者最低字节进行排序,如果最低有效位在最高有效位的前面,则成为小端序,反之成为大端序
消息结构
编码类型
Type | Mesning | Used For | |
---|---|---|---|
0 | Varints | int32 ,int64,uint32,uint64,sint32,sint64,bool,enum | |
1 | 64-bit | fiexd64 ,sfixed64 ,double | |
2 | Length-delimited | string,bytes,embedded message,packed,repeated fields | |
3 | Start group | groups(deprecated) | |
4 | End group | groups(deprecated) | |
5 | 32-bit | fixed32,sfixed32,float |
消息结构
key
key 的具体值为(field_number<<3|wire_type)
key的范围
wire_type只有6种类型,用3bit表示,在一个Byte里,去掉mbs,以3bit的wire_type,只剩下4bit来表示field_number,因此在一个Byte里,field_number只能表达0-15,如果超过15,则需要两个或者多个Byte来表示。常用字段放在前面
Varints优缺点
- Varints适用于表达比较小的证书,当数字很大时,采用定长编码类型(64bit,32bit)
- Varints不擅长表达负数,负数采用补码表示,会占用更多的字节,因此如果确定会出现负数,可以采用sint32或者sint64,他会采用ZigZig编码将负数映射为整数,之后再使用Varints编码
Length-delimited:编码格式则会将数据的length也编码进最终数据中,使用Length-delimited编码格式的数据类型包括string bytes和自定义消息
repeated/packed:repeated类型是把每个字段依次进行序列化,key相同,value不同,但是如果repeated的字段比较多,每次都带上相同的key则会浪费空间,因此 protobuf提供了packed选项,当repeated字段设置了packed选项,则会使用Length-delimited格式来编码字段值
Protocol Buffer使用注意事项
- 尽量不要修改tag
- 字段数量不要超过16个,否则会采用2个字节进行编码
- 如果确定使用负数,采用sint32/sint64