Protocol Buffer V3.3.0 在Andoroid中的使用介绍

前面翻译了谷歌的Proto3 语言指南

这一篇讲实战应用
先总结一下 Protocol Buffer

一、什么是Protocol Buffer

Google 出品 的一种结构化数据 的数据存储格式,类似于 XML、Json。但更小,更快,更简单。
它可以通过将 结构化的数据 进行 串行化(序列化),从而实现 数据存储 / RPC 数据交换的功能。

  • 序列化: 将 数据结构或对象 转换成 二进制串 的过程
  • 反序列化:将在序列化过程中所生成的二进制串 转换成 数据结构或者对象 的过程
1 使用场景

传输数据量大 & 网络环境不稳定 的数据存储、RPC 数据交换 的需求场景(如即时IM )

2 相对于其他数据存储格式 Protocol Buffer的优势与缺点
二、Window下 Protocol Buffer的安装环境

要使用Protocol Buffer,先要配好环境

1 下载Protocol Buffers v3.3.0

官方下载Protocol Buffers v3.3.0
需翻墙(翻墙都不会?!!还好我有百度云)
百度云下载

2 下载protoc-3.3.0-win32.zip

protoc-3.3.0-win32.zip

3 将protoc-3.3.0-win32.zip解压后的bin/protoc.exe文件拷贝到解压后的protobuf-3.3目录中,并配置好环境变量。

执行命令检查是否安装成功

protoc --version

这样环境算是搞好了

三、Protocol Buffer在JAVA中的应用

如果你对Proto的基本语法以及结构还不熟悉,那么请你先快速看一下这篇 Proto3 语言指南

1、创建 .proto 文件

我们先通过官方的Demo来感知一下 .proto 文件是怎么样的,然后根据它的格式创建你想要的结构文件。

addressbook.proto

// [START declaration]
syntax = "proto3";
package tutorial;
// [END declaration]

// [START java_declaration]
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
// [END java_declaration]

// [START csharp_declaration]
option csharp_namespace = "Google.Protobuf.Examples.AddressBook";
// [END csharp_declaration]

// [START messages]
message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;
}

// Our address book file is just one of these.
message AddressBook {
  repeated Person people = 1;
}
// [END messages]

解释一下上面部分关键字的定义

  • required 字段必须提供,否则消息将被认为是 "未初始化的 (uninitialized)"。尝试构建一个未初始化的消息将抛出一个 RuntimeException。解析一个未初始化的消息将抛出一个 IOException。
  • optional 字段可以设置也可以不设置。如果可选的字段值没有设置,则将使用默认值。默认值你可以自己定义,也可以用系统默认值:数字类型为0,字符串类型为空字符串,bools值为false。
  • repeated 字段可以重复任意多次 (包括0)(相当于JAVA中的list)
  • default 默认值
2、编译 .proto 文件

通过以下命令来编译 .proto 文件:

protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

-I--java_out 分别用于指定源目录 (放置应用程序源代码的地方 —— 如果没有提供则使用当前目录),目的目录 (希望放置生成的代码的位置),最后的参数为 .proto 文件的路径。
protoc会按照标准Java风格,生成Java类及目录结构。如对于上面的例子,会生成 com/example/tutorial/ 目录结构,及 AddressBookProtos.java 文件。

如我的addressbook.proto文件放在E盘下,执行:

E:\pro>protoc -I=E:/pro --java_out=E:/pro E:/pro/addressbook.proto

//或者
E:\pro>protoc -I=./ --java_out=./ ./addressbook.proto

最后生成此文件

3、但是当你把生成的文件放进项目中的时候,你会发现报错了
...
Error:(9, 26) 错误: 程序包com.google.protobuf不存在
...

解决方法一:利用eclipse程序进行编译 前面下载protoc-3.3.0-win32.zip
右键点击java文件夹下面的pom.xml文件


生成protobuf-java-3.3.0.jar

解决方法二:网上下一个protobuf-java-3.3.0.jar,如果找不到留言联系我

关于 Protocol Buffers 编译文件后的细节
1、简单分析通过protoc编译后的文件到底是有什么东西

这么简单的一个结构文件居然生成了快2700行的代码,看着就头痛

