Android:手把手帶你分析 Protocol Buffer使用 原始碼
前言
- 習慣用
Json、XML
資料儲存格式的你們,相信大多都沒聽過Protocol Buffer
Protocol Buffer
其實 是Google
出品的一種輕量 & 高效的結構化資料儲存格式,效能比Json、XML
真的強!太!多!由於
Google
出品,我相信Protocol Buffer
已經具備足夠的吸引力今天,我將講解
Protocol Buffer
使用的原始碼分析,並解決以下兩個問題:
a.Protocol Buffer
序列化速度 & 反序列化速度為何如此快
b.Protocol Buffer
的資料壓縮效果為何如此好,即序列化後的資料量體積小
目錄
1. 定義
一種 結構化資料 的資料儲存格式(類似於 `XML、Json` )
Protocol Buffer
目前有兩個版本:proto2
和proto3
- 因為
proto3
還是beta 版,所以本次講解是proto2
2. 作用
通過將 結構化的資料 進行 序列化(序列化),從而實現 資料儲存 / RPC 資料交換的功能
- 序列化: 將 資料結構或物件 轉換成 二進位制串 的過程
- 反序列化:將在序列化過程中所生成的二進位制串 轉換成 資料結構或者物件 的過程
3. 特點
- 對比於 常見的
XML、Json
資料儲存格式,Protocol Buffer
有如下特點:
4. 應用場景
傳輸資料量大 & 網路環境不穩定 的資料儲存、RPC 資料交換 的需求場景
如 即時IM (QQ、微信)的需求場景
總結
在 傳輸資料量較大的需求場景下,Protocol Buffer
比XML、Json
更小、更快、使用 & 維護更簡單!
5. 使用流程
6. 知識基礎
6.1 網路通訊協議
- 序列化 & 反序列化 屬於通訊協議的一部分
- 通訊協議採用分層模型:
TCP/IP
OSI
模型 (七層)
序列化 / 反序列化 屬於
TCP/IP
模型 應用層 和 OSI`模型 展示層的主要功能:- (序列化)把 應用層的物件 轉換成 二進位制串
- (反序列化)把 二進位制串 轉換成 應用層的物件
所以,
Protocol Buffer
屬於TCP/IP
模型的應用層 &OSI
模型的展示層
6.2 資料結構、物件與二進位制串
不同的計算機語言中,資料結構,物件以及二進位制串的表示方式並不相同。
a. 對於資料結構和物件
對於面向物件的語言(如
Java
):物件 =Object
= 類的例項化;在Java
中最接近資料結構 即POJO
(Plain Old Java Object
),或Javabean
(只有setter/getter
方法的類)對於半面向物件的語言(如
C++
),物件 =class
,資料結構 =struct
b. 二進位制串
- 對於
C++
,因為具有記憶體操作符,所以 二進位制串 容易理解:C++
的字串可以直接被傳輸層使用,因為其本質上就是以'\0'
結尾的儲存在記憶體中的二進位制串 - 對於
Java
,二進位制串 = 位元組陣列 =byte[]
byte
屬於Java
的八種基本資料型別- 二進位制串 容易和
String
混淆:String
一種特殊物件(Object)。對於跨語言間的通訊,序列化後的資料當然不能是某種語言的特殊資料型別。
6.3 Protocol Buffer
的序列化原理 & 資料儲存方式
7. 原始碼分析
7.1 核心分析內容
在下面的原始碼分析中,主要分析的是:
1. Protocol Buffer
具體是如何進行序列化 & 反序列化 ?
2. 與 XML、Json
相比,Protocol Buffer
序列化 & 反序列化速度 為什麼如此快 & 序列化後的資料體積這麼小?
本文主要講解
Protocol Buffer
在Android
平臺上的應用,即Java
平臺
7.2 例項的訊息物件內容
Demo.proto
package protocobuff_Demo;
option java_package = "com.carson.proto";
option java_outer_classname = "Demo";
// 生成 Person 訊息物件
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 phone = 4;
}
message AddressBook {
repeated Person person = 1;
}
7.3 使用步驟
- 原始碼分析的路徑會依據
Protocol Buffer
的使用步驟進行 - 具體使用步驟如下:(看註釋)
// 步驟1:通過 訊息類的內部類Builder類 構造 訊息類的訊息構造器
Demo.Person.Builder personBuilder = Demo.Person.newBuilder();
// 步驟2:設定你想要設定的欄位為你選擇的值
personBuilder.setName("Carson");
personBuilder.setEmail("[email protected]");
personBuilder.setId(123);
Demo.Person.PhoneNumber.Builder phoneNumber = Demo.Person.PhoneNumber.newBuilder();
phoneNumber.setType( Demo.Person.PhoneType.HOME);
phoneNumber.setNumber("0157-23443276");
// 步驟3:通過 訊息構造器 建立 訊息類 物件
Demo.Person person = personBuilder.build();
// 步驟4:序列化和反序列化訊息(兩種方式)
/*方式1:直接 序列化 和 反序列化 訊息 */
// a.序列化
byte[] byteArray1 = person.toByteArray();
// 把 person訊息類物件 序列化為 byte[]位元組陣列
System.out.println(Arrays.toString(byteArray1));
// 檢視序列化後的位元組流
// b.反序列化
try {
Demo.Person person_Request = Demo.Person.parseFrom(byteArray1);
// 當接收到位元組陣列byte[] 反序列化為 person訊息類物件
System.out.println(person_Request.getName());
System.out.println(person_Request.getId());
System.out.println(person_Request.getEmail());
// 輸出反序列化後的訊息
} catch (IOException e) {
e.printStackTrace();
}
/*方式2:通過輸入/ 輸出流(如網路輸出流) 序列化和反序列化訊息 */
// a.序列化
ByteArrayOutputStream output = new ByteArrayOutputStream();
try {
person.writeTo(output);
// 將訊息序列化 並寫入 輸出流(此處用 ByteArrayOutputStream 代替)
} catch (IOException e) {
e.printStackTrace();
}
byte[] byteArray = output.toByteArray();
// 通過 輸出流 轉化成二進位制位元組流
// b. 反序列化
ByteArrayInputStream input = new ByteArrayInputStream(byteArray);
// 通過 輸入流 接收訊息流(此處用 ByteArrayInputStream 代替)
try {
Demo.Person person_Request = Demo.Person.parseFrom(input);
// 通過輸入流 反序列化 訊息
System.out.println(person_Request.getName());
System.out.println(person_Request.getId());
System.out.println(person_Request.getEmail());
// 輸出訊息
} catch (IOException e) {
e.printStackTrace();
}
7.4 詳細分析
- 詳細分析前,先看下
Protocol Buffer
的主要類結構:
下面先講解 Protocol Buffer
的固有程式碼結構
a. MessageLite
介面 & Message
介面
- 作用:定義了訊息序列化 & 反序列化 的方法
- 二者關係:
MessageLite
介面是Message
的父介面 - 二者區別:
Message
介面中增加了Protocol Buffer
對反射(reflection
) & 描述符 (descriptor
) 功能的支援
- 在其實現類
GeneratedMessage
中給予了最小功能的實現Message
介面更加靈活 & 具備擴充套件性;但編碼效率低,佔用資源高
public interface Message extends MessageLite {
...
// 定義了訊息 序列化 & 反序列化 的方法
...
// Builders訊息構造器 設定欄位的方法
// 下面會詳細說明
}
- 設定:
// 通過在.proto檔案 新增option選項 選擇 MessageLite介面
option optimize_for = LITE_RUNTIME;
// 不設定則預設 使用 Message介面
// 選擇標準:一般來說,反射 & 描述符 的功能都不會使用到,所以為了避免佔用資源多、試用包大,選擇MessageLite介面會更好
// 此處就採用Message介面進行講解
b. GeneratedMessageLite
類 & GeneratedMessage類
- 定義:是
.proto
檔案生成的所有Java
類的父類
分別是
MessageLite
介面 &Message
介面的實現類 - 作用:定義了 序列化 & 反序列化 的方法的具體實現
- 二者區別:同上
c.
MessageOrBuilder
介面 & MessageOrBuilderLite
介面
作用:定義了一系列對 訊息中欄位操作的方法
- 如初始化、錯誤設定等
- 關於對訊息物件中欄位的設定、修改等是通過
<訊息名>OrBuilder
介面 繼承該介面進行功能的擴充套件
二者關係:
MessageOrBuilderLite
介面是MessageOrBuilder
的父介面
public interface MessageOrBuilder extends MessageLiteOrBuilder {
...
// 定義了一系列對 訊息中欄位操作的方法
// 下面會詳細說明
}
下面,我將按照Protocol Buffer
的使用步驟逐步進行原始碼分析,即分析Protocol Buffer
根據 .proto
檔案生成的程式碼結構
再次貼出
Protocol Buffer
的主要類結構:
步驟1:通過 訊息類的內部類Builder類 構造 訊息類的訊息構造器
Demo.Person.Builder personBuilder = Demo.Person.newBuilder();
// Demo類:.proto檔案的option java_outer_classname = "Demo";
// Person類:訊息物件類
// Builder類:訊息構造器類
// 下面會逐一說明
下面我先介紹三個比較重要的類:Demo
類、Person
類、Builder
類。
a. Demo
類
Protocol Buffer
編譯器會為每個.proto
檔案生成一個Protocol Buffer
類類名 取決於.proto檔案的語句
option java_outer_classname = "Demo"
類裡包含了訊息物件類 / 介面 & 訊息構造器類
public final class Demo {
// 訊息物件類 介面
public interface PersonOrBuilder extends com.google.protobuf.MessageOrBuilder {
...
}
// 訊息物件 類
// Protocol Buffer類的內部類
public static final class Person extends
com.google.protobuf.GeneratedMessage implements PersonOrBuilder {
...
// 訊息構造器類
// 訊息物件類的內部類
public static final class Builder extends
com.google.protobuf.GeneratedMessage.Builder<Builder> implements
com.carson.proto.Demo.PersonOrBuilder {
..
}
}
b. Person
類
Protocol Buffer
編譯器為 每個訊息物件 生成一個 訊息物件類類名 = 訊息物件 名
作用:定義了訊息 序列化 & 反序列化的方法 & 訊息欄位的獲取方法
// 訊息類被宣告為final類,即不可以在被繼承(自淚花)
// Google 官方文件中給予了明確的說明:因為子類化將會破壞序列化和反序列化的過程
public static final class Person extends
com.google.protobuf.GeneratedMessage implements PersonOrBuilder {
// 1. 繼承自GeneratedMessage類
// 2. 實現了PersonOrBuilder介面 ->>關注1
// Person類的構造方法
private Person(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
super(builder);
this.unknownFields = builder.getUnknownFields();
}
private Person(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); }
// 特別注意
// 由於Person類裡的構造方法都是 私有屬性(Private),所以建立例項物件時只能通過內部類Builder類進行建立,而不能獨自建立
// 下面會詳細說明
...
// 序列化 & 反序列化方法(兩種方式)
<-- 方式1:直接序列化和反序列化 訊息 -->
public byte[] toByteArray();
// 序列化訊息
public Person parseFrom(byte[] data);
// 反序列化
<-- 方式2:通過輸入/ 輸出流(如網路輸出流) 序列化和反序列化訊息 -->
public OutputStream writeTo(OutputStream output);
public byte[] toByteArray();
// 序列化訊息
public Person parseFrom(InputStream input);
// 反序列化
// 下面會對序列化 & 反序列化進行更加的詳細說明
// 獲取 訊息欄位的方法
// 只含包含欄位的getters方法
// required string name = 1;
public boolean hasName();// 如果欄位被設定,則返回true
public java.lang.String getName();
// required int32 id = 2;
public boolean hasId();
public int getId();
// optional string email = 3;
public boolean hasEmail();
public String getEmail();
// repeated .tutorial.Person.PhoneNumber phone = 4;
// 重複(repeated)欄位有一些額外方法
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
// 列表大小的速記
// 作用:通過索引獲取和設定列表的特定元素的getters和setters
}
<-- 關注1:PersonOrBuilder介面 -->
// Protocol Buffer編譯器為 每個訊息物件 生成一個<訊息名>OrBuilder 介面
// 作用:定義了 訊息中所有欄位的 get方法(用於獲取欄位值) & has<field>方法(用以判斷欄位是否設值)
// 使用了設計模式中的建造者模式:通過 對應的Builder介面 完成對 所有訊息欄位 的修改操作
public interface PersonOrBuilder extends
com.google.protobuf.MessageOrBuilder {
// 繼承自 MessageOrBuilder 介面 或 MessageOrBuilderLite 介面
/**
* <code>required string name = 1;</code>
*/
boolean hasName();
java.lang.String getName();
com.google.protobuf.ByteString
getNameBytes();
/**
* <code>required int32 id = 2;</code>
*/
boolean hasId();
int getId();
/**
* <code>optional string email = 3;</code>
*/
boolean hasEmail();
java.lang.String getEmail();
com.google.protobuf.ByteString
getEmailBytes();
/**
* <code>repeated .protocobuff_Demo.Person.PhoneNumber phone = 4;</code>
*/
java.util.List<com.carson.proto.Demo.Person.PhoneNumber>
getPhoneList();
com.carson.proto.Demo.Person.PhoneNumber getPhone(int index);
int getPhoneCount();
java.util.List<? extends com.carson.proto.Demo.Person.PhoneNumberOrBuilder>
getPhoneOrBuilderList();
com.carson.proto.Demo.Person.PhoneNumberOrBuilder getPhoneOrBuilder(
int index);
}
// 由於Person類實現了該介面,所以也能獲取訊息中的欄位值
c. Builder
類
Protocol Buffer
編譯器為 每個訊息物件 在訊息類內部生成一個 訊息構造器類(Builder
類)- 作用:定義了 訊息中所有欄位的
get
方法(用於獲取欄位值) &has<field>
方法(用以判斷欄位是否設值) 和set
方法(用於設定欄位值)
與 訊息類
Person
類作用類似,但多了set
方法(用於設定欄位值)
public static final class Builder extends
com.google.protobuf.GeneratedMessage.Builder<Builder> implements
com.carson.proto.Demo.PersonOrBuilder {
...
// 定義了 訊息中所有欄位的 `get`方法(用於獲取欄位值) & `has<field>`方法(用以判斷欄位是否設值) 和 `set`方法(用於設定欄位值)
// 下面會進行更加詳細的說明
}
看完Demo
類、Person
類、Builder
類 三個類後,終於可以開始分析
- 具體使用
// 通過 訊息類的內部類Builder類 構造 訊息類的訊息構造器
Demo.Person.Builder personBuilder = Demo.Person.newBuilder();
- 原始碼分析
public static final class Builder extends
com.google.protobuf.GeneratedMessage.Builder<Builder> implements
com.carson.proto.Demo.PersonOrBuilder {
// 該靜態方法是 Builder類 的建造者模式方法
// 通過建造者模式 建立 Builder類 的 例項,即訊息構造器的例項
public static Builder newBuilder() {
return Builder.create(); --> 關注1
}
<--Builder.create() -->
private static Builder create() {
return new Builder();
// 建立並返回一個訊息構造器的例項
}
}
步驟2:通過 訊息構造器設定 訊息中欄位的值
- 具體使用
// 步驟2:通過 訊息構造器設定 訊息中欄位的值
personBuilder.setName("Carson");// 在定義.proto檔案時,該欄位的欄位修飾符是required,所以必須賦值
personBuilder.setEmail("[email protected]");// 在定義.proto檔案時,該欄位的欄位修飾符是required,所以必須賦值
personBuilder.setId(123); // 在定義.proto檔案時,該欄位的欄位修飾符是optional,所以可賦值 / 不賦值(不賦值時將使用預設值)
- 原始碼分析
public static final class Builder extends
com.google.protobuf.GeneratedMessage.Builder<Builder> implements
com.carson.proto.Demo.PersonOrBuilder {
// Builder定義了大量對訊息中欄位操作的方法
// 1. Builder類中修改訊息欄位的方法
// 標準的JavaBeans風格:含getters和setters
// required string name = 1;
public boolean hasName();// 如果欄位被設定,則返回true
public java.lang.String getName();
public Builder setName(String value);
public Builder clearName(); // 將欄位設定回它的空狀態
// required int32 id = 2;
public boolean hasId();
public int getId();
public Builder setId(int value);
public Builder clearId();
// optional string email = 3;
public boolean hasEmail();
public String getEmail();
public Builder setEmail(String value);
public Builder clearEmail();
// repeated .tutorial.Person.PhoneNumber phone = 4;
// 重複(repeated)欄位有一些額外方法
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
// 列表大小的速記
// 作用:通過索引獲取和設定列表的特定元素的getters和setters
// 用Name欄位為例詳細分析這一系列方法
// a. 判斷是否設定了Name欄位值
// 設定了則返回true
/**
* <code>required string Name = 1;</code>
*/
public boolean hasName() {
return ((bitField0_ & 0x00000001) == 0x00000001);
}
// b. 獲得欄位的值
public java.lang.String getName() {
java.lang.Object ref = number_;
if (!(ref instanceof java.lang.String)) {
com.google.protobuf.ByteString bs =
(com.google.protobuf.ByteString) ref;
java.lang.String s = bs.toStringUtf8();
if (bs.isValidUtf8()) {
number_ = s;
}
return s;
} else {
return (java.lang.String) ref;
}
// c. 設定Number欄位
// 該函式呼叫後hasName函式將返回true
public Builder setName(
java.lang.String value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000001;
number_ = value;
onChanged();
return this;
}
// 返回值為Builder物件,這樣就可以讓呼叫者在一條程式碼中方便的連續設定多個欄位
// 如:Message.setID(100).setName("MyName");
// 2. 清空當前物件中的所有設定
// 呼叫該函式後,所有欄位的 has*欄位名*()都會返回false。
public Builder clear() {
super.clear();
number_ = "";
bitField0_ = (bitField0_ & ~0x00000001);
type_ = com.carson.proto.Demo.Person.PhoneType.HOME;
bitField0_ = (bitField0_ & ~0x00000002);
return this;
}
// 3. 其他可能會用到的方法
// 由於使用頻率較低,此處不展開分析,僅作定性分析
public Builder isInitialized()
// 檢查所有 required 欄位 是否都已經被設定
public Builder toString() :
// 返回一個人類可讀的訊息表示(用於除錯)
public Builder mergeFrom(Message other)
// 將 其他內容 合併到這個訊息中,覆寫單數的欄位,附接重複的。
public Builder clear()
// 清空所有的元素為空狀態。
}
步驟3:通過 訊息構造器 建立 訊息類 物件
- 具體使用
// 通過 訊息構造器 建立 訊息類 物件
Demo.Person person = personBuilder.build();
- 原始碼分析
public static final class Builder extends
com.google.protobuf.GeneratedMessage.Builder<Builder> implements
com.carson.proto.Demo.PersonOrBuilder {
public com.carson.proto.Demo.Person build() {
com.carson.proto.Demo.Person result = buildPartial();
// 建立訊息類物件 ->>關注1
if (!result.isInitialized()) {
// 判斷是否初始化
// 標準是:required欄位是否被賦值,如果沒有則丟擲異常
throw newUninitializedMessageException(result);
}
return result;
}
<-- buildPartial()方法 -->
public com.carson.proto.Demo.Person buildPartial() {
com.carson.proto.Demo.Person result = new com.carson.proto.Demo.Person(this);
// 在訊息構造器Builder類裡建立訊息類的物件
// 下面是對欄位的標識號進行判斷 & 賦值
int from_bitField0_ = bitField0_;
int to_bitField0_ = 0;
if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
to_bitField0_ |= 0x00000001;
}
result.name_ = name_;
if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
to_bitField0_ |= 0x00000002;
}
result.id_ = id_;
if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
to_bitField0_ |= 0x00000004;
}
result.email_ = email_;
if (phoneBuilder_ == null) {
if (((bitField0_ & 0x00000008) == 0x00000008)) {
phone_ = java.util.Collections.unmodifiableList(phone_);
bitField0_ = (bitField0_ & ~0x00000008);
}
result.phone_ = phone_;
} else {
result.phone_ = phoneBuilder_.build();
}
result.bitField0_ = to_bitField0_;
onBuilt();
return result;
// 最終返回一個已經賦值標識號的訊息類物件
}
再次說明:由於訊息類Person
類裡的構造方法都是 私有屬性(Private
),所以建立例項物件時只能通過內部類Builder類進行建立而不能獨自建立。
步驟4:序列化和反序列化訊息(兩種方式)
- 終於來到
Protocol Buffer
的重頭戲了:序列化和反序列化訊息 - 此處會著重說明序列化 & 反序列化的原理,從而解釋為什麼
Protocol Buffer
的效能如此地好(序列化速度快 & 序列化後資料量小) - 具體使用
// 步驟4:序列化和反序列化訊息(兩種方式)
/*方式1:直接 序列化 和 反序列化 訊息 */
// a.序列化
byte[] byteArray1 = person.toByteArray();
// b.反序列化
Demo.Person person = Demo.Person.parseFrom(byteArray1);
/*方式2:通過輸入/ 輸出流(如網路輸出流) 序列化和反序列化訊息 */
// a.序列化
ByteArrayOutputStream output = new ByteArrayOutputStream();
person.writeTo(output);
// 將訊息寫入 輸出流(此處用 ByteArrayOutputStream 代替)
byte[] byteArray = output.toByteArray();
// 通過 輸出流 序列化訊息
// b. 反序列化
ByteArrayInputStream input = new ByteArrayInputStream(byteArray);
// 通過 輸入流 接收訊息流(此處用 ByteArrayInputStream 代替)
Demo.Person person_Request = Demo.Person.parseFrom(input);
// 通過輸入流 反序列化 訊息
下面將分析介紹 兩種序列化 & 反序列化方式 的原始碼分析
- 方式1的原始碼分析
/*方式1:直接 序列化 和 反序列化 訊息 */
// a.序列化(返回一個位元組陣列)
byte[] byteArray1 = person.toByteArray();
// b.反序列化(從一個位元組資料進行反序列化)
Demo.Person person = Demo.Person.parseFrom(byteArray1);
a. 序列化分析:person.toByteArray()
// 僅貼出關鍵程式碼
public static final class Person extends
com.google.protobuf.GeneratedMessage implements PersonOrBuilder {
// 直接進行序列化成二進位制位元組流
public byte[] toByteArray() {
try {
final byte[] result = new byte[getSerializedSize()];
// 建立一個位元組陣列
final CodedOutputStream output = CodedOutputStream.newInstance(result);
// 建立一個輸出流
writeTo(output);
// 將 訊息類物件 序列化到輸出流裡 -->關注1
}
<-- writeTo()分析-->
// 以下便是真正序列化的過程
public void writeTo(com.google.protobuf.CodedOutputStream output)
throws java.io.IOException {
// 計算出序列化後的二進位制流長度,分配該長度的空間,以備以後將每個欄位填充到該空間
getSerializedSize();
// 判斷每個欄位是否有設定值
// 有才會進行寫操作(編碼)
if (((bitField0_ & 0x00000001) == 0x00000001)) {
output.writeBytes(1, getNameBytes()); // ->>關注2
// 根據 欄位標識號 將已經賦值的 欄位標識號和欄位值 通過不同的編碼方式進行編碼
// 最終寫入到輸出流
}
if (((bitField0_ & 0x00000002) == 0x00000002)) {
output.writeInt32(2, id_); // ->>關注3
}
if (((bitField0_ & 0x00000004) == 0x00000004)) {
output.writeBytes(3, getEmailBytes());
}
// NumberPhone為巢狀的message
// 通過遞迴進行裡面每個欄位的序列化
for (int i = 0; i < phone_.size(); i++) {
output.writeMessage(4, phone_.get(i)); // ->>關注4
}
getUnknownFields().writeTo(output);
}
<-- 關注2:String欄位型別的編碼方式:LENGTH_DELIMITED-->
public void writeBytes(final int fieldNumber, final ByteString value)
throws IOException {
writeTag(fieldNumber, WireFormat.WIRETYPE_LENGTH_DELIMITED);
// 將欄位的標識號和欄位型別按照Tag = (field_number << 3) | wire_type組成Tag
writeBytesNoTag(value);
// 對欄位值進行編碼
}
public void writeBytesNoTag(final ByteString value) throws IOException {
writeRawVarint32(value.size());
// 對於String欄位型別的欄位長度採用了Varint的編碼方式
writeRawBytes(value);
// 對於String欄位型別的欄位值採用了utf-8的編碼方式
}
<-- 關注3:Int32欄位型別的編碼方式:VARINT-->
public void writeInt32(final int fieldNumber, final int value)
throws IOException {
writeTag(fieldNumber, WireFormat.WIRETYPE_VARINT);
// 將欄位的標識號和欄位型別按照Tag = (field_number << 3) | wire_type組成Tag
writeInt32NoTag(value);
// 對欄位值採用了Varint的編碼方式
}
<-- 關注4:Message欄位型別的編碼方式:LENGTH_DELIMITED-->
public void writeMessage(final int fieldNumber, final MessageLite value)
throws IOException {
writeTag(fieldNumber, WireFormat.WIRETYPE_LENGTH_DELIMITED);
writeMessageNoTag(value);
}
}
序列化過程描述:
- 建立一個輸出流
- 計算出序列化後的二進位制流長度,分配該長度的空間,以備以後將每個欄位填充到該空間
- 判斷每個欄位是否有設定值,有值才會進行編碼
若
optional
或repeated
欄位沒有被設定欄位值,那麼該欄位在序列化時的資料中是完全不存在的,即不進行序列化(少編碼一個欄位);在解碼時,相應的欄位才會被設定為預設值 根據 欄位標識號&資料型別 將 欄位值 通過不同的編碼方式進行編碼
以下是 不同欄位資料型別 對應的編碼方式
將已經編碼成功的位元組寫入到 輸出流,即資料儲存,最終等待輸出
從上面可以看出:序列化 主要是經過了 編碼 & 資料儲存兩個過程
b. 反序列化Demo.Person person = Demo.Person.parseFrom(byteArray1);
// 僅貼出關鍵程式碼
public static final class Person extends
com.google.protobuf.GeneratedMessage implements PersonOrBuilder {
public static com.carson.proto.Demo.Person parseFrom(byte[] data)
throws com.google.protobuf.InvalidProtocolBufferException {
return PARSER.parseFrom(data); // ->>關注1
}
<-- 關注1-->
public MessageType parseFrom(byte[] data)
throws InvalidProtocolBufferException {
return parseFrom(data, EMPTY_REGISTRY);// ->>關注2
}
<-- 關注2-->
public MessageType parseFrom(byte[] data,
ExtensionRegistryLite extensionRegistry)
throws InvalidProtocolBufferException {
return parseFrom(data, 0, data.length, extensionRegistry); // ->>關注3
}
<-- 關注3-->
public MessageType parseFrom(byte[] data, int off, int len,
ExtensionRegistryLite extensionRegistry)
throws InvalidProtocolBufferException {
return checkMessageInitialized(
parsePartialFrom(data, off, len, extensionRegistry));// ->>關注4
}
<-- 關注4 -->
public MessageType parsePartialFrom(byte[] data, int off, int len,
ExtensionRegistryLite extensionRegistry)
throws InvalidProtocolBufferException {
try {
CodedInputStream input = CodedInputStream.newInstance(data, off, len);
// 建立一個輸入流
MessageType message = parsePartialFrom(input, extensionRegistry);
// ->>關注5
try {
input.checkLastTagWas(0);
return message;
}
}
<-- 關注5 -->
public Person parsePartialFrom(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
return new Person(input, extensionRegistry);
// 最終建立並返回了一個訊息類Person的物件
// 建立時會呼叫Person 帶有該兩個引數的構造方法 ->>關注6
}
};
<-- 關注6:呼叫的構造方法 -->
// 作用:反序列化訊息
private Person(
com.google.protobuf.CodedInputStream input,
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
throws com.google.protobuf.InvalidProtocolBufferException {
initFields();
int mutable_bitField0_ = 0;
com.google.protobuf.UnknownFieldSet.Builder unknownFields =
com.google.protobuf.UnknownFieldSet.newBuilder();
try {
boolean done = false;
while (!done) {
int tag = input.readTag();
// 通過While迴圈 從 輸入流 依次讀tag值
// 根據從tag值解析出來的標識號,通過case分支讀取對應欄位型別的資料並通過反編碼對欄位進行解析 & 賦值
// 欄位越多,case分支越多
switch (tag) {
case 0:
done = true;
break;
default: {
if (!parseUnknownField(input, unknownFields,
extensionRegistry, tag)) {
done = true;
}
break;
}
case 10: {
com.google.protobuf.ByteString bs = input.readBytes();
bitField0_ |= 0x00000001;
name_ = bs;
break;
}
case 16: {
bitField0_ |= 0x00000002;
id_ = input.readInt32();
break;
}
case 26: {
com.google.protobuf.ByteString bs = input.readBytes();
bitField0_ |= 0x00000004;
email_ = bs;
break;
}
case 34: {
if (!((mutable_bitField0_ & 0x00000008) == 0x00000008)) {
phone_ = new java.util.ArrayList<com.carson.proto.Demo.Person.PhoneNumber>();
mutable_bitField0_ |= 0x00000008;
}
phone_.add(input.readMessage(com.carson.proto.Demo.Person.PhoneNumber.PARSER, extensionRegistry));
break;
}
}
}
// 最終是返回了一個 訊息類 物件
總結
反序列化的過程總結如下:
1. 從 輸入流 依次讀 欄位的標籤值(即Tag
值)
2. 根據從標籤值(即Tag
值)值解析出來的標識號(Field_Number
),判斷對應的資料型別(wire_type
)
3. 呼叫對應的解碼方法 解析 對應欄位值
下圖用例項來看看 Protocol Buffer
如何解析經過Varint
編碼的位元組
相關推薦
Android:手把手帶你分析 Protocol Buffer使用 原始碼
前言 習慣用 Json、XML 資料儲存格式的你們,相信大多都沒聽過Protocol Buffer Protocol Buffer 其實 是 Google出品的一種輕量 & 高效的結構化資料儲存格式,效能比 Json、XML 真的強!太!多!
Android:手把手帶你 深入讀懂 Retrofit 2.0 原始碼
前言 在Android開發中,網路請求十分常用 而在Android網路請求庫中,Retrofit是當下最熱的一個網路請求庫 Github截圖 今天,我將手把手帶你深入剖析Retrofit v2.0的原始碼,希望你們會喜歡 請儘量在PC端
Android:手把手帶你深入剖析 Retrofit 2.0 原始碼
前言 在Andrroid開發中,網路請求十分常用 而在Android網路請求庫中,Retrofit是當下最熱的一個網路請求庫 今天,我將手把手帶你深入剖析Retrofit v2.0的原始碼,希望你們會喜歡 目錄 1. 簡介
Android:手把手帶你入門神祕的 Rxjava
前言 Rxjava由於其基於事件流的鏈式呼叫、邏輯簡潔 & 使用簡單的特點,深受各大 Android開發者的歡迎。 本文主要: 面向 剛接觸Rxjava的初學者 提供了一份 清晰、簡潔、易懂的Rxjava入門教程 涵蓋 基本介紹、
Android性能優化:手把手帶你全面了解 內存泄露 & 解決方案
new t 簡單介紹 新建 cti 接口 stat you bit ray . 簡介 即 ML (Memory Leak)指 程序在申請內存後,當該內存不需再使用 但 卻無法被釋放 & 歸還給 程序的現象2. 對應用程序的影響 容易使得應用程序發生內存溢出,即 OO
Android效能優化:手把手帶你全面瞭解 記憶體洩露 & 解決方案
前言 在Android中,記憶體洩露的現象十分常見;而記憶體洩露導致的後果會使得應用Crash 本文 全面介紹了記憶體洩露的本質、原因 & 解決方案,最終提供一些常見的記憶體洩露分析工具,希望你們會喜歡。 掃碼檢視公眾號: 目錄 1. 簡介 即 ML (
Android效能優化:手把手帶你全面瞭解 繪製優化
前言 在 Android開發中,效能優化策略十分重要 本文主要講解效能優化中的繪製優化,希望你們會喜歡。 目錄 1. 影響的效能 繪製效能的好壞 主要影響 :Android應用中的頁面顯示速度 2. 如何影響效能 繪製
曹工雜談:手把手帶你讀懂 JVM 的 gc 日誌
一、前言 今天下午本來在划水,突然看到微信聯絡人那一個紅點點,看了下,應該是部落格園的朋友。加了後,這位朋友問了我一個問題: 問我,這兩塊有什麼關係? 看到這段 gc 日誌,一瞬間腦子還有點懵,嗯,這個可能要翻下書了,周志明的 Java 虛擬機器那本神書裡面有講,我果斷地打
GitHub 熱點速覽 Vol.26:手把手帶你做資料庫
![](https://img2020.cnblogs.com/blog/759200/202006/759200-20200629224830448-312237694.png) 作者:HelloGitHub-**小魚乾** > 摘要:手把手帶你學知識,應該是學習新知識最友好的姿勢了。toyDB
深入淺出!阿里P7架構師帶你分析ArrayList集合原始碼,建議是先收藏再看!
# ArrayList簡介 ArrayList 是 Java 集合框架中比較常用的資料結構了。ArrayList是可以**動態增長和縮減的索引序列**,內部封裝了一個**動態再分配的Object[]陣列** ![](https://upload-images.jianshu.io/upload_image
手把手帶你畫一個 時尚儀表盤 Android 自己定義View
androi alias 屬性 extend 三角函數 blank xutils content 還在 拿到美工效果圖。咱們程序猿就得畫得一模一樣。 為了不被老板噴,僅僅能多練啊。 聽說你認為前面幾篇都so easy,那今天就帶你做個相對照較復雜的。
手把手帶你擼一套Android簡易ORM框架
ORM概念 實體模型建立 註解列 ID 主鍵 自增長 資料表的列
手把手帶你打造一個 Android 熱修復框架(上篇)
本文來自網易雲社群作者:王晨彥前言熱修復和外掛化是目前 Android 領域很火熱的兩門技術,也是 Android 開發工程師必備的技能。目前比較流行的熱修復方案有微信的 Tinker,手淘的 Sophix,美團的 Robust,以及 QQ 空間熱修復方案。QQ 空間熱修復方
手把手帶你畫一個動態錯誤提示 Android自定義view
嗯。。再差1篇就可以獲得持之以恆徽章了,今天帶大家畫一個比較簡單的view。 轉載請註明出處:http://blog.csdn.net/wingichoy/article/details/50477108 廢話不多說,看效果圖: 首先 建構函式 測量... 這裡就一筆帶
手把手帶你畫一個 時尚儀表盤 Android 自定義View
拿到美工效果圖,咱們程式設計師就得畫得一模一樣。 為了不被老闆噴,只能多練啊。 聽說你覺得前面幾篇都so easy,那今天就帶你做個相對比較複雜的。 轉載請註明出處:http://blog.csdn.net/wingichoy/article/details/50468
Python資料分析:手把手教你用Pandas生成視覺化圖表
大家都知道,Matplotlib 是眾多 Python 視覺化包的鼻祖,也是Python最常用的標準視覺化庫,其功能非常強大,同時也非常複雜,想要搞明白並非易事。但自從Python進入3.0時代以後,pandas的使用變得更加普及,它的身影經常見於市場分析、爬蟲、金融分析以及
Android自定義EditText:手把手教你做一款含一鍵刪除&自定義樣式的SuperEditText
前言 Android開發中,EditText的使用 非常常見 本文將手把手教你做一款 附帶一鍵刪除功能 & 自定義樣式豐富的 SuperEditText控制元件,希望你們會喜歡。 目錄 1. 簡介 一款 附帶一鍵刪除功
Android 自定義控制元件-Canvas和Paint繪圖詳解-手把手帶你繪製一個時鐘.
,Android - Paint基礎 在自定義控制元件時,經常需要使用canvas、paint等,在canvas類中,繪畫基本都是靠drawXXX()方法來完成的,在這些方法中,很多時候都需要用到paint型別的引數, Paint作為一個非常重要的元素,功能
手把手帶你打造一個 Android 熱修復框架
前言 熱修復和外掛化是目前 Android 領域很火熱的兩門技術,也是 Android 開發工程師必備的技能。 目前比較流行的熱修復方案有微信的 Tinker,手淘的 Sophix,美團的 Robust,以及 QQ 空間熱修復方案。 QQ 空間熱修復方案使用Java實現,比較容易上手。 如
Android效能優化:手把手教你如何讓App更快、更穩、更省(含記憶體、佈局優化等)
前言 在 Android開發中,效能優化策略十分重要 因為其決定了應