1. 程式人生 > >Android:手把手帶你分析 Protocol Buffer使用 原始碼

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` )
  1. Google 出品 (開源)
  2. Protocol Buffer 目前有兩個版本:proto2proto3
  3. 因為proto3 還是beta 版,所以本次講解是 proto2

2. 作用

通過將 結構化的資料 進行 序列化(序列化),從而實現 資料儲存 / RPC 資料交換的功能

  1. 序列化: 將 資料結構或物件 轉換成 二進位制串 的過程
  2. 反序列化:將在序列化過程中所生成的二進位制串 轉換成 資料結構或者物件 的過程

3. 特點

  • 對比於 常見的 XML、Json 資料儲存格式,Protocol Buffer有如下特點:

Protocol Buffer 特點

4. 應用場景

傳輸資料量大 & 網路環境不穩定 的資料儲存、RPC 資料交換 的需求場景

如 即時IM (QQ、微信)的需求場景

總結

傳輸資料量較大的需求場景下,Protocol BufferXML、Json 更小、更快、使用 & 維護更簡單!

5. 使用流程

6. 知識基礎

6.1 網路通訊協議

  • 序列化 & 反序列化 屬於通訊協議的一部分
  • 通訊協議採用分層模型:TCP/IP
    模型(四層) & OSI 模型 (七層)

網路通訊協議

  • 序列化 / 反序列化 屬於 TCP/IP模型 應用層 和 OSI`模型 展示層的主要功能:

    1. (序列化)把 應用層的物件 轉換成 二進位制串
    2. (反序列化)把 二進位制串 轉換成 應用層的物件
  • 所以, Protocol Buffer屬於 TCP/IP模型的應用層 & OSI模型的展示層

6.2 資料結構、物件與二進位制串

不同的計算機語言中,資料結構,物件以及二進位制串的表示方式並不相同。

a. 對於資料結構和物件

  • 對於面向物件的語言(如Java):物件 = Object = 類的例項化;在Java中最接近資料結構 即 POJOPlain Old Java Object),或Javabean(只有 setter/getter 方法的類)

  • 對於半面向物件的語言(如C++),物件 = class,資料結構 = struct

b. 二進位制串

  • 對於C++,因為具有記憶體操作符,所以 二進位制串 容易理解:C++的字串可以直接被傳輸層使用,因為其本質上就是以 '\0' 結尾的儲存在記憶體中的二進位制串
  • 對於 Java,二進位制串 = 位元組陣列 =byte[]
    1. byte 屬於 Java 的八種基本資料型別
    2. 二進位制串 容易和 String混淆:String 一種特殊物件(Object)。對於跨語言間的通訊,序列化後的資料當然不能是某種語言的特殊資料型別。

6.3 Protocol Buffer 的序列化原理 & 資料儲存方式

7. 原始碼分析

7.1 核心分析內容

在下面的原始碼分析中,主要分析的是:
1. Protocol Buffer具體是如何進行序列化 & 反序列化 ?
2. 與 XML、Json 相比,Protocol Buffer 序列化 & 反序列化速度 為什麼如此快 & 序列化後的資料體積這麼小?

本文主要講解Protocol BufferAndroid 平臺上的應用,即 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主要程式碼結構

下面先講解 Protocol Buffer 的固有程式碼結構

a. MessageLite介面 & Message介面


  • 作用:定義了訊息序列化 & 反序列化 的方法
  • 二者關係:MessageLite介面是Message的父介面
  • 二者區別:Message介面中增加了Protocol Buffer對反射(reflection) & 描述符 (descriptor) 功能的支援
  1. 在其實現類 GeneratedMessage中給予了最小功能的實現
  2. 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 介面
  • 作用:定義了一系列對 訊息中欄位操作的方法

    1. 如初始化、錯誤設定等
    2. 關於對訊息物件中欄位的設定、修改等是通過 <訊息名>OrBuilder 介面 繼承該介面進行功能的擴充套件
  • 二者關係:MessageOrBuilderLite介面是MessageOrBuilder的父介面

public interface MessageOrBuilder extends MessageLiteOrBuilder {

...
// 定義了一系列對 訊息中欄位操作的方法
// 下面會詳細說明
}

下面,我將按照Protocol Buffer的使用步驟逐步進行原始碼分析,即分析Protocol Buffer根據 .proto檔案生成的程式碼結構

再次貼出Protocol Buffer的主要類結構:

Protocol Buffer主要程式碼結構

步驟1:通過 訊息類的內部類Builder類 構造 訊息類的訊息構造器

Demo.Person.Builder personBuilder =  Demo.Person.newBuilder();
// Demo類:.proto檔案的option java_outer_classname = "Demo";
// Person類:訊息物件類
// Builder類:訊息構造器類
// 下面會逐一說明

下面我先介紹三個比較重要的類:Demo類、Person類、Builder類。

Protocol Buffer主要程式碼結構

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);
  }
    }
  • 序列化過程描述:

    1. 建立一個輸出流
    2. 計算出序列化後的二進位制流長度,分配該長度的空間,以備以後將每個欄位填充到該空間
    3. 判斷每個欄位是否有設定值,有值才會進行編碼

      optionalrepeated 欄位沒有被設定欄位值,那麼該欄位在序列化時的資料中是完全不存在的,即不進行序列化(少編碼一個欄位);在解碼時,相應的欄位才會被設定為預設值

    4. 根據 欄位標識號&資料型別 將 欄位值 通過不同的編碼方式進行編碼

      以下是 不同欄位資料型別 對應的編碼方式
      資料型別 對應的編碼方式

    5. 將已經編碼成功的位元組寫入到 輸出流,即資料儲存,最終等待輸出

  • 從上面可以看出:序列化 主要是經過了 編碼 & 資料儲存兩個過程

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開發中,效能優化策略十分重要 因為其決定了應