抽取一些对象以及它们相关的代码发现共同点:

  • 每个类都有它自己的 Builder 类,你可以用来创建那个类的实例。
  • 消息只有getters,而builders则同时具有getters和setters。
  • 每个字段都会有四个常用方法(hasX,getX,setX,clearX)
  • 定义了 repeated 之后会多( getXList()、getXCount、addXPhone)这几个方法。
  • accessor方法是如何以驼峰形式命名的,所以在命名.proto 文件字段时,尽量使用小写字母加下划线。
// required string name = 1;
public boolean hasName();
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;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);
public Builder setPhone(int index, PhoneNumber value);
public Builder addPhone(PhoneNumber value);
public Builder addAllPhone(Iterable<PhoneNumber> value);
public Builder clearPhone();

2 每个消息和builder类还包含大量的其它方法,来让你检查或管理整个消息
  • isInitialized() : 检查是否所有的required字段都已经被设置了。
  • toString() : 返回一个人类可读的消息表示,对调试特别有用。
  • mergeFrom(Message other): (只有builder可用) 将 other 的内容合并到这个消息中,覆写单数的字段,附接重复的。
  • clear(): (只有builder可用) 清空所有的元素为空状态。

更多信息,请参考 Message的完整API文档

3 解析和序列化

每个protocol buffer类都有使用protocol buffer 二进制格式写和读你所选择类型的消息的方法。这些方法包括:

  • byte[] toByteArray();: 序列化消息并返回一个包含它的原始字节的字节数组。
  • static Person parseFrom(byte[] data);: 从给定的字节数组解析一个消息。
  • void writeTo(OutputStream output);: 序列化消息并将消息写入 OutputStream。
  • static Person parseFrom(InputStream input);: 从一个 InputStream 读取并解析消息。
4 写消息

你要想把你个人信息写进AddressBook,流程如下:

  • 程序通过文件读取一个AddressBook。
  • 添加一个Person,并将新的AddressBook写回文件。
import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintStream;

class AddPerson {
  // This function fills in a Person message based on user input.
  static Person PromptForAddress(BufferedReader stdin,
                                 PrintStream stdout) throws IOException {
    Person.Builder person = Person.newBuilder();

    stdout.print("Enter person ID: ");
    person.setId(Integer.valueOf(stdin.readLine()));

    stdout.print("Enter name: ");
    person.setName(stdin.readLine());

    stdout.print("Enter email address (blank for none): ");
    String email = stdin.readLine();
    if (email.length() > 0) {
      person.setEmail(email);
    }

    while (true) {
      stdout.print("Enter a phone number (or leave blank to finish): ");
      String number = stdin.readLine();
      if (number.length() == 0) {
        break;
      }

      Person.PhoneNumber.Builder phoneNumber =
        Person.PhoneNumber.newBuilder().setNumber(number);

      stdout.print("Is this a mobile, home, or work phone? ");
      String type = stdin.readLine();
      if (type.equals("mobile")) {
        phoneNumber.setType(Person.PhoneType.MOBILE);
      } else if (type.equals("home")) {
        phoneNumber.setType(Person.PhoneType.HOME);
      } else if (type.equals("work")) {
        phoneNumber.setType(Person.PhoneType.WORK);
      } else {
        stdout.println("Unknown phone type.  Using default.");
      }

      person.addPhone(phoneNumber);
    }

    return person.build();
  }

  // Main function:  Reads the entire address book from a file,
  //   adds one person based on user input, then writes it back out to the same
  //   file.
  public static void main(String[] args) throws Exception {
    if (args.length != 1) {
      System.err.println("Usage:  AddPerson ADDRESS_BOOK_FILE");
      System.exit(-1);
    }

    AddressBook.Builder addressBook = AddressBook.newBuilder();

    // Read the existing address book.
    try {
      addressBook.mergeFrom(new FileInputStream(args[0]));
    } catch (FileNotFoundException e) {
      System.out.println(args[0] + ": File not found.  Creating a new file.");
    }

    // Add an address.
    addressBook.addPerson(
      PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),
                       System.out));

    // Write the new address book back to disk.
    FileOutputStream output = new FileOutputStream(args[0]);
    addressBook.build().writeTo(output);
    output.close();
  }
}

四、 Protocol Buffers 在Android中的使用
1 添加依赖
dependencies {
    ...
    compile 'com.google.protobuf:protobuf-java:3.3.0'
}

2、将刚编译好的文件对应包名放在你的项目目录下

放好之后,你可以通过如下方式去Builder 你的对象:

Person haiJia =
  Person.newBuilder()
    .setId(888)
    .setName("黄海佳")
    .setEmail("haijia@qq.com")
    .addPhone(
      Person.PhoneNumber.newBuilder()
        .setNumber("110-119")
        .setType(Person.PhoneType.HOME))
    .build();
