Google Protocol Buffer(protobuf)是一种高效且格式可扩展的编码结构化数据的方法。和JSON不同,protobuf支持混合二进制数据,它还有先进的和可扩展的模式支持。protobuf已在大多数软件平台上实现,包括适用于Android的精简Java版。
http://developers.google.com/protocol-buffers/上有protobuf文档,下载链接以及安装说明。需要注意的是,Android平台为构建精简版的protobuf,所以不能使用中央Maven仓库里的版本。在Java源码目录内执行mvn package -p lite可以生成精简版。检查是否有更多安装细节。
JSON允许对JSONObject对象进行任意数据的读写操作,但protobuf要求使用模式来定义要存储的数据。模式会定义一些消息,每个消息包含一些名-值对字段。字段可能是内置的原始数据类型,枚举或者其他消息。可以指定一个字段是必须的还是可选的,以及其他一些参数。一旦定义好模式,就可以使用protobuf工具生成Java代码。生成的Java类现在可以很方便地用来读写protobuf数据。
下面的代码使用protobuf模式定义了Task信息:
package com.aptl.code.task; option optimize_for = LITE_RUNTIME; option java_package = "com.aptl.protobuf"; option java_outer_classname = "TaskProtos"; message Task { enum Status { CREATED = 0; ONGOING = 1; CANCELLED = 2; COMPLETED = 3; } message Owner { required string name = 1; optional string email = 2; optional string phone = 3; } message Comment { required string author = 1; required uint32 timestamp = 2; required string content = 3; } required string name = 1; required uint64 created = 2; required int32 priority = 3; required Status status = 4; optional Owner owner = 5; repeated Comment comments = 6; }
这里将给出以上消息定义的关键性说明。
1. message是消息定义的关键字,等同于C++中的struct/class,或是Java中的class。
2. Task 为消息的名字,等同于结构体名或类名。
3. required前缀表示该字段为必要字段,既在序列化和反序列化之前该字段必须已经被赋值。与此同时,在Protocol Buffer中还存在另外两个类似的关键字,optional和repeated,带有这两种限定符的消息字段则没有required字段这样的限制。相比于optional,repeated主要用于表示数组字段。具体的使用方式在后面的用例中均会一一列出。
4. int64和string分别表示长整型和字符串型的消息字段,在Protocol Buffer中存在一张类型对照表,既Protocol Buffer中的数据类型与其他编程语言(C++/Java)中所用类型的对照。该对照表中还将给出在不同的数据场景下,哪种类型更为高效。该对照表将在后面给出。
5. name,created ,priority ,status,owner 和comments分别表示消息字段名,等同于Java中的域变量名,或是C++中的成员变量名。
6. 标签数字1和2则表示不同的字段在序列化后的二进制数据中的布局位置。在该例中,created字段编码后的数据一定位于name之后。需要注意的是该值在同一message中不能重复。另外,对于Protocol Buffer而言,标签值为1到15的字段在编码时可以得到优化,既标签值和类型信息仅占有一个byte,标签范围是16到2047的将占有两个bytes,而Protocol Buffer可以支持的字段数量则为2的29次方减一。有鉴于此,我们在设计消息结构时,可以尽可能考虑让repeated类型的字段标签位于1到15之间,这样便可以有效的节省编码后的字节数量。
Protocol Buffer允许我们在.proto文件中定义一些常用的选项,这样可以指示Protocol Buffer编译器帮助我们生成更为匹配的目标语言代码。Protocol Buffer内置的选项被分为以下三个级别:
1. 文件级别,这样的选项将影响当前文件中定义的所有消息和枚举。
2. 消息级别,这样的选项仅影响某个消息及其包含的所有字段。
3. 字段级别,这样的选项仅仅响应与其相关的字段。
下面将给出一些常用的Protocol Buffer选项。
1. option java_package = "com.aptl.protobuf";
java_package是文件级别的选项,通过指定该选项可以让生成Java代码的包名为该选项值,如上例中的Java代码包名为com.aptl.protobuf。与此同时,生成的Java文件也将会自动存放到指定输出目录下的com/aptl/protobuf子目录中。如果没有指定该选项,Java的包名则为package关键字指定的名称。该选项对于生成C++代码毫无影响。
2. option java_outer_classname = "TaskProtos";
java_outer_classname是文件级别的选项,主要功能是显示的指定生成Java代码的外部类名称。如果没有指定该选项,Java代码的外部类名称为当前文件的文件名部分,同时还要将文件名转换为驼峰格式,如:my_project.proto,那么该文件的默认外部类名称将为MyProject。该选项对于生成C++代码毫无影响。
注:主要是因为Java中要求同一个.java文件中只能包含一个Java外部类或外部接口,而C++则不存在此限制。因此在.proto文件中定义的消息均为指定外部类的内部类,这样才能将这些消息生成到同一个Java文件中。在实际的使用中,为了避免总是输入该外部类限定符,可以将该外部类静态引入到当前Java文件中,如:import static com.aptl.protobuf.TaskProtos.*。
3. 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。
4. [pack = true]: 因为历史原因,对于数值型的repeated字段,如int32、int64等,在编码时并没有得到很好的优化,然而在新近版本的Protocol Buffer中,可通过添加[pack=true]的字段选项,以通知Protocol Buffer在为该类型的消息对象编码时更加高效。如:
repeated int32 samples = 4 [packed=true]。
注:该选项仅适用于2.3.0以上的Protocol Buffer。
5. [default = default_value]: optional类型的字段,如果在序列化时没有被设置,或者是老版本的消息中根本不存在该字段,那么在反序列化该类型的消息是,optional的字段将被赋予类型相关的缺省值,如bool被设置为false,int32被设置为0。Protocol Buffer也支持自定义的缺省值,如:
optional int32 result_per_page = 3 [default = 10]。
从InputStream反序列化protobuf对象非常容易,如下例所示。生成的Java代码提供一些用于合并字节数组,byteBuffer和InputStream对象的函数。
public static TaskProtos.Task readBrotoBufFromStream(InputStream inputStream) throws IOException { TaskProtos.Task task = TaskProtos.Task.newBuilder() .mergeFrom(inputStream).build(); Log.d("ProtobufDemo", "Read Task from stream: " + task.getName() + ", " + new Date(task.getCreated()) + ", " + (task.hasOwner() ? task.getOwner().getName() : "no owner") + ", " + task.getStatus().name() + ", " + task.getPriority() + task.getCommentsCount() + " comments."); return task; }
本例显示了如何检索protobuf对象的值。注意:protobuf对象是不可变的。修改它们唯一的方法是从现有对象创建一个新的构建器,设置新的值,并生成一个取代原有对象的Task。这使得protobuf有点不好用,但它强制开发者在持久化复杂对象时使用更好的设计。
下面的方法显示了如何构建一个新的protobuf对象。首先为构造的对象创建一个新的Builder,然后设置所需要的值并调用Builder.build()方法来创建不可变的protobuf对象。
public static TaskProtos.Task buildTask(String name, Date created, String ownerName, String ownerEmail, String ownerPhone, TaskProtos.Task.Status status, int priority, List<TaskProtos.Task.Comment> comments) { TaskProtos.Task.Builder builder = TaskProtos.Task.newBuilder(); builder.setName(name); builder.setCreated(created.getTime()); builder.setPriority(priority); builder.setStatus(status); if(ownerName != null) { TaskProtos.Task.Owner.Builder ownerBuilder = TaskProtos.Task.Owner.newBuilder(); ownerBuilder.setName(ownerName); if(ownerEmail != null) { ownerBuilder.setEmail(ownerEmail); } if(ownerPhone != null) { ownerBuilder.setPhone(ownerPhone); } builder.setOwner(ownerBuilder); } if (comments != null) { builder.addAllComments(comments); } return builder.build(); }
API提供了一系列方法用来把protobuf对象写到文件或者网络流中。下面的代码演示了如何把Task对象序列化到OutputStream中。
public static void writeTaskToStream(TaskProtos.Task task, OutputStream outputStream) throws IOException { task.writeTo(outputStream); }
protobuf主要的优点是它比JSON消耗的内存少,而且读写速度更快。protobuf对象还是不可变的,如果要确保对象的值在整个生命周期中保持不变,该特性会非常有用。
Protocol Buffers 2.6.1 full source: protobuf-2.6.1.tar.gz (MD5: f3916ce13b7fcb3072a1fa8cf02b2423) Protocol Compiler 2.6.1 binary for windows: protoc-2.6.1-win32.zip (MD5: b057f86ef83835010bb227eb2d82de04)