网络性能优化的终极手法就是不通过网络传输,但这常常是不可能的。但我们还是可以通过对网络传输的数据本身做优化,来获得更好的性能,性能就应该从每一个可能的地方榨取。这里来看一下 Protocol Buffers 。
Protocol Buffers 是一个序列化结构数据的灵活、高效且自动化的机制——类似于XML,但更小,更快,更简单。定义一次结构化数据的方式,然后就可以使用专门生成的代码简单地写入,或用不同的语言从大量的数据流读出结构化数据。甚至可以更新数据结构而不破坏已部署的基于 老 格式编译的程序。我们看一下要如何将 Protocol Buffers 用到我们的Android项目中。
#总览
先来看一下 Protocol Buffers 项目已经为我们提供了什么,我们在使用 Protocol Buffers 时需要做什么的整体流程。如下图:
在使用 Protocol Buffers 时,我们需要以特殊的方式定义我们的结构化数据,保存为 .proto 消息定义文件。 Protocol Buffers 项目为我们提供了编译器,可以将 .proto 文件编译为Java文件以用于我们的Java 或 Android应用项目。这个编译器在我们的PC机上编译并运行。产生的Java文件是依赖于 Protocol Buffers 的Java库的,比如这些文件实现了库的借口等。我们将生成的这些Java文件和 Protocol Buffers 的Java库引入我们的Android应用项目,就可以方便地以 Protocol Buffers 的二进制格式操作结构化数据了。
每次手动执行 Protocol Buffers 编译器将 .proto 文件转换为Java文件显然有点太麻烦了。 Protocol Buffers 项目的开发者显然也想到了这一点,因而他们还为我们提供了一个Android Studio gradle插件 protobuf-gradle-plugin ,以便于在我们项目的编译期间自动地执行 Protocol Buffers 编译器。
我们可以为 protobuf-gradle-plugin 指定本地 Protocol Buffers 编译器的路径让它使用本地的编译执行编译,也可以使用 Protocol Buffers 项目提供的另外一个工具,在编译时动态地下载并执行编译过程。
后面我们详细地来看这个过程。
下载编译Protocol编译器
我们可以在如下位置:
下载打包好的protobuf,也可以直接clone protobuf的代码,自己手动编译编译器。这里我们从GitHub上clone代码并手动编译编译器:
下载代码之后,进入protobuf目录并执行 autogen.sh :
这个脚本主要用于下载测试用的gmock-1.7.0,并生成用于编译配置的 configure 等文件。可以通过如下命令了解我们可以对protobuf的编译做哪些配置,以及默认配置的信息:
执行configure对编译进行配置:
这样就生成了makefile文件,编译并安装:
这个过程在编译并安装 Protocol Buffers 编译器之外,还会为host编译用于支持在C++中使用 Protocol Buffers 的库。(编译生成的二进制文加在 protobuf/src/.libs 下。)
安装之后执行如下命令以确认已经装好:
在执行protoc时通过给它加上 –help 参数可以了解到这个工具更多的用法。
创建 .proto 文件
.proto 文件中的定义很简单:为每个想要序列化的数据结构添加一个 消息(message) ,然后为消息中的每个字段指定一个名字和类型以及一个tag数字。如官方提供的一个例子addressbook.proto:
可以参考 在Java中使用Protocol Buffers 一文了解更多关于创建 .proto 文件的基础知识。
编译 .proto 文件
可以通过如下命令编译 .proto 文件:
-I,–java_out 分别用于指定源目录 (放置应用程序源代码的地方 —— 如果没有提供则使用当前目录),目的目录 (希望放置生成的代码的位置;通常与$SRC_DIR相同),最后的参数为 .proto 文件的路径。protoc会按照标准Java风格,生成Java类及目录结构。如对于上面的例子,会生成 com/example/tutorial/ 目录结构,及 AddressBookProtos.java 文件。
在Android项目中使用 Protocol Buffers
我们将 由 .proto 文件生成的Java文件复制到我们的Android项目中:
在我们app的build.gradle中添加对 protobuf-java 的依赖,就像依赖其它那些Java库一样:
添加访问Protocol Buffers的类的类。这里我们添加两个类,AddPerson用于构造Person对象:
AddressBookProtobuf类则用于编码/解码AddressBook对象:
使用protobuf-gradle-plugin
每次单独执行protoc编译 .proto 文件总是太麻烦,通过protobuf-gradle-plugin可以在编译我们的app时自动地编译 .proto 文件,这样就大大降低了我们在Android项目中使用 Protocol Buffers 的难度。
首先我们需要将 .proto 文件添加进我们的项目中,如:
然后修改 app/build.gradle 对protobuf gradle插件做配置:
为buildscript添加对
protobuf-gradle-plugin
的依赖:123456789buildscript {repositories {jcenter()mavenCentral()}dependencies {classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.0'}}在
apply plugin: 'com.android.application'
后面应用protobuf的plugin:12apply plugin: 'com.android.application'apply plugin: 'com.google.protobuf'添加protobuf块,对protobuf-gradle-plugin的执行做配置:
12345678910111213141516171819protobuf {protoc {path = '/usr/local/bin/protoc'}generateProtoTasks {all().each { task ->task.builtins {remove java}task.builtins {java { }// Add cpp output without any option.// DO NOT omit the braces if you want this builtin to be added.cpp { }}}}}
protoc
块用于配置Protocol Buffers编译器,这里我们指定用我们之前手动编译的编译器。task.builtins
的块必不可少,这个块用于指定我们要为那些编程语言生成代码,这里我们为C++和Java生成代码。缺少这个块的话,在编译时会报出如下的错误:
提示说没有指定输出目录的路径。
这是由于 protobuf-gradle-plugin 执行的protobuf编译器命令的参数是在protobuf-gradle-plugin/src/main/groovy/com/google/protobuf/gradle/GenerateProtoTask.groovy
中构造的:
可以看到,输出目录是由builtins构造的。
- 指定 .proto 文件的路径12345678910sourceSets {main {java {srcDir 'src/main/java'}proto {srcDir 'src/main/proto'}}}
这样我们就不用那么麻烦每次手动执行protoc了。
对前面的protobuf块做一点点修改,我们甚至来编译protobuf编译器都不需要了。修改如下:
关于 .proto 文件的编写方法,Protocol Buffers API等更多内容,可以参考 Protobuf开发者指南、在Java中使用Protocol Buffers及其它相关官方文档。
Protobuf 与 JSON 对比测试
说了半天,Protobuf的表现究竟如何呢?这里我们就对比一下我们最常用到的JSON格式与Protobuf的表现。测试基于在开发者中一向有着良好口碑的fastjson进行。
测试用的数据结构如我们前面看到的AddressBook。我们通过构造包含不同个数Person的AddressBook数据,并对这些数据执行多次编码解码操作,来测试Protobuf 与 JSON的表现。Protobuf的编码/解码测试代码如前面看到的AddressBookProtobuf。JSON的测试代码则如下面这样:
通过如下的这段代码来执行测试:
这里我们执行3组编码测试及3组解码测试。对于编码测试,第一组的单个数据中包含10个Person,第二组的包含50个,第三组的包含100个,然后对每个数据分别执行5000次的编码操作。
对于解码测试,三组中单个数据同样包含10个Person、50个及100个,然后对每个数据分别执行5000次的解码码操作。
在Galaxy Nexus的Android 4.4.4 CM平台上执行上述测试,最终得到如下结果:
编码后数据长度对比 (Bytes)
Person个数 | Protobuf | Protobuf(GZIP) | JSON | JSON(GZIP) |
---|---|---|---|---|
10 | 860 | 291 | 1703 | 344 |
50 | 4300 | 984 | 8463 | 1047 |
100 | 8600 | 1840 | 16913 | 1913 |
相同的数据,经过Protobuf编码的数据长度,大概只有JSON编码的数据长度的一半。但对编码后的数据再进行压缩,两者则差别比较小。
编码性能对比 (S)
Person个数 | Protobuf | JSON |
---|---|---|
10 | 4.687 | 6.558 |
50 | 23.728 | 41.315 |
100 | 45.604 | 81.667 |
编码性能最少提高了 28.5%,最多则提高了44.2%。Protobuf在编码性能上,相对于JSON还是有较大幅度的提升的。
解码性能对比 (S)
Person个数 | Protobuf | JSON |
---|---|---|
10 | 0.226 | 8.839 |
50 | 0.291 | 43.869 |
100 | 0.220 | 85.444 |
解码性能方面,Protobuf相对于JSON,则更是有惊人的提升。Protobuf的解码时间几乎不随着数据长度的增长而有太大的增长,而JSON则随着数据长度的增加,解码所需要的时间也越来越长。
Done。