Android 接入 protobuf 实战:从 Gradle 配置到避开 R8 混淆坑

整理旧电脑,发现在待发布文章的文件夹下还躺着好几篇博客,整理下也补发上来。

上篇《Protocol Buffers 入门:为什么它比 JSON 小 60%,快了 5 倍?》认识了 Protobuf 的原理和优势,本篇会实战 Android 开发中如何使用它。

一、前言

首先我们要申明:不是所有的接口都该使用 Protocol Buffers,要根据业务场景去选择:

  • 电商APP首页 feed、IM消息、埋点上报,这类数据量大、频率高,弱网下采用 JSON 只会造成巨大的带宽开销与电量消耗。
  • Gson / kotlinx.serialization 在运行时靠反射或运行时适配器解析,接口字段一变,崩溃会发生在用户手机端而不是程序编译期。
  • 如果软件接口/字段更改频繁,版本审核和用户更新不能及时跟上,使用 JSON 来兼容则全靠"自觉"。

这些内容是在上篇文章里都有提到的,我们这里简单回顾后马上开始今天的技术实战------Android APP 接入 Protobuf。

本文代码示例也依然用一个 User 消息贯穿全文,项目结构预览:

text 复制代码
ProtobufDemo/
├─ gradle/
│  └─ libs.versions.toml                  ← 版本集中管理(TOML)
└─ app/
   ├─ build.gradle.kts                    ← protobuf 插件 + sourceSets 挂载生成目录
   └─ src/main/
      ├─ proto/
      │  └─ user.proto                    ← 协议契约(唯一源头)
      └─ java/com/xxxxx/protobufdemo/protobuf/
         ├─ MainActivity.kt               ← 演示页
         ├─ net/
         │  ├─ ProtoConverterFactory.kt   ← Retrofit protobuf 转换器
         │  ├─ RetrofitProvider.kt        ← Retrofit 单例(protobuf 优先 + Gson 兜底)
         │  └─ UserApi.kt                 ← REST 接口定义
         ├─ chat/
         │  └─ ChatRepository.kt          ← gRPC 封装(一元 + 服务端流式)
         └─ data/
            └─ UserSerializer.kt          ← DataStore protobuf 序列化器

app/build/generated/java/generateDebugProto/   ← 编译期自动生成(进 .gitignore)
├─ java/   → User.java                       (protobuf builtin 生成)
└─ grpc/   → ChatServiceGrpc.java            (grpc 插件生成,独立目录)

二、Gradle 配置环境

Android Gradle 下集成 Protobuf 需要使用官方维护的 Gradle 插件 ------protobuf-gradle-plugin,它负责在编译期调用 protoc 把 .proto 生成 Java / Kotlin 代码,生成目录自动加入 sourceSet。

⚠️ 版本说明:protobuf-gradle-plugin 最新稳定版 0.10.0;grpc-android 最新 1.85.0。protoc 的版本必须与 protobuf 运行时依赖版本完全一致 ,否则运行期会出现诡异的 NoSuchMethodError / 字段错位。本文以 4.28.3 为例,统一写进 libs.versions.toml 管理。

项目默认使用 Gradle 版本目录(Version Catalogs) 管理依赖,先来看 gradle/libs.versions.toml:

toml 复制代码
[versions]
protobuf = "4.28.3" 
protobufPlugin = "0.10.0"

[libraries]
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" }
protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" }

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

然后去根目录下的 build.gradle.kts 里声明插件:

text 复制代码
plugins {
    alias(libs.plugins.protobuf) apply false
}

最后打开模块 :app/build.gradle.kts:

kotlin 复制代码
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    alias(libs.plugins.protobuf)
}

