Android protobuf 原理以及ProtoOutputStream、ProtoInputStream 使用(最全)

请支持原创~~

系列博文:

 Android protobuf 原理以及ProtoOutputStream、ProtoInputStream 使用(最全)

 Android protobuf 生成java 文件详解

Android protobuf 生成c++ 文件详解

android protobuf 在ProtoOutputStream和ProtoInputStream 中实现原理

Android protobuf 编码详解

 代码基于: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条数据
Protobuf195ms647ms
Json515ms2293ms

序列化空间效率对比:

数据格式5000条数据
Protobuf22MB
Json29MB

2. proto 支持语言


LanguageSource
C++src
Javajava
Pythonpython
Objective-Cobjectivec
C#csharp
JavaNanojavanano
JavaScriptjs
Rubyruby
Gogolang/protobuf
PHPphp
Dartdart-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 ~ 2^{29}-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];

对于系统默认值:

类型默认值
stringempty string
byteempty byte string
boolfalse
numeric0
enumfirst 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


详细参考:Android protobuf 编码详解

5. ProtoStream 和ProtoOutputStream、ProtoInputStream


详细参考:android protobuf 在ProtoOutputStream和ProtoInputStream 中实现原理

 系列博文:

 Android protobuf 原理以及ProtoOutputStream、ProtoInputStream 使用(最全)

 Android protobuf 生成java 文件详解

Android protobuf 生成c++ 文件详解

android protobuf 在ProtoOutputStream和ProtoInputStream 中实现原理

Android protobuf 编码详解

参考:

https://developers.google.com/protocol-buffers/docs/reference/overview

https://blog.csdn.net/weixin_45519413/article/details/113356501

https://juejin.cn/post/6844903582743920648

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

私房菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值