1. 關(guān)于 Protobuf
1.1 簡(jiǎn)介
Protocol Buffer戏蔑,簡(jiǎn)稱 Protobuf,是 Google 開發(fā)的一種數(shù)據(jù)描述語(yǔ)言捍壤。它是一種輕便高效的結(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)格式睹逃,適合做數(shù)據(jù)存儲(chǔ)或 RPC 數(shù)據(jù)交換格式,可用于數(shù)據(jù)傳輸量較大的即時(shí)通訊協(xié)議橄碾、數(shù)據(jù)存儲(chǔ)等場(chǎng)景。Protobuf 與語(yǔ)言無(wú)關(guān)、平臺(tái)無(wú)關(guān)埋市,目前提供了多種語(yǔ)言的 API污茵。
官方文檔:https://developers.google.com/protocol-buffers
開源地址:https://github.com/protocolbuffers/protobuf
1.2 優(yōu)勢(shì)
- 體積小速度快。像 XML 這種報(bào)文是基于文本格式的葬项,存在大量的描述信息泞当,雖然對(duì)于人來(lái)說可讀性更好,但增加了序列化時(shí)間玷室、網(wǎng)絡(luò)傳輸時(shí)間等零蓉。導(dǎo)致系統(tǒng)的整體性能下降笤受。而 PB 則將信息序列化為二進(jìn)制的格式穷缤,安全性提高的同時(shí)敌蜂,序列化后的數(shù)據(jù)大小縮小了3倍,序列化速度比 Json 快了20-100倍津肛,也必然會(huì)減小網(wǎng)絡(luò)傳輸時(shí)間章喉。
- 跨平臺(tái)跨語(yǔ)言。接收端和發(fā)送端只需要維護(hù)同一份 proto 文件即可身坐。proto 編譯器會(huì)根據(jù)不同的語(yǔ)言秸脱,生成對(duì)應(yīng)的代碼文件。
- 向后兼容性部蛇。不必破壞舊的數(shù)據(jù)格式摊唇,可以直接對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行更新。
2. 原理分析
通過本質(zhì)探究涯鲁,了解 Protobuf 為何如此高效巷查。
2.1 編碼背景
- 信源編碼:信源編碼是一種以提高通信有效性為目的而對(duì)信源符號(hào)進(jìn)行的變換,或者說為了減少或消除信源利余度而進(jìn)行的信源符號(hào)變換抹腿。具體說岛请,就是針對(duì)信源輸出符號(hào)序列的統(tǒng)計(jì)特性來(lái)尋找某種方法,把信源輸出符號(hào)序列變換為最短的碼字序列警绩,使后者的各碼元所載荷的平均信息量最大崇败,同時(shí)又能保證無(wú)失真地恢復(fù)原來(lái)的符號(hào)序列。信源編碼的作用之一是肩祥,即通常所說的數(shù)據(jù)壓縮后室;作用之二是將信源的模擬信號(hào)轉(zhuǎn)化成數(shù)字信號(hào),以實(shí)現(xiàn)模擬信號(hào)的數(shù)字化傳輸』旌荩現(xiàn)代通信應(yīng)用中常見的信源編碼方式有:Huffman編碼岸霹、算術(shù)編碼、L-Z編碼檀蹋,這三種都是無(wú)損編碼松申;另外還有一些采用壓縮方式的有損編碼。同時(shí)無(wú)損編碼也根據(jù)"是否把一個(gè)傳輸單位編碼為固定長(zhǎng)度"區(qū)分為定長(zhǎng)編碼和變長(zhǎng)編碼俯逾。定長(zhǎng)編碼就是一個(gè)符號(hào)變換后的碼字的比特長(zhǎng)度是固定的贸桶,比如 ASCII、Unicode 都是定長(zhǎng)編碼桌肴,碼字是8比特皇筛,16比特。變長(zhǎng)編碼則是將信源符號(hào)映射為不同的碼字長(zhǎng)度坠七,比如 Huffman 編碼水醋,PB 編碼旗笔。
- 信道編碼:信道編碼是為了對(duì)抗信道中的噪音和衰減,通過增加冗余來(lái)提高抗干擾能力以及糾錯(cuò)能力拄踪。信道編碼的本質(zhì)是降低誤碼率蝇恶、增加通信的可靠性。數(shù)字信號(hào)在傳輸中往往由于各種原因惶桐,使得在傳送的數(shù)據(jù)流中產(chǎn)生誤碼撮弧,所以通過信道編碼這一環(huán)節(jié)來(lái)避免碼流傳輸中誤碼的發(fā)生。常用的處理技術(shù)有奇偶校驗(yàn)碼姚糊、糾錯(cuò)碼贿衍、信道交織編碼等。
- 簡(jiǎn)單來(lái)說救恨,信源編碼就是將信源產(chǎn)生的消息變換為數(shù)字序列的過程贸辈,主要目的是降低數(shù)據(jù)率,提高信息量效率肠槽,一般用來(lái)對(duì)視頻擎淤、音頻、數(shù)據(jù)進(jìn)行處理署浩。而信道編碼的主要目的是提高系統(tǒng)的抗干擾能力揉燃,比如糾錯(cuò)碼啊,卷積碼這類筋栋,可以檢測(cè)出信息是否有被傳錯(cuò)炊汤。
- 從通信角度來(lái)看,Protobuf 是一種變長(zhǎng)的無(wú)損的信源編碼弊攘。
2.2 整數(shù)的編碼優(yōu)化
varint 編碼:一般情況下抢腐,一個(gè) int 值看作4字節(jié),也就是所謂的定長(zhǎng)編碼襟交。PB 考慮到現(xiàn)實(shí)情況中迈倍,數(shù)值較大的數(shù)比數(shù)值較小的數(shù)更少地被使用這一事實(shí),采用了變長(zhǎng)編碼捣域。如果一個(gè)數(shù)能夠用1個(gè)字節(jié)來(lái)表示啼染,那就用一個(gè)字節(jié)來(lái)表示。如數(shù)值1就會(huì)被編碼為0000 0001焕梅,而不是把它編碼為0000 0000 0000 0000 0000 0000 0000 0001迹鹅。但是也由此產(chǎn)生一個(gè)問題,每個(gè)整數(shù)的編碼長(zhǎng)度可能不一樣贞言,如何區(qū)分邊界呢斜棚?PB 將每個(gè)字節(jié)拿出1比特最高位的那個(gè)比特 MSB(Most Significant Bit)來(lái)作為邊界的標(biāo)記(編碼是否為最后一個(gè)字節(jié)),1表示還沒有到最后一個(gè)字節(jié),0表示到了最后一個(gè)字節(jié)弟蚀。
規(guī)則如下 ↓
- 0xxx xxxx表示某個(gè)整數(shù)編碼后的結(jié)果是單個(gè)字節(jié)蚤霞,因?yàn)镸SB=0;
- 1xxx xxxx 0xxx xxxx表示某個(gè)整數(shù)編碼后的結(jié)果是2個(gè)字節(jié)义钉,因?yàn)榍耙粋€(gè)字節(jié)的MSB=1(編碼結(jié)果未結(jié)束)昧绣,后一個(gè)字節(jié)的MSB=0;
- 同理断医,三個(gè)字節(jié)滞乙、四個(gè)字節(jié)都用這種方法來(lái)表示邊界奏纪。
代碼如下 ↓
final void bufferUInt32NoTag(int value) {
if (HAS_UNSAFE_ARRAY_OPERATIONS) {
final long originalPos = position;
while (true) {
if ((value & ~0x7F) == 0) {
//最后一次取出最高位補(bǔ)0
UnsafeUtil.putByte(buffer, position++, (byte) value);
break;
} else {
UnsafeUtil.putByte(buffer, position++, (byte) ((value & 0x7F) | 0x80));
//取出后面7位鉴嗤,最高位補(bǔ)1
value >>>= 7;
}
}
int delta = (int) (position - originalPos);
totalBytesWritten += delta;
} else {
while (true) {
if ((value & ~0x7F) == 0) {
buffer[position++] = (byte) value;
totalBytesWritten++;
return;
} else {
buffer[position++] = (byte) ((value & 0x7F) | 0x80);
totalBytesWritten++;
value >>>= 7;
}
}
}
}
示例如下 ↓
- 0000 0001表示整數(shù)1;
- 1010 1100 0000 0010表示兩個(gè)字節(jié)的結(jié)果序调。將兩字節(jié)的MSB去掉為:0101100 0000010醉锅。由于 PB 對(duì)于多個(gè)字節(jié)的情況采用低字節(jié)優(yōu)先,即后面的字節(jié)要放在高位发绢,于是拼在一起的結(jié)果為:00000100101100硬耍,表示300這個(gè)整數(shù)值。(其實(shí)就是將數(shù)字的二進(jìn)制補(bǔ)碼的每7位分為一組边酒, 低7位先輸出经柴,編碼在前面,在輸出下一組墩朦,依次類推)
可以看到以上的變長(zhǎng)編碼方式坯认,在數(shù)據(jù)壓縮上能節(jié)省很多空間,不過它也存在以下幾個(gè)小缺點(diǎn) ↓
- 造成了比特的1/8的浪費(fèi)氓涣,一個(gè)很大的數(shù)將可能使用5個(gè)字節(jié)來(lái)表示牛哺。
- 負(fù)數(shù)需要10個(gè)字節(jié)顯示(因?yàn)樨?fù)數(shù)最高位是1,會(huì)被當(dāng)作很大的整數(shù)處理)
2.3 對(duì)象的編碼優(yōu)化
2.3.1 Protobuf 對(duì) key-value 中 key 的優(yōu)化:使用序號(hào) key 代替變量名
對(duì)于一個(gè)對(duì)象劳吠,里面包含多個(gè)變量引润,如何編碼呢?假設(shè)一個(gè)類的定義如下 ↓
Class Student {
String name;
String sex;
int age;
}
如果使用 XML痒玩,那么傳輸?shù)母袷饺缦?↓
<?xml version="1.0" encoding="UTF-8" ?>
<name>Bob</name>
<sex>male</sex>
<age>18</age>
如果使用 Json淳附,那么傳輸?shù)母袷饺缦?↓
{
"name": "Bob",
"sex": "male",
"age": "18"
}
而 Protobuf 認(rèn)為 "name"洛姑、"sex"严拒、"age" 這些變量名不應(yīng)該包含在傳輸消息中,因?yàn)榫幗獯a追他、傳輸這些信息也需要資源便瑟。Protobuf 為了節(jié)省空間缆毁,在通信雙方都保持一份文檔,記錄了變量名的編號(hào)到涂,比如上述三個(gè)變量名字分別編號(hào)為1脊框、2颁督、3。于是在序列化的時(shí)候浇雹,只需要傳輸下面的信息 ↓
1:"Bob", 2:"male", 3:"18"
由于對(duì)方也保留了一份編號(hào)文檔沉御,于是就可以反序列化了。這些編號(hào)本身也可以用上面對(duì)整數(shù)的編碼優(yōu)化方式進(jìn)行編碼昭灵。??
2.3.2 Protobuf 對(duì) key-value 中 value 的優(yōu)化
如果 value 為整數(shù)吠裆,那么直接使用前面提到的對(duì)整數(shù)的編碼優(yōu)化即可。即大多數(shù)整數(shù)只占一兩個(gè)字節(jié)烂完。
如果 value 為字符串试疙,這時(shí)候每個(gè)字節(jié)都拿出1個(gè) bit 來(lái)區(qū)分邊界就太浪費(fèi)空間了,而且字符串本身就是一個(gè)一個(gè)字節(jié)的抠蚣,被打亂后也會(huì)影響解碼效率祝旷。因此,Protobuf 將 value 長(zhǎng)度信息的指示可以放在 key 和 value 之間(長(zhǎng)度本身也是一個(gè)整數(shù)嘶窄,也能編碼優(yōu)化)怀跛。在解碼 value 時(shí),解析長(zhǎng)度就可以知道 value 值到哪里結(jié)束柄冲。不過也因此產(chǎn)生一個(gè)問題吻谋,比如整數(shù)這種情況,value 中已經(jīng)自帶了結(jié)束標(biāo)識(shí)符现横,那就不需要 value 的長(zhǎng)度指示信息了漓拾。因此 Protobuf 引入了 Type 類型,即提前告訴接收端 value 的類型长赞。Protobuf 將這個(gè) Type 信息放在了 key 中的最后 3 個(gè) bit 中晦攒。根據(jù)這個(gè) Type,即可讓接受端注意或者忽略 value 的長(zhǎng)度字段得哆。value 的類型在 Protobuf 中稱為 wire_type脯颜。主要有以下幾種 ↓
wire type = 0
// 0 表示這個(gè)Value是一個(gè)變長(zhǎng)整數(shù),比如int32, int64, uint32, uint64, sint32, sint64, bool, enum
wire type = 1
// 1 表示這個(gè)Value是一個(gè)64位的定長(zhǎng)數(shù)贩据,比如fixed64, sfixed64, double
wire type = 2
// 2 表示string, bytes等栋操,這些Value的長(zhǎng)度需要置于Key后面
wire type = 3
// 3 表示groups中的Start Group,就是有一組饱亮,3表示接下來(lái)的Value是第一組
wire type = 4
// 4 表示groups中的End Group
wire type = 5
// 5 表示32位固定長(zhǎng)度的fixed32, sfixed32, float等
2.4 示例
- 例子1:08 ac 02 這三個(gè)字節(jié)矾芙,分析如下 ↓
- 08 二進(jìn)制為 0000 1000,最高位 0 表示這是最后一個(gè)字節(jié)近上,去除最高位為 0001 000剔宪。
- 最后 3 個(gè) bit 為 Type 類型,000 表示 wire type = 0,前面的 0001 表示這是編號(hào)為1的變量葱绒。
- 后面的 ac 02感帅,寫成二進(jìn)制為 10101100 00000010,去掉最高位分隔符為 0101100 0000010地淀,因?yàn)榈妥止?jié)優(yōu)先失球,于是串起來(lái)為 0000010 0101100 = 300。
- 最終帮毁,08 ac 02 這三個(gè)字節(jié)解碼為編號(hào)為 1 的變量值為整數(shù) 300实苞。
- 例子2:12 07 74 65 73 74 69 6e 67 這九個(gè)字節(jié),分析如下 ↓
- 12 的二進(jìn)制為 0001 0010烈疚,最高位 0 表示這是最后一個(gè)字節(jié)黔牵,去除最高位為 0010 010。
- 最后 3 個(gè) bit 010 表示 wire type = 2胞得,前四位 0010 表示這是編號(hào)為 2 的變量荧止。
- 因?yàn)閣ire type = 2,表示 value 是 String阶剑、bytes 等變長(zhǎng)流。接下來(lái)要解碼 value 的長(zhǎng)度危号。
- 07 的二進(jìn)制為 0000 0111牧愁,最高位為 0,表示這是最后一個(gè)字節(jié)外莲,去除最高位后是 000 0111猪半,表示Value的長(zhǎng)度為 7,也就是后面的 7 個(gè)字節(jié):74 65 73 74 69 6e 67偷线。
- 這 7 個(gè)字節(jié)如果是 String磨确,那么根據(jù) ASCII 碼可解碼為:"testing"。
- 最終声邦,12 07 74 65 73 74 69 6e 67 這幾個(gè)字節(jié)解碼為編號(hào)為 2 的變量值為字符串"testing"乏奥。
2.5 總結(jié)
2.5.1 體積壓縮優(yōu)勢(shì)
由 2.4 的第二個(gè)例子,08 ac 02 12 07 74 65 73 74 69 6e 67 這九個(gè)字節(jié)等價(jià)于 Json 中的 ↓
{"testInt":"300", "testString":"testing"}
可看出亥曹,Json 使用了40個(gè)左右的字節(jié)邓了,而 Protocol 只使用了12個(gè)字節(jié),這也就解釋了為什么 Protobuf 將信息序列化為二進(jìn)制后媳瞪,體積縮小了3倍骗炉,也因此減少了數(shù)據(jù)網(wǎng)絡(luò)傳輸?shù)臅r(shí)間。
2.5.2 序列化速度優(yōu)勢(shì)
以 XML 的解包過程為例蛇受,XML 首先需要將得到的字符串轉(zhuǎn)換為 XML 文檔對(duì)象的結(jié)構(gòu)模型句葵,再?gòu)慕Y(jié)構(gòu)模型中讀取指定節(jié)點(diǎn)的字符串,最后再將這個(gè)字符串指定為某個(gè)對(duì)象的變量值。這個(gè)過程非常復(fù)雜乍丈,其中轉(zhuǎn)換文檔對(duì)象結(jié)構(gòu)模型的過程熊响,通常需要完成詞法文法分析等大量消耗 CPU 的復(fù)雜計(jì)算。而 Protobuf 只需要簡(jiǎn)單地將一個(gè)二進(jìn)制序列诗赌,按照指定的格式讀取賦值到某個(gè)對(duì)象的變量值即可汗茄。因此它的的序列化速度非常快铭若。
3. 在 Android 中的簡(jiǎn)單使用
分析完 Protobuf 的原理之后洪碳,便是開始學(xué)習(xí)如何使用。前面說到它可用于多種語(yǔ)言叼屠,且與平臺(tái)無(wú)關(guān)瞳腌。不過我只稍微學(xué)習(xí)了如何在 Android studio 中使用 Protobuf。
3.1 Gradle 配置
在根目錄的 build.gradle 中添加如下代碼 ↓
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.6' // 添加這行
}
}
在 module 的 build.gradle 中首先添加如下代碼 ↓
apply plugin: 'com.google.protobuf' // 添加插件
接著添加 protobuf 塊(與android同級(jí))↓
protobuf {
// 配置protoc編譯器
protoc {
artifact = 'com.google.protobuf:protoc:3.0.0-alpha-3'
}
plugins {
javalite {
artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0'
}
}
// 這里配置生成目錄镜雨,編譯后會(huì)在build的目錄下生成對(duì)應(yīng)的java文件
generateProtoTasks {
all().each { task ->
task.plugins {
javalite {}
}
}
}
}
再添加依賴 ↓
implementation 'com.google.protobuf:protobuf-lite:3.0.0'
此時(shí)可以編譯項(xiàng)目嫂侍,會(huì)生成 proto java class,這個(gè)類就是我們后面所要使用到的荚坞。
3.2 定義 proto 文件
一般是在 java/res 同級(jí)目錄下建立 proto 文件夾挑宠,然后再創(chuàng)建 .proto 文件。
我們?cè)?.proto 文件里定義數(shù)據(jù)結(jié)構(gòu)颓影。這里有份官網(wǎng)的示例代碼 ↓
package tutorial;
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
關(guān)鍵字說明 ↓ (更多內(nèi)容可以參考官網(wǎng) Protocol Buffer Language Guide)
syntax | 聲明版本各淀。例如上面代碼的syntax="proto3",如果沒有聲明诡挂,則默認(rèn)是proto2 |
package | 聲明包名 |
import | 導(dǎo)入包 |
java_package | 指定生成的類應(yīng)該放在什么Java包名下碎浇。如果你沒有顯式地指定這個(gè)值,則它簡(jiǎn)單地匹配由package 聲明給出的Java包名璃俗,但這些名字通常都不是合適的Java包名 (由于它們通常不以一個(gè)域名打頭) |
java_outer_classname | 定義應(yīng)該包含這個(gè)文件中所有類的類名奴璃。如果你沒有顯式地給定java_outer_classname ,則將通過把文件名轉(zhuǎn)換為首字母大寫來(lái)生成城豁。例如上面例子編譯生成的文件名和類名是AddressBookProtos |
message | 類似于java中的class關(guān)鍵字 |
repeated | 用于修飾屬性苟穆,表示對(duì)應(yīng)的屬性是個(gè)array |
optional | 可選字段,可以不傳入數(shù)據(jù)钮蛛,或者設(shè)置默認(rèn)值 |
required | 必填字段鞭缭,如果創(chuàng)建數(shù)據(jù)對(duì)象時(shí)不傳入?yún)?shù),編碼時(shí)就會(huì)拋出exception魏颓。使用required時(shí)要注意岭辣,如果你升級(jí)協(xié)議時(shí)把這個(gè)字段改為optional,接收方?jīng)]有升級(jí)甸饱,你發(fā)送的數(shù)據(jù)對(duì)方將無(wú)法解釋沦童。因此不建議使用它仑濒,一般只使用optional和repeated |
這里我跟著教程,定義了一個(gè)簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu) ↓
syntax = "proto3";
package tutorial;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
string phone = 4;
}
編譯后偷遗,在以下的目錄中會(huì)生成對(duì)應(yīng)的 java 文件 ↓
Dataformat.java 即是 dataformat.proto 生成的對(duì)應(yīng) java 文件墩瞳,里面代碼行數(shù)有點(diǎn)多。
3.3 獲取數(shù)據(jù)
通過網(wǎng)絡(luò)獲取數(shù)據(jù)流氏豌,然后解析成 proto 文件定義的格式 ↓
Observable.just("http://elyeproject.x10host.com/experiment/protobuf")
.map(new Function<String, Dataformat.Person>() {
@Override
public Dataformat.Person apply(String url) throws Exception {
OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
Call call = okHttpClient.newCall(request);
Response response = call.execute();
if (response.isSuccessful()) {
ResponseBody responseBody = response.body();
if (responseBody != null) {
return Dataformat.Person.parseFrom(responseBody.byteStream());
}
}
return null;
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Dataformat.Person>() {
@Override
public void accept(Dataformat.Person person) throws Exception {
Log.i(TAG, person.getName());
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
Log.i(TAG, throwable.getMessage());
}
});
該網(wǎng)站返回的數(shù)據(jù)如下 ↓
Android 獲取的數(shù)據(jù)如下 ↓
PS:doc-android-client 項(xiàng)目的 bitable module 主要實(shí)現(xiàn) native UI喉酌,它所引入的 bitable_bridge 包則負(fù)責(zé)業(yè)務(wù)邏輯。bitable_bridge 是個(gè) React Native 工程泵喘,邏輯是用 JS 寫的泪电,數(shù)據(jù)格式則為 Protobuf 。
4. 數(shù)據(jù)交互格式比較
4.1 XML纪铺、Json相速、Protobuf
Json | 一般的 web 項(xiàng)目中,最流行的主要還是 Json鲜锚。因?yàn)闉g覽器對(duì)于 Json 數(shù)據(jù)支持非常友好突诬,有很多內(nèi)建的函數(shù)支持。 Json 使用了鍵值對(duì)的方式芜繁,不僅壓縮了一定的數(shù)據(jù)空間旺隙,同時(shí)也具有可讀性。 |
XML | 在 webservice 中應(yīng)用最為廣泛浆洗,但是相比于 Json催束,它的數(shù)據(jù)更加冗余,因?yàn)樾枰蓪?duì)的閉合標(biāo)簽伏社。 |
Protobuf | 后起之秀,適合高性能塔淤,對(duì)響應(yīng)速度有要求的數(shù)據(jù)傳輸場(chǎng)景摘昌。因?yàn)槭嵌M(jìn)制數(shù)據(jù)格式,需要編碼和解碼高蜂。數(shù)據(jù)本身不具有可讀性聪黎。因此只能反序列化之后得到真正可讀的數(shù)據(jù)。 |
相對(duì)于其他兩種語(yǔ)言备恤,Protobuf 具有的優(yōu)勢(shì)如下 ↓
- 序列化后體積相比 Json 和 XML 很小稿饰,適合網(wǎng)絡(luò)傳輸;
- 支持跨平臺(tái)多語(yǔ)言露泊;
- 消息格式升級(jí)和兼容性還不錯(cuò)喉镰;
- 序列化和反序列化速度很快,快于 Json 的處理速度惭笑。
PS:雖然 Protobuf 并非像 Json 那樣直接明文顯示侣姆,不過我們只需定義對(duì)象結(jié)構(gòu)生真,然后由 Protbuf 庫(kù)去把對(duì)象自動(dòng)轉(zhuǎn)換成二進(jìn)制,用的時(shí)候再自動(dòng)反解碼過來(lái)捺宗。傳輸過程于我們而言是透明的柱蟀。我們只負(fù)責(zé)傳輸?shù)膶?duì)象就可以了,所以用起來(lái)很方便蚜厉。
結(jié)論:在一個(gè)需要大量的數(shù)據(jù)傳輸?shù)膱?chǎng)景中长已,如果數(shù)據(jù)量很大,那么選擇 Protobuf 可以明顯地減少數(shù)據(jù)量昼牛,減少網(wǎng)絡(luò) IO术瓮,從而減少傳輸所消耗的時(shí)間。
4.2 Protobuf 與 Json 速度比較
測(cè)試平臺(tái) | Android studio 3.2 |
所引用的庫(kù) | google.protobuf(proto3)匾嘱、google.gson.Gson |
目的 | 比較 Protobuf 與 Json 的序列化/反序列化速度 |
方法 | 控制變量法 |
過程簡(jiǎn)述 ↓
- 為 Protobuf 和 Json 創(chuàng)建一樣的數(shù)據(jù)結(jié)構(gòu)(.proto 文件 和 .class 文件)斤斧,然后存進(jìn)以下的數(shù)據(jù),當(dāng)然這些數(shù)據(jù)可以多次賦值霎烙、多次測(cè)試撬讽。
String name = "王小明";
int id = 15331016;
String email = "chen@bytedance.com";
String phone = "12345678910";
- Protobuf 操作
Dataformat.Person.Builder builder = Dataformat.Person.newBuilder();
// 存進(jìn)數(shù)據(jù)
builder.setName(name);
builder.setId(id);
builder.setEmail(email);
builder.setPhone(phone);
// 序列化
Dataformat.Person person_write = builder.build();
byte[] result = person_write.toByteArray();
// 反序列化
Dataformat.Person person_read = Dataformat.Person.parseFrom(result);
可以看到,protocol 序列化后得到的編碼結(jié)果為 49 個(gè)字節(jié)悬垃。
- Json 操作
Gson gson = new Gson();
// 存進(jìn)數(shù)據(jù)
Person person1 = new Person(name, id, email, phone);
// 序列化
String person1_write = gson.toJson(person1);
// 反序列化
Person person1_read = gson.fromJson(person1_write, Person.class);
序列化后的大小為 82 個(gè)字節(jié)游昼。
- Log輸出每個(gè)節(jié)點(diǎn)的時(shí)間,為了效果明顯尝蠕,每個(gè)(反)序列化操作重復(fù)進(jìn)行1000000次
結(jié)果分析 ↓ (單位:ms)
結(jié)論 ↓
Protobuf 的體積比 Json 小烘豌,(反)序列化速度比 Json 快,在數(shù)據(jù)傳輸上更具優(yōu)勢(shì)看彼。