一、Protobuf簡介
protocolbuffer(以下簡稱protobuf)是google 的是一種輕便高效的結(jié)構(gòu)化數(shù)據(jù)存儲格式,作用形同于xml和json。它獨(dú)立于語言,獨(dú)立于平臺。google 提供了多種語言的實(shí)現(xiàn):java、c#、c++、go 和 python,每一種實(shí)現(xiàn)都包含了相應(yīng)語言的編譯器以及庫文件。由于它是一種二進(jìn)制的格式,比使用 xml 進(jìn)行數(shù)據(jù)交換快許多??梢园阉糜诜植际綉?yīng)用之間的數(shù)據(jù)通信或者異構(gòu)環(huán)境下的數(shù)據(jù)交換。作為一種效率和兼容性都很優(yōu)秀的
二進(jìn)制數(shù)據(jù)傳輸格式,可以用于諸如網(wǎng)絡(luò)傳輸、配置文件、數(shù)據(jù)存儲等諸多領(lǐng)域。
Protobuf是一種平臺無關(guān)、語言無關(guān)、可擴(kuò)展且輕便高效的序列化數(shù)據(jù)結(jié)構(gòu)的協(xié)議,可以用于網(wǎng)絡(luò)通信和數(shù)據(jù)存儲。 可簡單類比于 XML ,其具有以下特點(diǎn):
- 1、語言無關(guān)、平臺無關(guān)。即 ProtoBuf 支持 Java、C++、Python 等多種語言,支持多個平臺
- 2、高效。即比 XML 更?。? ~ 10倍)、更快(20 ~ 100倍)、更為簡單
- 3、擴(kuò)展性、兼容性好。你可以更新數(shù)據(jù)結(jié)構(gòu),而不影響和破壞原有的舊程
1、優(yōu)點(diǎn)
使用代碼生成器生成的代碼來讀寫這個數(shù)據(jù)結(jié)構(gòu)。你甚至可以在無需重新部署程序的情況下更新數(shù)據(jù)結(jié)構(gòu)。只需使用 Protobuf 對數(shù)據(jù)結(jié)構(gòu)進(jìn)行一次描述,即可利用各種不同語言或從各種不同數(shù)據(jù)流中對你的結(jié)構(gòu)化數(shù)據(jù)輕松讀寫。
它有一個非常棒的特性,即“向后”兼容性好,人們不必破壞已部署的、依靠“老”數(shù)據(jù)格式的程序就可以對數(shù)據(jù)結(jié)構(gòu)進(jìn)行升級。這樣您的程序就可以不必?fù)?dān)心因?yàn)橄⒔Y(jié)構(gòu)的改變而造成的大規(guī)模的代碼重構(gòu)或者遷移的問題。因?yàn)樘砑有碌南⒅械?field 并不會引起已經(jīng)發(fā)布的程序的任何改變。
2、缺點(diǎn)
Protbuf 與 XML 相比也有不足之處。它功能簡單,無法用來表示復(fù)雜的概念。
XML 已經(jīng)成為多種行業(yè)標(biāo)準(zhǔn)的編寫工具,Protobuf 只是 Google 公司內(nèi)部使用的工具,在通用性上還差很多。
由于文本并不適合用來描述數(shù)據(jù)結(jié)構(gòu),所以 Protobuf 也不適合用來對基于文本的標(biāo)記文檔(如 HTML)建模。另外,由于 XML 具有某種程度上的自解釋性,它可以被人直接讀取編輯,在這一點(diǎn)上 Protobuf 不行,它以二進(jìn)制的方式存儲,除非你有 .proto 定義,否則你沒法直接讀出 Protobuf 的任何內(nèi)容。
二、使用protobuf步驟
android中使用protobuf,過程是這樣的:
- 1、定義proto文件;
- 2、使用該文件生成對應(yīng)的java類;
- 3、利用該java類實(shí)現(xiàn)數(shù)據(jù)傳輸;
從以上過程中就可以看出,我們并不是直接使用proto文件,而是對應(yīng)的java類,如何根據(jù)proto文件生成java類呢?官方推薦的是命令行的方式生成,但是Android Studio生成方式更加簡單,這里直接介紹as生成方式(同樣適用服務(wù)端開發(fā)工具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中加入如下配置,頂部加上應(yīng)用插件
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的目錄下生成對應(yīng)的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相關(guān)依賴
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文件夾,并新建對應(yīng)的proto文件,這里以LoginRequest.proto為例

LoginRequest.proto文件內(nèi)容為:
syntax = "proto3"; //聲明 proto 協(xié)議版本 ( proto2 和 proto3 在定義看數(shù)據(jù)結(jié)構(gòu)時有些差別)
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中生成對應(yīng)的java文件和C++文件,拷出來備用。

三、Window 系統(tǒng)下使用protobuf
-
1)、protobuf的安裝
protoc的源碼和各個系統(tǒng)的預(yù)編譯包
選擇對應(yīng)的安裝文件下載:
在path中添加到環(huán)境變量中
2)、protobuf的使用方法
查看protoc的版本
protoc --version #查看protoc的版本
代碼轉(zhuǎn)換顯例(把目錄切換到 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("123@QQ.com")
.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
- 該數(shù)字只要不重復(fù),可以定義為任何數(shù)字,不需要總是從1或者0開始
- 這個數(shù)字表示在序列化數(shù)組里面的順序
注意:定義的proto文件中的編號對應(yīng)字段名前后可以不行同,但是編號對應(yīng)的字段類型的相同。
五、語法
syntax = "proto3";
message LoginRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 文件的第一行指定了你正在使用proto3語法:如果你沒有指定這個,編譯器會使用proto2。這個指定語法行必須是文件的非空非注釋的第一個行。
- SearchRequest消息格式有3個字段,在消息中承載的數(shù)據(jù)分別對應(yīng)于每一個字段。其中每個字段都有一個名字和一種類型。
關(guān)鍵字
syntax:聲明版本。例如上面syntax="proto3",如果沒有聲明,則默認(rèn)是proto2。package:聲明包名.import:導(dǎo)入包。類似于java,例如上面導(dǎo)入了timestamp.proto包。java_package:指定生成的類應(yīng)該放在什么Java包名下。如果你沒有顯式地指定這個值,則它簡單地匹配由package 聲明給出的Java包名,但這些名字通常都不是合適的Java包名 (由于它們通常不以一個域名打頭)。java_outer_classname:定義應(yīng)該包含這個文件中所有類的類名。如果你沒有顯式地給定java_outer_classname ,則將通過把文件名轉(zhuǎn)換為首字母大寫來生成。例如上面例子編譯生成的文件名和類名是AddressBookProtos。message:類似于java中的class關(guān)鍵字。repeated:用于修飾屬性,表示對應(yīng)的屬性是個array。
更多的關(guān)鍵字可以參考官方文檔,這里不做介紹。
1)、標(biāo)量數(shù)值類型