android {
    // ......常规配置省略
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

dependencies {
    // protobuf 运行时:Android 一律用 lite 版,体积更小、无反射
    implementation(libs.protobuf.javalite)
    // Kotlin 友好访问器(可选,但强烈建议:提供 user { } 作用域与属性访问)
    implementation(libs.protobuf.kotlin.lite)
}

protobuf {
    protoc {
        // protoc 与运行时版本必须一致:从版本目录读同一个版本号
        artifact = "com.google.protobuf:protoc:${libs.versions.protobuf.get()}"
    }
    generateProtoTasks {
        all().forEach { task ->
            // 生成 lite 版 Java 代码(protobuf ≥ 3.8 后 lite 已内置进 java builtin)
            task.builtins {
                create("java") {
                    option("lite")
                }
            }
        }
    }
}

几个要点:

  1. 推荐使用 protobuf-javalite 而不是 protobuf-java,full 版带反射和 descriptor 机制,体积大;lite 是 Android 官方推荐运行时,砍掉了移动端用不到的部分。
  2. option("lite") 让生成的代码依赖 javalite。
  3. 版本号只写一处,升级时只改 libs.versions.toml 里的 protobuf 就可以,运行时和 protoc 自动跟着该版本,避免错配。

一个大坑注意:AGP 9 必看

AGP 9 默认是内置 Kotlin (项目里没有 kotlin-android 插件),此时 protobuf-gradle-plugin 0.10.0 的老式注册方式不会 把生成的 User.java 自动挂进 Kotlin 编译源集------现象是 generateDebugProto 任务跑成功了、文件也在 build/generated/java/generateDebugProto/ 里,但业务代码里写 User.newBuilder() 会报 Unresolved reference 'User'。

解决方法是手动把生成目录补进 sourceSets(写在 app/build.gradle.kts 的 android { } 块内,与 dependencies / protobuf 并列):

kotlin 复制代码
//app/build.gradle.kts(android {} 块内,与 dependencies / protobuf 并列)
android {
    sourceSets {
        getByName("main") {
            // ⭐AGP 9 内置 Kotlin 下,protobuf 生成目录不再自动进 Kotlin 编译源集,手动挂上
            java.srcDir("build/generated/java/generateDebugProto/java")
            java.srcDir("build/generated/java/generateReleaseProto/java")
        }
    }
}

AGP 8 及以前不需要这段生成目录,接入后的工程长这样:

🎓网络层、RPC、DataStore 在文章后面会讲到,这里可以先记下。

三、写 .proto 并生成类

在 :app/src/main/proto/ 下新建 user.proto :

text 复制代码
syntax = "proto3";

option java_package = "com.example.demo";// ⭐这里的包名你可以替换成你的
option java_multiple_files = true; // 让每个 message 生成独立文件

message User {
  int32  id    = 1;
  string name  = 3;
  string email = 2;
}

Sync Project with Gradle Files 编译后生成 User.java 会落到生成目录。再次提醒避坑:不同 AGP 版本路径不同,别找错地方:

  • AGP 8 及以前 :build/generated/source/proto/debug/java/com/example/demo/

  • AGP 9 起 :build/generated/java/generateDebugProto/java/com/example/demo/

使用也是很简单的:

kotlin 复制代码
// 构造:走 Builder
val user = User.newBuilder()
    .setId(1)
    .setName("张三")
    .setEmail("zhangsan@example.com")
    .build()

// 序列化:消息 → 二进制
val bytes: ByteArray = user.toByteArray()

// 反序列化:二进制 → 消息
val parsed = User.parseFrom(bytes)
Log.d("ProtobufActivity", "${parsed.id} / ${parsed.name} / ${parsed.email}")

💡不是非要写 Activity 里,直接在任意 kt 文件里写 User.newBuilder(),能编译、不飘红就说明生成 + 注册都成功了。

四、Retrofit + OkHttp 收发 protobuf

和使用 JSON 一样,网络请求场景是最常用的。把 protobuf 当作请求体 / 响应体,注册一个自定义 Converter.Factory,让 Retrofit 认得 MessageLite 类型。

先看工厂类 ProtoConverterFactory

kotlin 复制代码
/**
* 请求把 MessageLite 序列化成 application/x-protobuf,响应把字节流转回消息对象
*/
class ProtoConverterFactory : Converter.Factory() {

    override fun requestBodyConverter(
        type: Type,
        parameterAnnotations: Array<out Annotation>,
        methodAnnotations: Array<out Annotation>,
        retrofit: Retrofit
    ): Converter<*, RequestBody>? {
        if (type !is Class<*> || !MessageLite::class.java.isAssignableFrom(type)) return null
        return Converter<MessageLite, RequestBody> { msg ->
            msg.toByteArray().toRequestBody(MEDIA_TYPE)
        }
    }

    override fun responseBodyConverter(
        type: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): Converter<ResponseBody, *>? {
        if (type !is Class<*> || !MessageLite::class.java.isAssignableFrom(type)) return null
        // 生成的 message 都有静态 parser() 方法
        @Suppress("UNCHECKED_CAST")
        val parser = type.getMethod("parser").invoke(null) as Parser<Any>
        return Converter<ResponseBody, Any> { body ->
            parser.parseFrom(body.byteStream())
        }
    }

    companion object {
        private val MEDIA_TYPE = "application/x-protobuf".toMediaType()
        fun create() = ProtoConverterFactory()
    }
}

接口定义就用生成类当参数和返回值:

kotlin 复制代码
interface UserApi {
    @POST("user/sync")
    suspend fun syncUser(@Body user: User): User   // 请求和响应都是 protobuf
}

注册时让 protobuf 转换器排在前面,JSON 转换器兜底,这样同一套 Retrofit 里 protobuf 接口和 JSON 接口可以共存:

kotlin 复制代码
Retrofit.Builder()
    .baseUrl("https://api.example.com/") // ⭐替换真实接口或使用Mock
    .addConverterFactory(ProtoConverterFactory.create())   // protobuf 优先识别
    .addConverterFactory(GsonConverterFactory.create())     // 其余 JSON 接口兜底
    .build()

五、gRPC-Android 实战

讲完 REST 式收发,必须提 gRPC ------它是 protobuf 的官方亲儿子传输层,把 .proto 里的 service / rpc 直接生成客户端 Stub,可以像调用本地方法一样。

2026年背景下,大量 AI 推理 / 对话后端用 gRPC 暴露接口(流式回答、双向流非常适合 LLM 场景),作为一名 Android 开发还是值得去了解的。

追加上面的 user.proto :

text 复制代码
// app/src/main/proto/user.proto(接在第三节的 message 后面,把 service/rpc 这段追加进去)
syntax = "proto3";

service ChatService {
  rpc Ask(Question) returns (Answer);          // 一元调用
  rpc StreamAsk(Question) returns (stream Answer);  // 服务端流式
}

message Question { string prompt = 1; }
message Answer   { string text = 1; int32 tokens = 2; }

libs.versions.toml 加上 gRPC 的版本依赖:

toml 复制代码
[versions]
grpc = "1.85.0"

[libraries]
grpc-android = { module = "io.grpc:grpc-android", version.ref = "grpc" }
grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc" }
grpc-okhttp = { module = "io.grpc:grpc-okhttp", version.ref = "grpc" }

配置完成后可以建 channel、拿 Stub、发请求:

kotlin 复制代码
// 核心代码展示
val channel = AndroidChannelBuilder
    .forAddress("ai.example.com", 443)
    .useTransportSecurity()
    .context(applicationContext)
    .build()

// 阻塞式 Stub(示例;生产中建议用 newStub 配合协程/回调,别阻塞主线程)
val stub = ChatServiceGrpc.newBlockingStub(channel)
val answer = stub.ask(
    Question.newBuilder().setPrompt("用一句话解释 protobuf").build()
)

六、本地持久化:DataStore 存 protobuf

除了网络,protobuf 做本地存储也比 JSON 文件香:体积小、读写快、自带 schema 校验。而且 AndroidX DataStore 原生支持 protobuf。

实现一个 Serializer<User>,直接复用生成类的 parseFrom / writeTo:

kotlin 复制代码
object UserSerializer : Serializer<User> {
    override val defaultValue: User = User.getDefaultInstance()  // 缺失时给默认实例
    override suspend fun readFrom(input: InputStream): User =
        User.parseFrom(input)
    override suspend fun writeTo(t: User, output: OutputStream) =
        t.writeTo(output)
}

// 用 DataStoreFactory 创建(单对象 proto datastore)
val userStore: DataStore<User> = DataStoreFactory.create(
    serializer = UserSerializer,
    produceFile = { File(context.filesDir, "user.pb") } // ⭐后缀使用.pb
)

// 写
scope.launch {
    userStore.updateData { it.toBuilder().setName("张三").build() }
}
// 读
val name = userStore.data.first().name

七、R8 混淆

早期 Protobuf 类需要在混淆的时候进行 keep,避免打包后运行会 NoClassDefFoundError 或字段全空。但如果你是使用的protobuf-javalite(>= 3.x) 则可以直接跑,它自带了consumer ProGuard 规则。但下面的两类情况依旧建议手动添加 keep:

  • 用的是 protobuf-java(full 版)而非 javalite;
  • 在 gradle.properties 开了 R8 full mode (android.enableR8.fullMode=true)或自定义了激进混淆。

最稳的做法是显式 keep 你生成的 message 类与运行时基类,proguard-rules.pro 里加:

text 复制代码
# app/proguard-rules.pro(追加到文件末尾,不要删原内容)
# 生成的 message 类(把包名换成你的)

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

# 运行时:保留 MessageLite 实现类的字段(full mode / 激进混淆下必需)
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
    <fields>;
}
-keep class * implements com.google.protobuf.MessageLite
-keep class * implements com.google.protobuf.MessageLiteOrBuilder

