Android知识总结
一、Protobuf简介
protocolbuffer(以下简称protobuf)是google 的是一种轻便高效的结构化数据存储格式,作用形同于xml和json。它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的
二进制数据传输格式
,可以用于诸如网络传输、配置文件、数据存储等诸多领域。
Protobuf是一种平台无关、语言无关、可扩展且轻便高效的序列化数据结构的协议,可以用于网络通信和数据存储。 可简单类比于 XML ,其具有以下特点:
- 1、语言无关、平台无关。即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台
- 2、高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单
- 3、扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程
1、优点
使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。
它有一个非常棒的特性,即“向后”兼容性好,人们不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级。这样您的程序就可以不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题。因为添加新的消息中的 field 并不会引起已经发布的程序的任何改变。
2、缺点
Protbuf 与 XML 相比也有不足之处。它功能简单,无法用来表示复杂的概念。
XML 已经成为多种行业标准的编写工具,Protobuf 只是 Google 公司内部使用的工具,在通用性上还差很多。
由于文本并不适合用来描述数据结构,所以 Protobuf 也不适合用来对基于文本的标记文档(如 HTML)建模。另外,由于 XML 具有某种程度上的自解释性,它可以被人直接读取编辑,在这一点上 Protobuf 不行,它以二进制的方式存储,除非你有 .proto 定义,否则你没法直接读出 Protobuf 的任何内容。
二、使用protobuf步骤
android中使用protobuf,过程是这样的:
- 1、定义proto文件;
- 2、使用该文件生成对应的java类;
- 3、利用该java类实现数据传输;
从以上过程中就可以看出,我们并不是直接使用proto文件,而是对应的java类,如何根据proto文件生成java类呢?官方推荐的是命令行的方式生成,但是Android Studio生成方式更加简单,这里直接介绍as生成方式(同样适用服务端开发工具intellij idea)
第一步
在AS的Plugins中插入protobuf插件,如图
第二步
在根Project/build.gradle中加入protobuf插件
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8'
}
}
第三步
在app/build.gradle中加入如下配置,顶部加上应用插件
apply plugin: 'com.google.protobuf'
apply plugin: 'com.android.application'
android{}中加入
sourceSets {
main {
java {
srcDir 'src/main/java'
}
proto {
srcDir 'src/main/proto' //指定.proto文件路径
include '**/*.proto' //find it
}
}
}
android{}同级加入:
//编译并生成
protobuf {
protoc { // 也可以配置本地编译器路径
artifact = 'com.google.protobuf:protoc:3.4.0'
}
plugins {
javalite {
// The codegen for lite comes as a separate artifact
artifact = 'com.google.protobuf:protobuf-java:3.4.0'
}
}
//这里配置生成目录,编译后会在build的目录下生成对应的java文件和C文件
generateProtoTasks {
all().each { task ->
task.builtins {
// In most cases you don't need the full Java output
// if you use the lite output.
remove java
}
task.builtins {
java {} //java文件
cpp {} //C文件
}
}
}
}
dependencies中加入protobuf相关依赖
implementation 'com.google.protobuf:protobuf-java:3.4.0'
implementation 'com.google.protobuf:protoc:3.4.0'
- 混淆配置:
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
第四步
在app\src\main目录中新建proto文件夹,并新建对应的proto文件,这里以LoginRequest.proto为例
LoginRequest.proto文件内容为:
syntax = "proto3"; //声明 proto 协议版本 ( proto2 和 proto3 在定义看数据结构时有些差别)
package com.example.protobuf; //定义了 Protobuf 自动生成类的包名(即 java 类所在的包名)
option java_package = "com.example.protobuf";//java 类所在的包名 == package com.example.protobuf;
option java_outer_classname = "LoginRequestProto"; //定义了 Protobuf 自动生成类的类名
message LoginRequest {
string name = 1;
int32 id = 2;
string email = 3;
string phone = 4;
}
//定义了类中的字段(这里只有 account 和 password 两个字段)
message Login {
uint64 ID = 1;
string name = 2;
string password = 3;
oneof pet {
Dog dog = 4;
Cat cat = 5;
}
}
message Dog {
string name = 1;
bool sex = 2;
}
message Cat {
string name = 1;
//属性可以与Dog不同
}
第五步
Build/Clean Project跑完即可,此时会在\app\build\generated\source\proto中生成对应的java文件和C++文件,拷出来备用。
三、Window 系统下使用protobuf
-
1)、protobuf的安装
protoc的源码和各个系统的预编译包
选择对应的安装文件下载:
在path中添加到环境变量中
2)、protobuf的使用方法
查看protoc的版本
protoc --version #查看protoc的版本
代码转换显例(把目录切换到 E:\user\protoc-3.15.8-win64\bin
, protoc的bin目录下)
protoc.exe --java_out=E:\java Immortaldb.proto
输出文件夹是E:\java
输入是Immortaldb.proto
四、简单使用
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = findViewById(R.id.name_tv);
//序列化
LoginRequestProto.LoginRequest loginRequest = LoginRequestProto.LoginRequest
.newBuilder()
.setName("XaoLi")
.setId(122)
.setEmail("[email protected]")
.setPhone("123456")
.build();
byte[] bytes = loginRequest.toByteArray();
//反序列化
try {
LoginRequestProto.LoginRequest login = LoginRequestProto.LoginRequest
.parseFrom(bytes);
mTextView.setText(login.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 在proto3枚举值第一个必须是0,其他的随意
- 在proto2,每个属性前必须加required,optional,repeated
- 该数字只要不重复,可以定义为任何数字,不需要总是从1或者0开始
- 这个数字表示在序列化数组里面的顺序
注意:定义的proto文件中的编号对应字段名前后可以不行同,但是编号对应的字段类型的相同。
五、语法
syntax = "proto3";
message LoginRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 文件的第一行指定了你正在使用proto3语法:如果你没有指定这个,编译器会使用proto2。这个指定语法行必须是文件的非空非注释的第一个行。
- SearchRequest消息格式有3个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和一种类型。
关键字
syntax
:声明版本。例如上面syntax="proto3",如果没有声明,则默认是proto2。package
:声明包名.import
:导入包。类似于java,例如上面导入了timestamp.proto包。java_package
:指定生成的类应该放在什么Java包名下。如果你没有显式地指定这个值,则它简单地匹配由package 声明给出的Java包名,但这些名字通常都不是合适的Java包名 (由于它们通常不以一个域名打头)。java_outer_classname
:定义应该包含这个文件中所有类的类名。如果你没有显式地给定java_outer_classname ,则将通过把文件名转换为首字母大写来生成。例如上面例子编译生成的文件名和类名是AddressBookProtos。message
:类似于java中的class关键字。repeated
:用于修饰属性,表示对应的属性是个array。
更多的关键字可以参考官方文档,这里不做介绍。
1)、标量数值类型
Protobuf3语言指南
2)、枚举
当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个SearchRequest消息添加一个 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一个。 其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(enum)并且为每个可能的值定义一个常量就可以了。
在下面的例子中,在消息格式中添加了一个叫做Corpus的枚举类型——它含有所有可能的值 ——以及一个类型为Corpus的字段
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
如你所见,Corpus枚举的第一个常量映射为0:每个枚举类型必须将其第一个类型映射为0,这是因为:
- 必须有有一个0值,我们可以用这个0值作为默认值。
- 这个零值必须为第一个元素,为了兼容proto2语义,枚举类的第一个值总是默认值。
3)、其他消息类型
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
所指定的消息字段修饰符必须是如下之一:
singular
:一个格式良好的消息应该有0个或者1个这种字段(但是不能超过1个)。repeated
:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。
在proto3中,repeated的标量域默认情况虾使用packed。
4)、嵌套类型
你可以在其他消息类型中定义、使用消息类型,在下面的例子中,Result消息就定义在SearchResponse消息内,如:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果你想在它的父消息类型的外部重用这个消息类型,你需要以Parent.Type的形式使用它,如:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
5)、字段编号
在message定义中每个字段都有一个唯一的编号,这些编号被用来在二进制消息体中识别你定义的这些字段,一旦你的message类型被用到后就不应该在修改这些编号了。注意在将message编码成二进制消息体时字段编号1-15将会占用1个字节,16-2047将占用两个字节。所以在一些频繁使用用的message中,你应该总是先使用前面1-15字段编号。
你可以指定的最小编号是1,最大是2E29 - 1(536,870,911)。其中19000到19999是给protocol buffers实现保留的字段标号,定义message时不能使用。同样的你也不能重复使用任何当前message定义里已经使用过和预留的字段编号。
参考
Protobuf3语言指南
Language Guide (proto3)