Android Studio使用ProtocolBuffers

weixin_34236869發表於2017-04-01

protobuf的java庫比較大,為滿足android移動裝置在記憶體、效能等各方面的要求,google也推出了android定製版protobuf-lite庫。

使用步驟

  1. 在 .proto 檔案中定義訊息格式。
    語法學習可參考官方文件
    語法有syntax2 syntax3的區分,可在書寫schema的時候宣告用哪個版本,3相對2來說有更好的壓縮特性。
  2. 使用 Protocol Buffer 編譯器編譯生成所需的java檔案。
    編譯器生成及用法參見http://www.jianshu.com/p/e8712962f0e9
    每次手動執行 Protocol Buffers 編譯器將 .proto 檔案轉換為Java檔案顯然有點太麻煩,因此google提供了一個Android Studio gradle外掛 protobuf-gradle-plugin ,以便於在我們專案的編譯期間自動地執行 Protocol Buffers 編譯器。
  3. 使用Java Protocol Buffer API讀寫訊息。
    整個使用過程如下圖


    5142564-f9746141d496c255.png

    下面就以Demo展示protobuf-gradle-plugin+protobuf-lite庫實現訊息序列化和反序列化的過程。

gradle整合protobuf-gradle-plugin

app module的gradle指令碼如下:

apply plugin: 'com.android.application'
apply plugin: 'com.google.protobuf' //在gradle指令碼開始處宣告依賴的外掛

buildscript {
    repositories {
        mavenCentral()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.3'
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.0'//配置plugin的版本資訊
    }
}
android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.dy.testprotocolbuffer"
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
//編寫編譯任務,呼叫plugin編譯生成java檔案
protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.0.0'//編譯器版本
    }
    plugins {
        javalite {
            artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0'//指定當前工程使用的protobuf版本為javalite版,以生成javalite版的java類
        }
    }
    generateProtoTasks.generatedFilesBaseDir = "$projectDir/src/main/java" //指定編譯生成java類的存放位置
    generateProtoTasks {
        all().each { task ->
            task.plugins {
                javalite {
                    outputSubDir = '' //指定存放位置的二級目錄,這裡未指定
                }
            }
        }
    }
}
//指定原始.proto檔案的位置
android {
    sourceSets {
        main {
            java {
                srcDirs 'src/main/java'
            }
            proto {
                srcDirs 'src/main/proto'
            }
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.1.0'
    compile 'com.google.protobuf:protobuf-lite:3.0.0' //依賴protobuf-lite庫
}

工程目錄如下:

5142564-76cfe75ac9d527a3.png
pb工程目錄.png

關於protobuf-gradle-plugin的更多用法可參考官方文件https://github.com/google/protobuf-gradle-plugin

schema編寫(.proto檔案)

我們將以下Json示例轉為pb格式:

{
    "data":[
      {
        "datatype":1,
        "itemdata":
            {//共有欄位45個
              "sname":"\u5fae\u533b",
              "packageid":"330611",
              …
              "tabs":[
                        {
                          "type":1,
                          "f":"abc"
                        },
                        …
              ]
            }
      },
      …
    ],
    "hasNextPage":true,
    "dirtag":"soft"
  }

schema編寫如下:

syntax = "proto2";
package com.dy.messagepackdemo.protobuffer.model;

option java_package = "com.dy.messagepackdemo.protobuffer.model";
option java_outer_classname = "ResponsePB";

message Tab {
  required int32 type = 1;
  optional string f = 2;
}
message ItemData {
    required string sname = 1;
    required string packageid = 2;
    ...
    repeated Tab tabs = 45;
}
message DataItem {
    required int32 datatype = 1;
    required ItemData itemdata = 2;
}
message ResponsePB {
    repeated DataItem data = 1;
    required bool hasNextPage = 2;
    required string dirtag = 3;
}

這個schema已經將上面json示例的層次結構體現的很明顯了。簡單解釋一下各引數的意義及用法:

  • syntax指定用哪個版本的語法,proto3比2有更好的壓縮特性
  • package指定編譯生成java類的包名
  • java_outer_classname指定java類的類名
  • 修飾符:required表示必填,optional可選,repeated表示重複的list
  • 每個層級內的資料要按順序編號,也就是上一篇文中講的field_number
  • 每個結構體是一個message,message之間可以互相引用。

編譯

build工程,可以看到在java包下生成了debug目錄(由於當前是debug模式),再下面就是我們想要的包及schema編譯後的java檔案。


5142564-dba48c8c1fb75069.png

使用編譯生成的java類,就可以進行資料的序列化和反序列化了。

序列化操作

構造一個response資料並寫入檔案

public static void writeResponseToPbFile(String pbfilepath, ResponseJson responseJson) {
        File fproto = new File(pbfilepath);
        if (!fproto.exists()) {
            try {
                fproto.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //build response
        //構造builder
        ResponsePB.Response.Builder responseBuilder = ResponsePB.Response.newBuilder();
        //填充資料
        responseBuilder.setHasNextPage(resultJson.hasNextPage);
        responseBuilder.setDirtag(resultJson.dirtag);
        ...//此處省略若干行
        //結束 build
        ResponsePB.Response response = responseBuilder.build();
        //寫檔案
        try {
            FileOutputStream foProto = new FileOutputStream(fproto);
            response.writeTo(foProto);
            foProto.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

用法很簡單,生成物件的構造器builder,用提供的各種Set方法填充資料,最後build,二進位制資料就生成了。用法跟普通的pojo類沒有區別。

反序列化

將檔案中的資料解析到Response物件中

public static void parseXinruiPb(byte[] bytes) {
    ResponsePB.Response response = ResponsePB.Response.parseFrom(bytes);
    boolean hasNextPage = response.getHasNextPage();
    String dirtag = response.getDirtag();
    ...
}

用起來也非常簡單,parseFrom搞定。 值得一提的是,由於protobuf的儲存結構決定了它在進行資料解析的時候必須將整個資料完整解析一遍才能得到你想要的資料,也就是資料傳輸過程中所謂的封包-解析過程,這與json解析的過程類似,區別在於它對key鍵的特殊編碼,省去了字元匹配的過程。

更高階的用法--動態編譯

Protobuf 提供了 google::protobuf::compiler 包來完成動態編譯的功能。感興趣的同學可以自行研究。

相關文章