简介

Protocol Buffers(简称 Protobuf)是 Google 开发的一种独立于语言和平台的结构化数据序列化可扩展机制。它与 JSON 类似,只是体积更小、速度更快,而且能生成本地语言绑定。您只需定义一次数据的结构化方式,然后就可以使用特殊生成的源代码,使用各种语言轻松地将结构化数据写入各种数据流或从各种数据流中读取结构化数据。

Google 提供了多种语言的实现,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 XML 进行数据交换快许多(体积小 3~10 倍,速度快 20~100 倍)。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。

它有一个非常棒的特性,即“向后”兼容性好,人们不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级。这样您的程序就可以不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题。因为添加新的消息中的 field 并不会引起已经发布的程序的任何改变。

Protobuf 与 XML 和 JSON 相比也有不足之处。由于 XML 和 JSON 具有某种程度上的自解释性,它可以被人直接读取编辑,在这一点上 Protobuf 不行,它以二进制的方式存储。

语法

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}
  • 第一行说明使用的是 proto3 语法:如果不这样做,Protobuf 编译器会认为使用的是 proto2。这必须是文件中第一行非空、非注释的内容。
  • SearchRequest 报文定义指定了三个字段(名称/值对),每个字段代表一条要包含在此类报文中的数据。每个字段都有名称和类型。
  • 须为信息定义中的每个字段赋予一个介于 1536,870,91122912^{29} -1)之间的数字,在该报文中必须唯一,编号 19,00019,999 保留给 Protocol Buffers 实现。
  • 不能使用任何先前保留的编号。更改字段编号相当于删除该字段,然后创建一个类型相同但编号不同的新字段。删除字段定义时必须保留已删除的字段编号,如果不保留字段编号,开发人员将来有可能重新使用该编号。

使用

libs.versions.toml 定义版本:

[libraries]
protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }
protobuf-javalite = { group = "com.google.protobuf", name = "protobuf-javalite", version.ref = "protobuf" }

[plugins]
googleProtobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }

项目的 build.gradle.kts

plugins {
    // ...
    alias(libs.plugins.googleProtobuf) apply false
}

app 的 build.gradle.kts

import com.google.protobuf.gradle.proto

plugins {
    // ...
    alias(libs.plugins.googleProtobuf)
}

android {
    //...
    sourceSets {
        getByName("main").java.srcDir("src/main/java")
        getByName("main").proto {
            srcDir("src/main/proto")
            include("**/*.proto")
        }
    }
}
protobuf {
    protoc { 
        artifact = libs.protoc.get().toString()
    }
    plugins {
        generateProtoTasks {
            all().forEach {
                it.builtins {
                    create("java") {
                        option("lite")
                    }
                }
            }
        }
    }
}

dependencies {
    // ...
    implementation(libs.protobuf.protoc)
    implementation(libs.protobuf.javalite)
}

混淆配置:

-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }

app/src/main/proto 目录中创建 proto 文件:

Proto File

内容如下:

syntax = "proto3";  // 声明 proto 协议版本
package com.example.proto;  // 定义 Protobuf 自动生成类的包名
option java_package = "com.example.proto";  // Java 类所在的包名
option java_outer_classname= "SearchRequestProto";  // 定义 Protobuf 自动生成类的类名

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}

Build 项目,会在 app/build/generated/source/proto 中生成对应的 Java 文件:

Generated Java File

生成的 Java 文件内容太长,就不贴在这里了,但是结构简单,无非就是一堆 Getter 和 Setter 以及一些解析方法,相信你也可以轻易读懂。

接下来就可以在代码中调用它:

@Test
fun proto() {
    // 序列化
    val request = SearchRequestProto.SearchRequest.newBuilder()
        .setQuery("Proto")
        .setPageNumber(1)
        .setResultsPerPage(10)
        .build()
    val bytes = request.toByteArray()

    // 反序列化
    try {
        val result = SearchRequestProto.SearchRequest.parseFrom(bytes)
        val query = result.query
        val pageNumber = result.pageNumber
        val resultPerPage = result.resultsPerPage
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

序列化利用的是 Builder 模式,构建成一个 SearchRequest 对象后,再将其转换成 ByteArray。反序列化则利用 parseFrom() 方法将 ByteArray 或者 InputStream 转成 SearchRequest 对象,parseFrom() 方法就是在上方生成的 Java 文件中自动帮我们插入的方法。

参考内容