八、总结

本文内容安排还是比较清楚的:Gradle 接 protobuf-gradle-plugin → 写 .proto 生成类 → Retrofit 收发 body (REST式)→ gRPC 做 RPC → DataStore 做存储 → 最后介绍 keep 规则。

回看整个过程,真正容易翻车的其实不是 API 怎么写,而是下面几点:

  • 依赖选择:javalite vs javafull,Android 一律 lite。
  • 版本管理:protobuf 和运行时版本必须完全一致。
  • AGP 9:这个最坑人,解决办法就是手动去补 sourceSets 生成路径。
  • 混淆:full 版或 R8 full mode 需手动 keep,lite版本则自带 consumer rules。

九、参考资料

相关推荐
Marco_Marco1351 小时前
定位SDK接入实战:Android/iOS/鸿蒙三端怎么选、怎么接
android·ios·harmonyos
芝麻别开门13 小时前
gst-shark安卓编译使用方法
android·gstreamer·gst-shark
2501_9159214316 小时前
iOS应用耗电测试:使用Energy Log、Keymob和Instruments进行性能监控与能源消耗分析
android·ios·小程序·https·uni-app·iphone·webview
蒲公英内测分发17 小时前
热成像仪交付客户后,Android 配套 App 不上架,怎么持续下载安装和更新
android·机器人·项目管理·智能家居·智能硬件
脚踏实地,坚持不懈!17 小时前
Android 常见性能问题总结
android
高旭的旭18 小时前
图解 Android 系统架构:从 App、Framework、HAL 到 Linux 驱动
android·linux·系统架构
hunterandroid18 小时前
Navigation Component 多模块导航与深度链接实战
android
快乐非自愿18 小时前
低代码落地费控系统:从报销流程到业财闭环的实操方案
android·低代码·rxjava·ai低代码
摇滚侠19 小时前
《Java并发编程之美》第 2 章 并发编程的其他基础知识 阅读笔记 3
android·java·笔记