整理旧电脑,发现在待发布文章的文件夹下还躺着好几篇博客,整理下也补发上来。
上篇《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")
}
}
}
}
}
几个要点:
- 推荐使用
protobuf-javalite而不是protobuf-java,full 版带反射和 descriptor 机制,体积大;lite 是 Android 官方推荐运行时,砍掉了移动端用不到的部分。 option("lite")让生成的代码依赖javalite。- 版本号只写一处,升级时只改
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 怎么写,而是下面几点:
- 依赖选择:
javalitevsjavafull,Android 一律 lite。 - 版本管理:protobuf 和运行时版本必须完全一致。
- AGP 9:这个最坑人,解决办法就是手动去补
sourceSets生成路径。 - 混淆:full 版或 R8 full mode 需手动 keep,lite版本则自带 consumer rules。
九、参考资料
-
rotocol Buffers GitHub:https://github.com/protocolbuffers/protobuf
-
Protocol Buffers 文档(中文):https://protobuf.com.cn/
-
protobuf-gradle-plugin:https://github.com/protocolbuffers/protobuf-gradle-plugin
-
gRPC Android 快速开始:https://grpc.io/docs/languages/android/
-
AndroidX DataStore(Proto):https://developer.android.com/topic/libraries/architecture/datastore