Android 12中使用Google Protobuf

Android12中使用Google Protobuf
Views: 494
2 0
Read Time:5 Minute, 34 Second

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系统。

其官方网站可点击here

如何使用

在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 32uint32
uint64无符号int 64uint64
stringUTF-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。

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)成员函数进行获取,获取的是对象的引用。

Others

在使用protobuf时,我们还需要注意其版本要求,这里需要注意两个版本:一是运行库的版本要求,二是proto语法的版本,我们不能在一个程序中使用两个运行库版本或者两个proto语法版本。

Happy
Happy
100 %
Sad
Sad
0 %
Excited
Excited
0 %
Sleepy
Sleepy
0 %
Angry
Angry
0 %
Surprise
Surprise
0 %
FranzKafka95
FranzKafka95

极客,文学爱好者。如果你也喜欢我,那你大可不必害羞。

Articles: 85

Leave a Reply

Your email address will not be published. Required fields are marked *

en_USEN