Protocol Buffer For Android
什么是protocal buffer?
protocal buffer 以下简称protobuf是google 的一种数据交换的格式,它独立于语言,独立于平台。(作用类似json、xml等,但是更安全,更快)
简要说明一下流程:
文章分两个部分 我先讲讲protobuf的语法规则与介绍,再讲讲如何使用(已经使用在项目中,如不想了解介绍可以直接跳到使用部分,一般.proto文件服务端会提供好)
介绍
如果你用 android studio 在plugin中安装了 protobuf android插件。那么android studio 将可以识别.proto文件(.proto文件就是一种描述文件)
效果如下图所示:
Paste_Image.png
syntax
:是指定编译的格式、我们可以使用 “proto2” “proto3” 具体区别我也没有深究 默认使用“proto2”
packge
指定生成的java文件所在包
option java_package
指定生成的java文件所在的完整包名
-
message
是消息定义的关键字,等同于C++中的struct/class,或是Java中的class。 -
Request
为消息的名字,等同于结构体名或类名。 -
required
前缀表示该字段为必要字段,既在序列化和反序列化之前该字段必须已经被赋值。与此同时,在Protocol Buffer中还存在另外两个类似的关键字,optional
和repeated
,带有这两种限定符的消息字段则没有required
字段这样的限制。相比于optional
,repeated
主要用于表示数组字段。具体的使用方式在后面的用例中均会一一列出。 -
int64
和string
分别表示长整型和字符串型的消息字段,在Protocol Buffer中存在一张类型对照表,既Protocol Buffer中的数据类型与其他编程语言(C++/Java)中所用类型的对照。该对照表中还将给出在不同的数据场景下,哪种类型更为高效。该对照表将在后面给出。 -
token
和sign
分别表示消息字段名,等同于Java中的域变量名,或是C++中的成员变量名。 - 标签数字1和2则表示不同的字段在序列化后的二进制数据中的布局位置。在该例中,
sign
字段编码后的数据一定位于token
之后。需要注意的是该值在同一message
中不能重复。另外,对于Protocol Buffer而言,标签值为1到15的字段在编码时可以得到优化,既标签值和类型信息仅占有一个byte,标签范围是16到2047的将占有两个bytes,而Protocol Buffer可以支持的字段数量则为2的29次方减一。有鉴于此,我们在设计消息结构时,可以尽可能考虑让repeated类型的字段标签位于1到15之间,这样便可以有效的节省编码后的字节数量。
定义一个含有枚举字段
Protocol Buffer消息。
//在定义Protocol Buffer的消息时,可以使用和C++/Java代码同样的方式添加注释。
enum UserStatus {
OFFLINE = 0; //表示处于离线状态的用户
ONLINE = 1; //表示处于在线状态的用户
}
message UserInfo {
required int64 acctID = 1;
required string name = 2;
required UserStatus status = 3;
}
这里将给出以上消息定义的关键性说明(仅包括上一小节中没有描述的)。
-
enum
是枚举类型定义的关键字,等同于C++/Java中的enum。
-
UserStatus
为枚举的名字。 - 和C++/Java中的枚举不同的是,枚举值之间的分隔符是分号,而不是逗号。
-
OFFLINE/ONLINE
为枚举值。 - 0和1表示枚举值所对应的实际整型值,和C/C++一样,可以为枚举值指定任意整型值,而无需总是从0开始定义。如
enum OperationCode {
LOGON_REQ_CODE = 101;
LOGOUT_REQ_CODE = 102;
RETRIEVE_BUDDIES_REQ_CODE = 103;
LOGON_RESP_CODE = 1001;
LOGOUT_RESP_CODE = 1002;
RETRIEVE_BUDDIES_RESP_CODE = 1003;
}
定义含有嵌套消息字段的Protocol Buffer消息。
我们可以在同一个.proto文件中定义多个message
,这样便可以很容易的实现嵌套消息的定义。如:
enum UserStatus {
OFFLINE = 0;
ONLINE = 1;
}
message UserInfo {
required int64 acctID = 1;
required string name = 2;**
required UserStatus status = 3;**
}
message LogonRespMessage {**
required LoginResult logonResult = 1;**
required UserInfo userInfo = 2;**
}
这里将给出以上消息定义的关键性说明(仅包括上两小节中没有描述的)。
- LogonRespMessage消息的定义中包含另外一个消息类型作为其字段,如UserInfo userInfo。
- 上例中的UserInfo和LogonRespMessage被定义在同一个.proto文件中,那么我们是否可以包含在其他.proto文件中定义的message呢?Protocol Buffer提供了另外一个关键字import,这样我们便可以将很多通用的message定义在同一个.proto文件中,而其他消息定义文件可以通过import的方式将该文件中定义的消息包含进来,如:
import "myproject/CommonMessages.proto"**
限定符(required/optional/repeated)的基本规则
- 在每个消息中必须至少留有一个required类型的字段。
- 每个消息中可以包含0个或多个optional类型的字段。
- repeated表示的字段可以包含0个或多个数据。需要说明的是,这一点有别于C++/Java中的数组,因为后两者中的数组必须包含至少一个元素。
- 如果打算在原有消息协议中添加新的字段,同时还要保证老版本的程序能够正常读取或写入,那么对于新添加的字段必须是optional或repeated。道理非常简单,老版本程序无法读取或写入新增的required限定符的字段。
** Protocol Buffer消息升级原则。**
在实际的开发中会存在这样一种应用场景,既消息格式因为某些需求的变化而不得不进行必要的升级,但是有些使用原有消息格式的应用程序暂时又不能被立刻升级,这便要求我们在升级消息格式时要遵守一定的规则,从而可以保证基于新老消息格式的新老程序同时运行。规则如下:
- 不要修改已经存在字段的标签号。
- 任何新添加的字段必须是optional和repeated限定符,否则无法保证新老程序在互相传递消息时的消息兼容性。
- 在原有的消息中,不能移除已经存在的required字段,optional和repeated类型的字段可以被移除,但是他们之前使用的标签号必须被保留,不能被新的字段重用。
- int32、uint32、int64、uint64和bool等类型之间是兼容的,sint32和sint64是兼容的,string和bytes是兼容的,fixed32和sfixed32,以及fixed64和sfixed64之间是兼容的,这意味着如果想修改原有字段的类型时,为了保证兼容性,只能将其修改为与其原有类型兼容的类型,否则就将打破新老消息格式的兼容性。
- optional和repeated限定符也是相互兼容的。
Packages
我们可以在.proto文件中定义包名,如: package ourproject.lyphone; 该包名在生成对应的C++文件时,将被替换为名字空间名称,既namespace ourproject { namespace lyphone。而在生成的Java代码文件中将
Options。
Protocol Buffer允许我们在.proto文件中定义一些常用的选项,这样可以指示Protocol Buffer编译器帮助我们生成更为匹配的目标语言代码。Protocol Buffer内置的选项被分为以下三个级别:
- 文件级别,这样的选项将影响当前文件中定义的所有消息和枚举。
- 消息级别,这样的选项仅影响某个消息及其包含的所有字段。
- 字段级别,这样的选项仅仅响应与其相关的字段。 下面将给出一些常用的Protocol Buffer选项。
- option java_package = "com.companyname.projectname"; java_package是文件级别的选项,通过指定该选项可以让生成Java代码的包名为该选项值,如上例中的Java代码包名为com.companyname.projectname。与此同时,生成的Java文件也将会自动存放到指定输出目录下的com/companyname/projectname子目录中。如果没有指定该选项,Java的包名则为package关键字指定的名称。该选项对于生成C++代码毫无影响。
- option java_outer_classname = "LYPhoneMessage"; java_outer_classname是文件级别的选项,主要功能是显示的指定生成Java代码的外部类名称。如果没有指定该选项,Java代码的外部类名称为当前文件的文件名部分,同时还要将文件名转换为驼峰格式,如:my_project.proto,那么该文件的默认外部类名称将为MyProject。该选项对于生成C++代码毫无影响。 注:主要是因为Java中要求同一个.java文件中只能包含一个Java外部类或外部接口,而C++则不存在此限制。因此在.proto文件中定义的消息均为指定外部类的内部类,这样才能将这些消息生成到同一个Java文件中。在实际的使用中,为了避免总是输入该外部类限定符,可以将该外部类静态引入到当前Java文件中,如:import static com.company.project.LYPhoneMessage.*。
- option optimize_for = LITE_RUNTIME; optimize_for是文件级别的选项,Protocol Buffer定义三种优化级别SPEED/CODE_SIZE/LITE_RUNTIME。缺省情况下是SPEED。 SPEED: 表示生成的代码运行效率高,但是由此生成的代码编译后会占用更多的空间。 CODE_SIZE: 和SPEED恰恰相反,代码运行效率较低,但是由此生成的代码编译后会占用更少的空间,通常用于资源有限的平台,如Mobile。 LITE_RUNTIME: 生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲Protocol Buffer提供的反射功能为代价的。因此我们在C++中链接Protocol Buffer库时仅需链接libprotobuf-lite,而非libprotobuf。在Java中仅需包含protobuf-java-2.4.1-lite.jar,而非protobuf-java-2.4.1.jar。 注:对于LITE_MESSAGE选项而言,其生成的代码均将继承自MessageLite,而非Message。
- [pack = true]: 因为历史原因,对于数值型的repeated字段,如int32、int64等,在编码时并没有得到很好的优化,然而在新近版本的Protocol Buffer中,可通过添加[pack=true]的字段选项,以通知Protocol Buffer在为该类型的消息对象编码时更加高效。如: repeated int32 samples = 4 [packed=true]。 注:该选项仅适用于2.3.0以上的Protocol Buffer。
- [default = default_value]: optional类型的字段,如果在序列化时没有被设置,或者是老版本的消息中根本不存在该字段,那么在反序列化该类型的消息是,optional的字段将被赋予类型相关的缺省值,如bool被设置为false,int32被设置为0。Protocol Buffer也支持自定义的缺省值,如: optional int32 result_per_page = 3 [default = 10]。
类型对照表
proto Type | Notes | C++ Type | Java Type |
---|---|---|---|
double | double | double | |
float | float | float | |
int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int |
int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long |
uint32 | Uses variable-length encoding. | uint32 | int |
uint64 | Uses variable-length encoding. | uint64 | long |
sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int |
sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long |
fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 228. | uint32 | int |
fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 256. | uint64 | long |
sfixed32 | Always four bytes. | int32 | int |
sfixed64 | Always eight bytes. | int64 | long |
bool | bool | boolean | |
string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String |
bytes | May contain any arbitrary sequence of bytes. | string | ByteString |
使用
为了方便大家使用,我将下面用到的文件都放到了csdn上面供大家下载(mac和windows的)
http://download.csdn.net/detail/qq_22605283/9722829
-
生成java文件(普通版)
1.在git上或者下面谷歌的官网链接下载根据平台下载protoc的可运行文件,然后把 .proto文件放到同一个目录下(或者从上面链接下载)
2 cd 到该目录下用terminal 运行以下代码:
protoc --java_out ./ ./.proto
protoc
:表示利用刚才下载的可运行程序打包
--java_out :
是输出指令 第一个从参数是生成的java目录,第二个参数是指定编 译的.proto文件 ./.proto表示当前目录下所有.proto文件
3把java文件拷贝到项目上
4 在项目的build.gradle的dependencies节点下加入代码:
compile 'com.google.protobuf:protobuf-java:3.0.0'
这行代码是引用android 使用上面生成的java代码需要的jar包
该方法生成的java文件方法会比较多,文件也比较大,会占用许多空间,所以当运用到实际项目的时候会采用轻量版
-
生成java文件(lite版)
1.在git上或者下面谷歌的官网链接下载根据平台下载protoc的可运行文件,然后把 .proto文件放到同一个目录下(或者从上面链接下载)
2.在git上面下载proto-gen-javalite可运行文件放在上面的目录下
2 cd 到该目录下用terminal 运行以下代码:
protoc --javalite_out ./ ./*.proto
3把java文件拷贝到项目上
4 在项目的build.gradle的dependencies节点下加入代码:
compile 'com.google.protobuf:protobuf-lite:3.0.1'
注意有区别哦
还有你会发现虽然方法数少了,size也少了,可是这个java文件依旧占了很大的size。不要急,打开后你会发现其实有很大一部分的代码都是注释,所以当文件打包的时候或者混淆的时候实际会变得很小。举个实例:我的项目中生成的java文件将近700k,混淆完最后打包只占用了40k的大小。
-
使用java代码
例如 我生成了一个Pb.java文件 ,如果原本的.proto带有一个message Request,那么这个Pb.java文件中就会有一个Request的内部类。 看例子代码:
String appIdUTF8 = "";
try {//注意格式转码
appIdUTF8 = URLEncoder.encode("你好啊", "UTF-8");
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "err: " + e.getMessage());
}
//实例化一个PB对象
Pb.Request request = Pb.Request.newBuilder().setId(appIdUTF8).set***.builder
byte[] data = rq.toByteArray();//将PB对象转换成二进制流
String stream =new String(data);//讲PB对象转换成String
...//解析 从网络上或者数据 比如我们已经连接上一个HttpURLConnection conn
//可以被解析的参数有很多种,可以查看下面的图
Pb.Request rq =Pb.Request.parseFrom(conn.getInputStream())
rq.getSid();//获得Pb对象的sid属性
转换格式
好了到这里已经介绍完了,当我使用pb时,都是服务器给定的.proto文件,所以不会动态的去生成java文件,一次生成一直使用,如果你的项目有需求动态更新.proto文件的时候并且生成新的java代码 请参考这个帖子:
http://www.tuicool.com/articles/ruIFvif
资料参考
https://developers.google.com/protocol-buffers/
http://www.cnblogs.com/stephen-liu74/archive/2013/01/02/2841485.html