2)、枚舉
當(dāng)需要定義一個消息類型的時候,可能想為一個字段指定某“預(yù)定義值序列”中的一個值。例如,假設(shè)要為每一個SearchRequest消息添加一個 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一個。 其實(shí)可以很容易地實(shí)現(xiàn)這一點(diǎn):通過向消息定義中添加一個枚舉(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,這是因?yàn)椋?/p>
- 必須有有一個0值,我們可以用這個0值作為默認(rèn)值。
- 這個零值必須為第一個元素,為了兼容proto2語義,枚舉類的第一個值總是默認(rèn)值。
3)、其他消息類型
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
所指定的消息字段修飾符必須是如下之一:
singular:一個格式良好的消息應(yīng)該有0個或者1個這種字段(但是不能超過1個)。repeated:在一個格式良好的消息中,這種字段可以重復(fù)任意多次(包括0次)。重復(fù)的值的順序會被保留。
在proto3中,repeated的標(biāo)量域默認(rèn)情況蝦使用packed。
4)、嵌套類型
你可以在其他消息類型中定義、使用消息類型,在下面的例子中,Result消息就定義在SearchResponse消息內(nèi),如:
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定義中每個字段都有一個唯一的編號,這些編號被用來在二進(jìn)制消息體中識別你定義的這些字段,一旦你的message類型被用到后就不應(yīng)該在修改這些編號了。注意在將message編碼成二進(jìn)制消息體時字段編號1-15將會占用1個字節(jié),16-2047將占用兩個字節(jié)。所以在一些頻繁使用用的message中,你應(yīng)該總是先使用前面1-15字段編號。
你可以指定的最小編號是1,最大是2E29 - 1(536,870,911)。其中19000到19999是給protocol buffers實(shí)現(xiàn)保留的字段標(biāo)號,定義message時不能使用。同樣的你也不能重復(fù)使用任何當(dāng)前message定義里已經(jīng)使用過和預(yù)留的字段編號。

