Physical Address:
ChongQing,China.
WebSite:
Google Protobuf是由Google官方推出的序列化与反序列化框架,其支持跨语言、跨平台,具有良好的拓展性。Protobuf和其他所有的序列化框架一样(如Json、xml、toml等),都可以用于数据存储、通讯协议。在SOA(Service-Oriented Architecture)框架下,Google Protobuf得到极大程度的推广。其优点包括:
1.更小的资源消耗,Portobuf的序列化的结果体积要比XML、JSON小很多,可以减少内存空间的占用。
2.更快的响应速度。Portobuf序列化和反序列化速度比XML、JSON快很多,直接把对象和字节数组做转换。
3.良好的后向兼容性。Protobuf可以保证向下兼容,即使协议变更也能保证以往的应用可用。
4.高可复用的应用代码。Protobuf提供了IDL工具可以快速地帮助开发者生成通信所需的胶水代码,提升效率。
5.跨平台、跨语言。Protobuf可以支持C++/C#/JAVA/Kotlin/ Objective-C/PHP/Python/Ruby/Go/Dart等编程语言,适用于多个平台,自然也包括Android系统。
其官方网站可点击这里。
在Android中使用Google Protobuf时,我们需要确保Android系统中已经有了Protobuf的运行环境,也就是protobuf的运行库。这里以Trout x86虚拟机为例:
trout_x86:/system/lib # ls -la | grep protobuf
-rw-r--r-- 1 root root 2375220 2009-01-01 08:00 libprotobuf-cpp-full.so
-rw-r--r-- 1 root root 507396 2009-01-01 08:00 libprotobuf-cpp-lite.so
trout_x86:/system/lib #
我们可以看到这里有两个Protobuf运行库,相应的也对应两个变体:Protobuf-Full与Protobuf-Lite。这两个变体的差异在于:
大小:lite版本相比于full版本节省20%左右的代码容量。
速度: lite版本相比于full版本使用速度上快约20%。
特性:full版本比lite版本增加了许多新的特性,如反射,多语言支持等
如何选择使用哪个变体呢,很简单,如果你注重使用效率且不需要反射等功能,那么选择lite版本,否则推荐使用full版本。
AOSP中是通过源码方式来编译使用protobuf的,其源码路径为/external/protobuf,这里我们可以看一下Android.bp的内容:
// C++ full library for the platform and host
// =======================================================
cc_library {
name: "libprotobuf-cpp-full",
defaults: ["libprotobuf-cpp-full-defaults"],
host_supported: true,
vendor_available: true,
product_available: true,
// TODO(b/153609531): remove when no longer needed.
native_bridge_supported: true,
target: {
android: {
static: {
enabled: false,
},
},
windows: {
enabled: true,
},
},
apex_available: [
"//apex_available:platform",
"com.android.appsearch",
"com.android.virt",
],
}
// C++ lite library for the platform and host.
// =======================================================
cc_library {
name: "libprotobuf-cpp-lite",
host_supported: true,
recovery_available: true,
vendor_available: true,
vendor_ramdisk_available: true,
product_available: true,
double_loadable: true,
defaults: ["libprotobuf-cpp-lite-defaults"],
target: {
windows: {
enabled: true,
},
},
apex_available: [
"//apex_available:platform",
"//apex_available:anyapex",
],
min_sdk_version: "29",
}
该Android.bp除了编译上述两个库,同时也编译了Java与Python版本的库,具体规则可仔细查看Android.bp内的内容。
我们在使用protobuf时就需要引用对应的运行库,除了运行库,我们还需要另外一个基础材料——proto定义文件,也就是后缀为*.proto的配置文件。
在而具体使用时,我们主要有两种方式:
方式一:利用AOSP soong编译系统结合Android.bp自动根据*.proto文件生成对应的胶水代码进行编译后使用。
方式二:手动通过protoc(protobuf compiler)结合*.proto文件生成源代码,根据源代码编写编译配置文件(Android.mk或者Android.bp)进行编译后使用。
此处举例说明,比如我们有一堆编写好的* .proto文件,想要生成名为libfoo.so的这样一个库进行使用。在使用方式一时,直接通过Android.bp进行配置:
cc_library {
name: "libfoo",
srcs: [
"example1.proto",
"example2.proto",
"example3.proto"
],
proto: {
export_proto_headers: true,
type: "lite",
},
shared_libs: [
"libprotobuf-cpp-lite",
],
cppflags: [
"-Wall",
"-Werror",
"-Wunused",
"-Wunreachable-code",
"-Wno-unknown-pragmas",
"-Wno-unused-parameter",
"-Wno-non-virtual-dtor",
"-Wno-macro-redefined",
"-Wno-unused-lambda-capture",
"-fexceptions",
"-fPIE",
"-fPIC"
],
}
这里我们通过proto字段来设置protobuf的类型,设置type为lite则使用libprotobuf-cpp-lite.so运行库,设置type为full则使用libprotobuf-cpp-full.so运行库,相应地我们在shared_libs中引用对应的运行库。
在使用方式二时,我们需要使用protoc工具来将proto配置生成源码文件,这里我们看一下protoc的使用方法:
FranzKafka@Franz:/opt/FranzKafkaYu/Android12.1$ protoc
Usage: protoc [OPTION] PROTO_FILES
Parse PROTO_FILES and generate output based on the options given:
--cpp_out=OUT_DIR Generate C++ header and source.
--csharp_out=OUT_DIR Generate C# source file.
--java_out=OUT_DIR Generate Java source file.
--js_out=OUT_DIR Generate JavaScript source.
--objc_out=OUT_DIR Generate Objective C header and source.
--php_out=OUT_DIR Generate PHP source file.
--python_out=OUT_DIR Generate Python source file.
--ruby_out=OUT_DIR Generate Ruby source file.
-IPATH, --proto_path=PATH Specify the directory in which to search for
imports. May be specified multiple times;
directories will be searched in order. If not
given, the current working directory is used.
--version Show version info and exit.
-h, --help Show this text and exit.
--encode=MESSAGE_TYPE Read a text-format message of the given type
from standard input and write it in binary
to standard output. The message type must
be defined in PROTO_FILES or their imports.
--decode=MESSAGE_TYPE Read a binary message of the given type from
standard input and write it in text format
to standard output. The message type must
be defined in PROTO_FILES or their imports.
--decode_raw Read an arbitrary protocol message from
standard input and write the raw tag/value
pairs in text format to standard output. No
PROTO_FILES should be given when using this
flag.
这里我们生成C++的代码,如下所示:
FranzKafka@Franz:/opt/FranzKafkaYu/Android12.1$protoc adas_msg.proto --cpp_out=.
FranzKafka@Franz:/opt/FranzKafkaYu/Android12.1$ls -la
-rw-rw-r-- 1 FranzKafkaYu FranzKafkaYu 197858 May 13 11:30 adas_msg.pb.cc
-rw-rw-r-- 1 FranzKafkaYu FranzKafkaYu 151598 May 13 11:30 adas_msg.pb.h
-rw-rw-r-- 1 FranzKafkaYu FranzKafkaYu 3972 Apr 17 18:15 adas_msg.proto
FranzKafka@Franz:/opt/FranzKafkaYu/Android12.1$
对形如example.proto的proto配置文件,会生成example.pb.h的头文件和example.pb.cc源码文件,在生成这些文件后,我们直接引用生成的源码文件就好了。
使用protobuf重要的就是protobuf的配置文件编写,这里需要了解protbuf配置的语法规则。当前Google protobuf分为两个版本:proto2与proto3.这里以proto2为主进行介绍。
所有的数据结构定义文件以.proto结尾,以下为一个示例:
syntax = "proto2";
message SearchRequest {
optional string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
其中第一行规定了proto版本,这是必须的。
第二行开始定义一个message类型,message作为关键字存在,是protobuf中最小的数据定义单元,而SearchRequest作为message类型的名称。而SearchRequest类型内部包含三个子field,这些子filed就是我们需要传输的具体的消息字段。Message整体上类似于C/C++中的struct结构体。在同一个proto文件中,我们可以定义多个message。
针对这些子filed,我们需要定义其数据类型,且分配一个范围为1~536870911的id(排除19000-19999,这部分id是属于reserved保留字段),需要主注意的是该id在对应的message类型内必须是全局唯一的,推荐使用1~15的id(占用一个字节)
在定义子field时,我们可以使用optional、repeated、required等关键字为子field定义额外的规则。
Optional:表明该field在消息体中至多只有1次,也可能不会有对应的值
Repeated:表明该field在消息体中可以多次出现
Reqiuired:已不再推荐使用,在proto3中已被移除
注释:proto文件中可以使用注释,其语法规则类似于C/C++,使用//进行行注释,使用/**/进行块注释。
数据类型:proto中的数据类型以及对应到C/C++时的参考表
Proto类型 | 备注 | C/C++类型 |
doulble | doulble | |
float | float | |
int32 | 对于存在负数的数值编码效率较低 | int32 |
int64 | 对于存在负数的数值编码效率较低 | int64 |
sint32 | 有符号int 32,当值 | int32 |
sint64 | int64 | |
uint32 | 无符号int 32 | uint32 |
uint64 | 无符号int 64 | uint64 |
string | UTF-8编码 | string |
bytes | string |
此外,所有的数据类型都会执行类型检查以确保其值是有效的。
默认值:针对optional的field,在传输的message中可能是不存在值的,如果我们需要为某些这类子field设定默认值,可以使用default关键字,如下所示:
optional int32 result_per_page = 3 [default = 10]; 如果没有这样显式地定义其默认值,将会根据数据类型自动添加默认值,string类型的默认值为空字符串,bool类型默认值为false,数值类型默认值则为0。
复合类型之枚举:我们可以在proto文件中定义枚举体,与C/C++类似,定义枚举体使用enum关键字,在使用上也与C/C++中的枚举体类似,如下所示:
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
message SearchRequest {
optional string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
optional Corpus corpus = 4 [default = CORPUS_UNIVERSAL];
}
Message嵌套:我们可以在message的子field中使用定义好的其他message类型,从而实现嵌套使用。
导入其他proto文件:在实际使用中我们可能会定义多个proto文件,并且需要引入其他proto文件中定义好的message或者enum,此时可以使用import关键字进行导入。
定义packages:定义package可以为生成的代码提供类似于命名空间的作用,这样可以确保生成的代码不会有那么多的冲突。
如下所示:
syntax = "proto2";
//导入其他proto
import "plates.proto";
import "poles.proto";
import "calibration_state.proto";
package autoplt;
message AutoCalibMsg {
optional int32 example1 = 1;
optional int32 example2 = 2;
optional float example3 = 3;
}
在了解语法规则后,我们就可以编写自己的proto配置文件,生成代码进行编译使用了,接下来讲讲protobuf中的常用API。
在使用protoc将*.proto生成对应的代码文件后,我们可以看一下生成的代码内容,了解Google protobuf所提供的API,方便后续使用。
检查/管理API:
void CopyFrom(const ::google::protobuf::Message& from) final;
void MergeFrom(const ::google::protobuf::Message& from) final;
void CopyFrom(const xxxxxx& from); //用外部消息的值,覆写调用者消息内部的值。
void MergeFrom(const xxxxxx& from); //将外部消息的值合并到调用者消息内部的值。
void Clear() final;//清除所有的数据以及相关标志位
bool IsInitialized() const final;//检查消息中所有的字段是否设定初始值
size_t ByteSizeLong() const final; //获取消息的字节数大小
void Swap(xxxxxx* other); //将外部消息的值与调用者消息内部的值进行交换
诊断相关API:
string DebugString() const; //将消息内容以可读的方式输出
string ShortDebugString() const; //功能类似于,DebugString(),输出时会有较少的空白
string Utf8DebugString() const; //Like DebugString()
void PrintDebugString() const;/、GDB调试时打印至stdout
以traffic_sign.proto为例,其定义如下:
syntax = "proto2";
package example.vehicle;
message TrafficSignHead{
xxxx
}
message TrafficSignObject{
xxxx
xxx
}
message TrafficSignMsg {
optional TrafficSignHead head = 1;
repeated TrafficSignObject objects = 2;
};
其对应会生成traffic_sign.pb.cc和trafic_sign.pb.h两个文件;在trafic_sign.pb.h内,我们可以看到三个命名空间,分别为:protobuf_traffic_5fsign_2eproto,::example::vehicle,::google::protobuf
其中::example::vehicle命名空间内包含三个类 ,分别为TrafficSignHead、TrafficSignObject、TrafficSignMsg,这三个类就是我们在proto中定义好的message,针对message内不同属性的成员(optional/repeated/required),其生成的内容也有差异。
optional:获取对应的子field内容,有三个成员函数可选,如上示例中生成的代码如下,其中head为optional属性:
const ::example::vehicle::TrafficSignHead& head() const;
::example::vehicle::TrafficSignHead* release_head();
::example::vehicle:TrafficSignHead* mutable_head();
repeated:repeated的成员可能会在同一包message中携带多条重复的信息,此时我们可以使用xxx_size()获取具体的size大小,要获取对应的子field内容,可以使用如下成员函数:mutable_xxxx(int index),xxxx(int index),其中xxx为对应repeated子filed name,此处将以traffic_sign.proto为示例:
::example::vehicle::TrafficSignObject* mutable_objects(int index); //获取不同index所对应的数据
const ::example::vehicle::TrafficSignObject& objects(int index) const;//获取不同index所对应的数据
关于mutable_xxxx(int index)与xxxx(int index),的区别:两者都属于RepeatedPtrField类的成员函数,不过前者是通过RepeatedPtrField类的Mutable(int index)成员函数进行获取,获取的是对象的指针;而后者通过Get(int index)成员函数进行获取,获取的是对象的引用。
在使用protobuf时,我们还需要注意其版本要求,这里需要注意两个版本:一是运行库的版本要求,二是proto语法的版本,我们不能在一个程序中使用两个运行库版本或者两个proto语法版本。