3、写消息
public class AddPerson {


    // This function fills in a Person message based on user input.
    static Person PromptForAddress(BufferedReader stdin) throws IOException {
        Person.Builder person = Person.newBuilder();

        LogUtils.log("Enter person ID: ");
        person.setId(Integer.valueOf(stdin.readLine()));

        LogUtils.log("Enter name: ");
        person.setName(stdin.readLine());

        LogUtils.log("Enter email address (blank for none): ");
        String email = stdin.readLine();
        if (email.length() > 0) {
            person.setEmail(email);
        }

        while (true) {
            LogUtils.log("Enter a phone number (or leave blank to finish): ");
            String number = stdin.readLine();
            if (number.length() == 0) {
                break;
            }

            Person.PhoneNumber.Builder phoneNumber =
                    Person.PhoneNumber.newBuilder().setNumber(number);

            LogUtils.log("Is this a mobile, home, or work phone? ");
            String type = stdin.readLine();
            if (type.equals("mobile")) {
                phoneNumber.setType(Person.PhoneType.MOBILE);
            } else if (type.equals("home")) {
                phoneNumber.setType(Person.PhoneType.HOME);
            } else if (type.equals("work")) {
                phoneNumber.setType(Person.PhoneType.WORK);
            } else {
                LogUtils.log("Unknown phone type.  Using default.");
            }

            person.addPhone(phoneNumber);
        }

        return person.build();
    }

    // Main function:  Reads the entire address book from a file,
    //   adds one person based on user input, then writes it back out to the same
    //   file.
    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("Usage:  AddPerson ADDRESS_BOOK_FILE");
            System.exit(-1);
        }

        AddressBook.Builder addressBook = AddressBook.newBuilder();

        // Read the existing address book.
        try {
            addressBook.mergeFrom(new FileInputStream(args[0]));
        } catch (FileNotFoundException e) {
            System.out.println(args[0] + ": File not found.  Creating a new file.");
        }

        // Add an address.
        addressBook.addPerson(PromptForAddress(new BufferedReader(new InputStreamReader(System.in))));

        // Write the new address book back to disk.
        FileOutputStream output = new FileOutputStream(args[0]);
        addressBook.build().writeTo(output);
        output.close();
    }


}
4 读消息
class ListPeople {
  // Iterates though all people in the AddressBook and prints info about them.
  static void Print(AddressBook addressBook) {
    for (Person person: addressBook.getPersonList()) {
      System.out.println("Person ID: " + person.getId());
      System.out.println("  Name: " + person.getName());
      if (person.hasEmail()) {
        System.out.println("  E-mail address: " + person.getEmail());
      }

      for (Person.PhoneNumber phoneNumber : person.getPhoneList()) {
        switch (phoneNumber.getType()) {
          case MOBILE:
            System.out.print("  Mobile phone #: ");
            break;
          case HOME:
            System.out.print("  Home phone #: ");
            break;
          case WORK:
            System.out.print("  Work phone #: ");
            break;
        }
        System.out.println(phoneNumber.getNumber());
      }
    }
  }

  // Main function:  Reads the entire address book from a file and prints all
  //   the information inside.
  public static void main(String[] args) throws Exception {
    if (args.length != 1) {
      System.err.println("Usage:  ListPeople ADDRESS_BOOK_FILE");
      System.exit(-1);
    }

    // Read the existing address book.
    AddressBook addressBook =
      AddressBook.parseFrom(new FileInputStream(args[0]));

    Print(addressBook);
  }
}


五、使用protobuf-gradle-plugin

每次单独执行protoc编译 .proto 文件总是太麻烦,通过protobuf-gradle-plugin可以在编译我们的app时自动地编译 .proto 文件,这样就大大降低了我们在Android项目中使用 Protocol Buffers 的难度。

1 首先我们需要将 .proto 文件添加进我们的项目中,如:
2 然后修改 app/build.gradle 对protobuf gradle插件做配置:
buildscript {
 repositories {
     jcenter()
     mavenCentral()
 }
 dependencies {
     classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.0'
 }
}


3 添加protobuf块,对protobuf-gradle-plugin的执行做配置:
protobuf {
 protoc {
     path = '/usr/local/bin/protoc'
 }

 generateProtoTasks {
     all().each { task ->
         task.builtins {
             remove java
         }
         task.builtins {
             java { }
             // Add cpp output without any option.
             // DO NOT omit the braces if you want this builtin to be added.
             cpp { }
         }
     }
 }
}

