Android 解析 Protocol Buffers 格式数据

protocolbuffers/protobuf GitHub 地址

protocolbuffers/protobuf 官网

一、前言

Protocol Buffers 简称为 protobuf,是 Google 公司开发的一种数据描述语言,类似于 XML 能够将结构化数据序列化,可用于数据存储、通信协议等方面。相比于现在流行的 XML 以及 JSON 格式存储数据,通过 Protocol Buffers 来定义的文件体积更小,解析速度更快。

假设你要为具有姓名和电子邮件的人建模,使用 XML 格式:

  
    John Doe
    [email protected]
  

而对应的 protocol buffers 格式:

# Textual representation of a protocol buffer.
# This is *not* the binary format used on the wire.
person {
  name: "John Doe"
  email: "[email protected]"
}

1.1 编写 protobuf 文件

.proto 文件中的定义很简单:为要序列化的每个数据结构添加 message,然后为 message 中的每个字段指定名称和类型。通俗地说,message 类似 Java 中的类,里面可以定义我们需要的属性。下面是官方的一个示例,定义一个 addressbook.proto 文件。地址簿中的每个人都有姓名,ID,邮件地址和联系电话。

syntax = "proto2";

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;
}

如上所示,语法类似于 C++ 或 Java。

.proto 文件以包声明为开头,这有助于防止不同项目之间的命名冲突。在 Java 中,包的名称被当作 Java 包,除非你明确指定了 java_package,就像例子中的一样。即使你提供了 java_package,你仍然应该定义一个普通的包名,以避免在 Protocol Buffers 命名空间和非 Java 语言中发生命名冲突。

在包声明之后,你可以看到两个对于 Java 的特定选项:java_packagejava_outer_classnamejava_package 指定生成的类应该放在哪个 Java 包下。如果没有明确指定它,它只是匹配包声明给出的包名,但这些名称通常不是合适的 Java 包名(因为它们通常不以域名开头)。java_outer_classname 选项用来定义包含此文件中所有类的类名,即最外层类的类名。如果你没有提供 java_outer_classname,则会通过骆驼命名法转换文件名来生成它。例如,my_proto.proto 将使用 MyProto 作为外部类名。

接下来,你需要定义 message (信息)message 只是包含一组类型字段的集合。许多标准的简单数据类型都可用作字段类型,包括 boolint32floatdoublestring。你还可以使用其他信息类型作为字段类型。在上面的示例中,Person 信息包含 PhoneNumber 信息,而 AddressBook 信息包含 Person 信息。你甚至可以定义嵌套在其他信息中的信息类型,例如,PhoneNumber 类型在 Person 中定义。如果你希望某个字段具有预定义的值列表中的一个值,你可以定义枚举类型。例如你要指定电话号码可以是 MOBILEHOMEWORK 之一。

每个元素上的 = 1= 2 标记标识该字段在二进制编码中使用的唯一标签。标签号 1-15 比更大的数字要少一个字节进行编码,因此作为优化,你可以将这些标签用于常用或重复的元素,将标签 16 或更高的标签留给不太常用的可选元素。重复字段中的每个元素都需要重新编码标签号,因此重复字段特别适合此优化。

protocol buffers 不支持继承。

1.2 修饰符类型

必须使用以下修饰符之一修饰每个字段:

  • required:必须提供该字段的值,否则信息将被视为 “未初始化”。构建未初始化的信息将抛出 RuntimeException。解析未初始化的信息将抛出 IOException。除此之外,required 字段的行为与 optional 字段完全相同。

  • optional:该字段可以设置也可以不设置。如果未设置 optional 字段值,则会使用默认值。对于简单类型,你可以指定自己的默认值,就像我们在示例中为电话号码类型所做的那样。否则,将使用系统默认值:数字类型为 0,字符串为空字符串,boolsfalse。对于嵌入式信息,默认值始终是信息的 默认实例原型,其中没有设置任何字段。通过访问器获取尚未显式设置的 optional(或 required)字段的值始终返回该字段的默认值。

  • repeated:该字段可以被重复任意次数(包括 0 次),但是它们的顺序会被保留。可以将重复字段视为动态大小的数组。

二、解析 protobuf

2.1 下载最新的 proctoc.exe

在这里可以下载最新发布的 protobuf。这是 protoc-3.6.1-win32.zip 的下载地址,适用于 windows 系统,也有适用于 linux 系统的,大家可以按需下载。下载之后里面有 protoc.exe,我们把它配置到环境变量,然后使用 cmd 运行即可。

2.2 生成 Java 文件

运行 protoc.exe 编译 .proto 文件生成 Java 文件,指定源目录(应用程序的源代码所在的位置,如果不提​​供值,则使用当前目录),目标目录(生成文件的输出路径,通常跟源目录相同),以及 .proto 文件的所在目录。执行命令如下:

protoc -I=$SRC_DIR(源目录) --java_out=$DST_DIR(目标目录) $SRC_DIR(.proto 文件所在目录)/addressbook.proto

如果使用上述官方示例的 .proto 文件,则会在指定的目标目录中生成 com/example/tutorial/AddressBookProtos.java

笔者为了适配自己的应用,修改了下 java_package 的值,如下:

syntax = "proto2";

package tutorial;

option java_package = "com.example.jerry.myapplication.tutorial";

option java_outer_classname = "AddressBookProtos";

然后执行编译命令:

protoc -I=D:/AndroidProjects/MyApplication 
--java_out=D:/AndroidProjects/MyApplication/app/src/main/java/ 
D:/AndroidProjects/MyApplication/addressbook.proto

于是我们想要的 Java 文件就顺利生成了。

其实笔者在执行命令的时候还是遇到不少问题的:

  • 如果命令中没有指定源目录,则 .proto 文件必须放在 cmd 的当前目录下

  • 如果指定了源目录,则 .proto 文件必须放在源目录下

  • 还有很重要的一点,在 windows 系统下路径分隔符不能用 \,而要改为 /

有关生成的 Java 文件的内部代码解析,请看官方说明,这里就不一一阐述了。关键部分就是 MessageMessage.Builder 的联系和区别。还有几个标准的 Message 方法:

  • isInitialized():检查是否设置了所有 required 字段。

  • toString():返回一个具有可读性的 message 表示,对调试特别有用。

  • mergeFrom(Message other):仅限 Builder 使用,将其他 message 的内容合并到此 message 中,覆盖标量字段,合并复合字段以及连接重复字段。

  • clear():仅限 Builder 使用,将所有字段清除回空状态。

2.3 添加 Maven 依赖

因为 protobuf 是 Google 提供的,所以使用 Android Studio 很容易引入最新的依赖库。我们在 Project Structure 中选择 Dependencies 选项卡,从网络添加依赖库,输入关键字 com.google.protobuf 就可以搜索到最新的 protocol buffer 依赖库。例如:

dependencies {
    ...
    implementation 'com.google.protobuf:protobuf-java:3.6.1'
}

2.4 序列化和反序列化

每个 protocol buffer 类都有使用 protocol buffer 二进制格式编写和读取所选类型 message 的方法。比如上述地址簿的例子:

  • byte[] toByteArray():序列化 message 对象并返回包含其原始字节的字节数组。

  • static Person parseFrom(byte[] data):解析来自给定字节数组的 message 并返回 Java 类对象。

  • void writeTo(OutputStream output):序列化 message 并将其写入 OutputStream

  • static Person parseFrom(InputStream input):读取并解析来自 InputStreammessage

2.5 写入信息 Writing A Message

笔者在这里就直接展示官网的示例代码了,还是地址簿的例子,主要就是掌握上述所说的序列化和反序列化的那几个方法。

package com.example.jerry.myapplication.tutorial;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;

public class AddPerson {

    // This function fills in a Person message based on user input.
    static AddressBookProtos.Person PromptForAddress(BufferedReader stdin,
                                                     PrintStream stdout) throws IOException {
        AddressBookProtos.Person.Builder person = AddressBookProtos.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;
            }

            AddressBookProtos.Person.PhoneNumber.Builder phoneNumber =
                    AddressBookProtos.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(AddressBookProtos.Person.PhoneType.MOBILE);
            } else if (type.equals("home")) {
                phoneNumber.setType(AddressBookProtos.Person.PhoneType.HOME);
            } else if (type.equals("work")) {
                phoneNumber.setType(AddressBookProtos.Person.PhoneType.WORK);
            } else {
                stdout.println("Unknown phone type.  Using default.");
            }

            person.addPhones(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);
        }

        AddressBookProtos.AddressBook.Builder addressBook = AddressBookProtos.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.addPeople(
                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();
    }
}

2.6 读取信息 Reading A Message

同上。

package com.example.jerry.myapplication.tutorial;

import java.io.FileInputStream;

public class ListPeople {

    // Iterates though all people in the AddressBook and prints info about them.
    static void Print(AddressBookProtos.AddressBook addressBook) {
        for (AddressBookProtos.Person person : addressBook.getPeopleList()) {
            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 (AddressBookProtos.Person.PhoneNumber phoneNumber : person.getPhonesList()) {
                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.
        AddressBookProtos.AddressBook addressBook =
                AddressBookProtos.AddressBook.parseFrom(new FileInputStream(args[0]));

        Print(addressBook);
    }
}

2.7 小结

当把依赖库和实体类全部导入到项目中后,就可以根据服务端提供的接口获取数据,然后开始解析。

解析其实不难,如果实体类叫做 Person,那么只需一句代码:

Person person = Person.parseFrom(byte[] data);

你可能感兴趣的:(Android 解析 Protocol Buffers 格式数据)