请支持原创~~
系列博文:
Android protobuf 原理以及ProtoOutputStream、ProtoInputStream 使用(最全)
android protobuf 在ProtoOutputStream和ProtoInputStream 中实现原理
代码基于:Android R
0. 前言
Protobuf是一种灵活高效可序列化的数据协议,相于XML,具有更快、更简单、更轻量级等特性。支持多种语言,只需定义好数据结构,利用Protobuf框架生成源代码,就可很轻松地实现数据结构的序列化和反序列化。一旦需求有变,可以更新数据结构,而不会影响已部署程序。
Protobuf 是一个小型的软件框架,也可以称为protocol buffer 语言,带着疑问会发现Proto 有很多需要了解:
- Proto 文件书写格式,关键字package、option、Message、enum 等含义和注意点是什么?
- 消息等嵌套如何使用?实现的原理?
- Proto 文件对于不同语言的编译,和产生的obj 文件的位置?
- Proto 编译后的cc 和java 文件中不同函数的意义?
- 如何实现*.proto 到*.java、*.h、*.cc 等文件?
- 数据包的组成方式、repeated 的含义和实现?
- Proto 在service和client 的使用,在java 端和native 端如何使用?
- 与xml 、json 等相比时间、空间上的比较如何?
- ...
说明:
- 本文主要分析protobuf 在java 和c++ 中的引用,其他语言暂时不做过多阐述;
- 本文很多地方的代码引用将采用Android AOSP 中 windowmanagerservie.proto为例;
- 本文从最基本的开始剖析、总结,上面说Protobuf 是个小型的软件框架,但是当真正进入使用时,会发现很多需要注意的地方。所以,后面持续补充、长期更新;
- 由于时间原因,后面会持续补充,希望最后尽可能地让这个知识点完整!!
1. proto 特点
其中高效性从网上copy 了一个表格,注明:本人没有验证过(不懂Json),仅供参考
序列化时间效率对比:
数据格式 | 1000条数据 | 5000条数据 |
---|---|---|
Protobuf | 195ms | 647ms |
Json | 515ms | 2293ms |
序列化空间效率对比:
数据格式 | 5000条数据 |
---|---|
Protobuf | 22MB |
Json | 29MB |
2. proto 支持语言
Language | Source |
---|---|
C++ | src |
Java | java |
Python | python |
Objective-C | objectivec |
C# | csharp |
JavaNano | javanano |
JavaScript | js |
Ruby | ruby |
Go | golang/protobuf |
PHP | php |
Dart | dart-lang/protobuf |
3. proto 语法
3.1 syntax
syntax = "proto2";
用以制定语法版本,proto2 或proto3
3.2 import
对于引用,如果在同一个 proto 文件中,可以直接引用,例如:
message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
这个其实可以放message 一节之后在讲解,这里有个概念也可以。因为 import 关键字都是在proto 文件的开头使用,所以,这里提前讲解。
如果message 声明的proto 类型是位于同一个proto 文件,可以直接引用,但如果引用的是另外的文件时直接添加import 引用即可。例如,
import "frameworks/base/core/proto/android/view/surface.proto";
import "frameworks/base/core/proto/android/view/windowlayoutparams.proto";
import "frameworks/base/core/proto/android/privacy.proto";
用以引入其他文件中的message、enum等
注意,import 引用的proto 文件在编译成java 和c++ 是不同处理
- java 中是忽略该import,而是采用完全引用;
- c++ 中将import 的proto 文件,换成引用头文件形式;
有可能遇到一个特殊情况,proto 文件有可能会更换location,可能会被移到新的目录,这个时候client 引用的地方,有可能都要更新一遍。proto 中提供了一个机制:
在旧路径下,放一个仿的proto 文件,里面指定import public 到新的路径下。
例如,
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
上面代表三个proto 文件,new proto、old proto 和 client proto。old proto 不需要使用原来的,只需要弄个假的,里面通过import public 指定新路径的proto 即可。
3.3 package
package com.android.server.wm;
用以指定proto 的包名,如果其他的proto 文件import 后,可以根据这个包名进行引用,也可以通过package 确定编译后的文件的路径。
例如,
package foo.bar;
message Open { ... }
引用的时候:
message Foo {
...
required foo.bar.Open open = 1;
...
}
3.3.1 package 在c++ 中引用
这里是proto 中的引用,当编译称c++ 文件时,这里的package 则会被编译称namespace。如上面的例子,foo.bar 在编译称c++ 后,Foo 会编译成class,而Foo 类位于foo::bar 这个namespace 中。
3.3.2 package 在java 中引用
package 在java 中的引用稍微有些不同,因为对于java 文件可以在proto 中通过java_package 进行修改,例如:
package foo.bar;
option java_package = "com.example.foo.bar";
message Open { ... }
The
java_package
option is provided because normal.proto
package
declarations are not expected to start with a backwards domain name.
3.3.3 pacakge 在python/go 中引用
package 在python 或 go 中是被忽略的,它们有各自的规则
3.4 option java_multiple_files
option java_multiple_files = true;
在 *.proto 文件中可以指定该属性为true,在编译java 文件时会将message、enum 等声明以单独java 文件生成。
如果没有指定该属性或设置为false,所有proto 文件内的message、service、enum等都会以嵌套的方式存在于以个java 文件中。
这个属性只针对java 文件,如果不生成java 文件,该option 无效。
3.5 option java_outer_classname
option java_outer_classname = "DataStallEventProto";
在 *.proto 文件中可以指定生成的 java 文件名,如果未指定,java 文件名为 proto 的文件名。
这个属性只针对java 文件,如果不生成java 文件,该option 无效。
3.6 message
protobuf 语言中最关键的格式,message 声明的结构体会在编译成java文件时变成class,该class 会implements 谷歌Message 接口。
3.6.1 声明
声明方式类似:
message ActivityRecordProto {
...
}
对于implements 的Message java 接口位于AOSP 中external/protobuf/java/core/src/main/java/com/google/protobuf/Message.java 文件
package com.google.protobuf;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
public interface Message extends MessageLite, MessageOrBuilder {
对于c++ 接口位于AOSP 中
external/protobuf/src/google/protobuf/message.h 文件
class PROTOBUF_EXPORT Message : public MessageLite {
public:
inline Message() {}
~Message() override {}
Message* New() const override = 0;
3.6.2 message 编译java
android 编译proto 使用的是protoc 命令,详细可以看build/make/core/definitions.mk:
define transform-proto-to-java
@mkdir -p $(dir $@)
@echo "Protoc: $@ <= $(PRIVATE_PROTO_SRC_FILES)"
@rm -rf $(PRIVATE_PROTO_JAVA_OUTPUT_DIR)
@mkdir -p $(PRIVATE_PROTO_JAVA_OUTPUT_DIR)
$(hide) for f in $(PRIVATE_PROTO_SRC_FILES); do \
$(PROTOC) \
$(addprefix --proto_path=, $(PRIVATE_PROTO_INCLUDES)) \
$(PRIVATE_PROTO_JAVA_OUTPUT_OPTION)="$(PRIVATE_PROTO_JAVA_OUTPUT_PARAMS):$(PRIVATE_PROTO_JAVA_OUTPUT_DIR)" \
$(PRIVATE_PROTOC_FLAGS) \
$$f || exit 33; \
done
$(SOONG_ZIP) -o $@ -C $(PRIVATE_PROTO_JAVA_OUTPUT_DIR) -D $(PRIVATE_PROTO_JAVA_OUTPUT_DIR)
endef
详细的规则是定义在build/make/core/java_common.mk,感兴趣的同学可以自行查看,需要注意的是:
ifeq ($(LOCAL_PROTOC_OPTIMIZE_TYPE),micro)
$(proto_java_srcjar): PRIVATE_PROTO_JAVA_OUTPUT_OPTION := --javamicro_out
$(proto_java_srcjar): PRIVATE_PROTOC_FLAGS += --plugin=$(HOST_OUT_EXECUTABLES)/protoc-gen-javamicro
$(proto_java_srcjar): $(HOST_OUT_EXECUTABLES)/protoc-gen-javamicro
else ifeq ($(LOCAL_PROTOC_OPTIMIZE_TYPE),nano)
$(proto_java_srcjar): PRIVATE_PROTO_JAVA_OUTPUT_OPTION := --javanano_out
$(proto_java_srcjar): PRIVATE_PROTOC_FLAGS += --plugin=$(HOST_OUT_EXECUTABLES)/protoc-gen-javanano
$(proto_java_srcjar): $(HOST_OUT_EXECUTABLES)/protoc-gen-javanano
else ifeq ($(LOCAL_PROTOC_OPTIMIZE_TYPE),stream)
$(proto_java_srcjar): PRIVATE_PROTO_JAVA_OUTPUT_OPTION := --javastream_out
$(proto_java_srcjar): PRIVATE_PROTOC_FLAGS += --plugin=$(HOST_OUT_EXECUTABLES)/protoc-gen-javastream
$(proto_java_srcjar): $(HOST_OUT_EXECUTABLES)/protoc-gen-javastream
else
$(proto_java_srcjar): PRIVATE_PROTO_JAVA_OUTPUT_OPTION := --java_out
endif
这里会根据不同的LOCAL_PROTOC_OPTIMIZE_TYPE 指定不同的option,例如
- 默认情况是编译对应的proto java 文件,使用 --java_out 选项指定输出目录;
- 配合java stream 需要使用 --javastream_out 选项指定输出目录,android framework 中的protobuf 指定的field 都是通过此选项编译成的 java 识别,field 值包含了value的类型、field id、是否为repeate;
当然,对于上面第二点,可以查看frameworks/base/Android.bp,android R 是在该bp 文件中执行指定了规则:
gensrcs {
name: "framework-javastream-protos",
depfile: true,
tools: [
"aprotoc",
"protoc-gen-javastream",
"soong_zip",
],
cmd: "mkdir -p $(genDir)/$(in) " +
"&& $(location aprotoc) " +
" --plugin=$(location protoc-gen-javastream) " +
" --dependency_out=$(depfile) " +
" --javastream_out=$(genDir)/$(in) " +
" -Iexternal/protobuf/src " +
" -I . " +
" $(in) " +
"&& $(location soong_zip) -jar -o $(out) -C $(genDir)/$(in) -D $(genDir)/$(in)",
srcs: [
":ipconnectivity-proto-src",
"core/proto/**/*.proto",
"libs/incident/**/*.proto",
],
output_extension: "srcjar",
}
ok 回到起点
以上面ActivityRecordProto 为例,该message 声明在windowsmanagerservice.proto 文件中,在编译的时候会将message ActivityRecordProto 编译称单独的final class,不允许出现子类。
ActivityRecordProto 类会继承自GeneratedMessage,默认情况下会override GeneratedMessage 中的大部分接口,但如果proto 中声明:
option optimize_for = CODE_SIZE;
ActivityRecordProto 会override 仅仅一些必要的少量集的接口,而且依赖GeneratedMessage 生下来的反射方式的实现。这个配置会减少编译生成的代码,但是也同时降低了性能。
proto 文件中也可以包含:
option optimize_for = LITE_RUNTIME;
这样class 会快速实现MessageLite 接口,该接口只包含Message 的子集。生成的代码只需要link libprotobuf-lite.jar 而不需要link libprotobuf.jar,lite 的库比完成的库要小很多,更适合于资源受限的系统,如移动电话。
另外,对于java 编译,还有个属性:
option java_multiple_files = true;
如果设置,如果将 .proto 中的message、enum、service 都编译成单独的 *.java 文件。
对于 android 生成代码文件位于out\soong\.intermediates\frameworks\base\platformprotos\linux_glibc_common 下,其中包含 *.class 和 *.srcjar,其中 *.srcjar 是java 文件的打包,例如这里的ActivityRecordProto.java:
package com.android.server.wm;
/**
* <pre>
* represents ActivityRecordProto
* </pre>
*
* Protobuf type {@code com.android.server.wm.ActivityRecordProto}
*/
public final class ActivityRecordProto extends
com.google.protobuf.GeneratedMessageV3 implements
// @@protoc_insertion_point(message_implements:com.android.server.wm.ActivityRecordProto)
ActivityRecordProtoOrBuilder {
private static final long serialVersionUID = 0L;
// Use ActivityRecordProto.newBuilder() to construct.
private ActivityRecordProto(com.google.protobuf.GeneratedMessageV3.Builder<?> builder) {
super(builder);
}
private ActivityRecordProto() {
name_ = "";
frozenBounds_ = java.util.Collections.emptyList();
state_ = "";
}
...
详细的代码细节这里不做过多的阐述,后面根据使用情况再补充说明。不过,这里需要提示下,该java 文件中是一套proto 的操作,例如,
- 可以内部的Builder 类进行message 的构建,最后通过build() 产生ActivityRecordProto 实例;
- 可以通过static 函数getDefaultInstance,获取默认的ActivityRecordProto 实例;
- 可以通过static 变量PARSER 或函数parser 创建一个parser 进行解码实例;
- 也可以通过ActivityRecordProto.parseFrom 进行直接解析返回一个解析好的ActivityRecordProto 实例;
详细的剖析请参考:Android protobuf 生成java 文件详解
对于andoird frameworks,java 层还提供了ProtoOutputStream 和 ProtoInputStream 两个类,可以通过ProtoOutputStream 对message 进行编码,通过ProtoInputStream 对message 进行解码,并不需要通过上面的ActivityRecordProto.java,而是通过protoc-gen-javastream 命令产生一个新的ActivityRecordProto.java(此java 非彼java):
package com.android.server.wm;
/** @hide */
// message ActivityRecordProto
public final class ActivityRecordProto {
// optional string name = 1;
public static final long NAME = 0x0000010900000001L;
// optional .com.android.server.wm.WindowTokenProto window_token = 2;
public static final long WINDOW_TOKEN = 0x0000010b00000002L;
// optional bool last_surface_showing = 3;
public static final long LAST_SURFACE_SHOWING = 0x0000010800000003L;
// optional bool is_waiting_for_transition_start = 4;
public static final long IS_WAITING_FOR_TRANSITION_START = 0x0000010800000004L;
// optional bool is_animating = 5;
public static final long IS_ANIMATING = 0x0000010800000005L;
...
过多的细节请参考:android protobuf 在ProtoOutputStream和ProtoInputStream 中实现原理
3.6.3 message 编译c++
编译c++ 的流程基本上同编译java,在build/make/core/definitions.mk 中定义了编译命令:
define transform-proto-to-cc
@echo "Protoc: $@ <= $<"
@mkdir -p $(dir $@)
$(hide) \
$(PROTOC) \
$(addprefix --proto_path=, $(PRIVATE_PROTO_INCLUDES)) \
$(PRIVATE_PROTOC_FLAGS) \
$<
@# aprotoc outputs only .cc. Rename it to .cpp if necessary.
$(if $(PRIVATE_RENAME_CPP_EXT),\
$(hide) mv $(basename $@).cc $@)
endef
具体的规则,感兴趣可以查看build/make/core/binary.mk,这里不过多的阐述了。
对于android framework,生成代码位于out/soong/.intermediates/frameworks/base/libplatformprotos/linux_glibc_x86_64_static/gen 下,其中包含了 *.h 和 *.cc 文件。还是以ActivityRecordProto 为例,编译后的头文件如下:
class ActivityRecordProto :
public ::PROTOBUF_NAMESPACE_ID::Message /* @@protoc_insertion_point(class_definition:com.android.server.wm.ActivityRecordProto) */ {
public:
ActivityRecordProto();
virtual ~ActivityRecordProto();
ActivityRecordProto(const ActivityRecordProto& from);
ActivityRecordProto(ActivityRecordProto&& from) noexcept
: ActivityRecordProto() {
*this = ::std::move(from);
}
inline ActivityRecordProto& operator=(const ActivityRecordProto& from) {
CopyFrom(from);
return *this;
}
...
不过,与java 不同的是,java 可以通过
option java_multiple_files = true;
方式将 *.proto 中声明的message、enum、service 都编译称单独的问题,c++ 中只是以 proto 的名称命名 *.h 和 *.cc
另外,编译出来的 *.h 中会include 所有proto 中的import 依赖(*.h 中都是以头文件形式),proto 中嵌套的类别在c++ 中都是以完整的类名引用(namespace::class)。
详细c++ 解析可以参考:Android protobuf 生成c++ 文件详解
3.6.4 fields number
message 中每个field 都有一个唯一的number,用以在编码后识别自身,而且这个number 在message type 使用过程中不能修改。
field number 可以是1 ~ -1,预留三位给wire type, 详细参考:Android protobuf 编码详解
不能使用19000 ~ 19999,这个区间的数是reserved 给Protocol buffers 实现。
3.6.5 fields rules
- required: 编码好的消息中必须有一个这样的field;
- optional: 编码好的消息中可以有0 或 1 个这样的field;
- repeated:这个修饰的field 可以重复多次(包括 0 次),重复的value 顺序需要维护;
required 在使用的时候需要消息,谷歌的工程得出结论,使用required 是弊大于利,他们更倾向于是用optional 和 repeated。
2.6.6 value types
3.6.7 optional fields and Default value
对于拥有optional fields 的message,在做编码的时候,并不一定会将该fields 添加到字节流中,那么在做解码的时候,可能需要赋予这个fields value 默认值,默认值获取方式有两种:
- 明确指定;
- 使用系统默认值;
对于明确指定是在声明message 的时候:
optional int32 result_per_page = 3 [default = 10];
对于系统默认值:
类型 | 默认值 |
---|---|
string | empty string |
byte | empty byte string |
bool | false |
numeric | 0 |
enum | first value |
3.7 enum
格式大致如下:
enum ProbeResult {
UNKNOWN = 0;
VALID = 1;
INVALID = 2;
PORTAL = 3;
PARTIAL = 4;
}
基本类似message,在编译成java 文件时,可能是单独的文件,文件名为enum 后的类型名。
但是有个特殊的时protoc-gen-javastream 编译后,*.proto 中的所有enum 都会放在同一个java 文件中,即使指定了option java_multiple_files = true;这一点不同于message,message 会以不同的java 文件生成。
3.8 service
3.9 注释
采用C/C++ 样式,使用:// 和 /* ... */
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
message SearchRequest {
required string query = 1;
optional int32 page_number = 2; // Which page number do we want?
optional int32 result_per_page = 3; // Number of results to return per page.
}
4. Encoding
5. ProtoStream 和ProtoOutputStream、ProtoInputStream
详细参考:android protobuf 在ProtoOutputStream和ProtoInputStream 中实现原理
系列博文:
Android protobuf 原理以及ProtoOutputStream、ProtoInputStream 使用(最全)
android protobuf 在ProtoOutputStream和ProtoInputStream 中实现原理
参考:
https://developers.google.com/protocol-buffers/docs/reference/overview
https://blog.csdn.net/weixin_45519413/article/details/113356501