protoc块用于配置Protocol Buffers编译器,这里我们指定用我们之前手动编译的编译器。
task.builtins的块必不可少,这个块用于指定我们要为那些编程语言生成代码,这里我们为C++和Java生成代码。缺少这个块的话,在编译时会报出如下的错误:

Information:Gradle tasks [:app:generateDebugSources, :app:mockableAndroidJar, :app:prepareDebugUnitTestDependencies, :app:generateDebugAndroidTestSources, :netlib:generateDebugSources, :netlib:mockableAndroidJar, :netlib:prepareDebugUnitTestDependencies, :netlib:generateDebugAndroidTestSources]
Error:Execution failed for task ':app:generateDebugProto'.
> protoc: stdout: . stderr: /media/data/CorpProjects/netlibdemo/app/build/extracted-protos/main: warning: directory does not exist.
/media/data/CorpProjects/netlibdemo/app/src/debug/proto: warning: directory does not exist.
/media/data/CorpProjects/netlibdemo/app/build/extracted-protos/debug: warning: directory does not exist.
/media/data/CorpProjects/netlibdemo/app/build/extracted-include-protos/debug: warning: directory does not exist.
/media/data/CorpProjects/netlibdemo/app/src/debug/proto: warning: directory does not exist.
/media/data/CorpProjects/netlibdemo/app/build/extracted-protos/debug: warning: directory does not exist.
/media/data/CorpProjects/netlibdemo/app/build/extracted-include-protos/debug: warning: directory does not exist.
Missing output directives.

提示说没有指定输出目录的路径。
这是由于 protobuf-gradle-plugin 执行的protobuf编译器命令的参数是在protobuf-gradle-plugin/src/main/groovy/com/google/protobuf/gradle/GenerateProtoTask.groovy中构造的:

def cmd = [ tools.protoc.path ]
 cmd.addAll(dirs)

 // Handle code generation built-ins
 builtins.each { builtin ->
   String outPrefix = makeOptionsPrefix(builtin.options)
   cmd += "--${builtin.name}_out=${outPrefix}${getOutputDir(builtin)}"
 }

 // Handle code generation plugins
 plugins.each { plugin ->
   String name = plugin.name
   ExecutableLocator locator = tools.plugins.findByName(name)
   if (locator == null) {
     throw new GradleException("Codegen plugin ${name} not defined")
   }
   String pluginOutPrefix = makeOptionsPrefix(plugin.options)
   cmd += "--${name}_out=${pluginOutPrefix}${getOutputDir(plugin)}"
   cmd += "--plugin=protoc-gen-${name}=${locator.path}"
 }

 if (generateDescriptorSet) {
   def path = getDescriptorPath()
   // Ensure that the folder for the descriptor exists;
   // the user may have set it to point outside an existing tree
   def folder = new File(path).parentFile
   if (!folder.exists()) {
     folder.mkdirs()
   }
   cmd += "--descriptor_set_out=${path}"
   if (descriptorSetOptions.includeImports) {
     cmd += "--include_imports"
   }
   if (descriptorSetOptions.includeSourceInfo) {
     cmd += "--include_source_info"
   }
 }

 cmd.addAll protoFiles
 logger.log(LogLevel.INFO, cmd.toString())
 def stdout = new StringBuffer()
 def stderr = new StringBuffer()
 Process result = cmd.execute()
 result.waitForProcessOutput(stdout, stderr)
 def output = "protoc: stdout: ${stdout}. stderr: ${stderr}"
 logger.log(LogLevel.INFO, cmd)
 if (result.exitValue() == 0) {
   logger.log(LogLevel.INFO, output)
 } else {
   throw new GradleException(output)
 }

可以看到,输出目录是由builtins构造的。

4 指定 .proto 文件的路径
sourceSets {
     main {
         java {
             srcDir 'src/main/java'
         }
         proto {
             srcDir 'src/main/proto'
         }
     }
 }

这样我们就不用那么麻烦每次手动执行protoc了。
对前面的protobuf块做一点点修改,我们甚至来编译protobuf编译器都不需要了。修改如下:

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.0.0'
    }

    generateProtoTasks {
        all().each { task ->
            task.builtins {
                remove java
            }
            task.builtins {
                java { }
                cpp { }
            }
        }
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容