一、gRPC 到底是什么?

说实话,我第一次接触 gRPC 的时候,心里是有点抵触的。那时候团队里都在用 RESTful API,大家习惯了 JSON 一把梭。但后来有个项目需要做微服务之间的高频通信,REST 的性能瓶颈就暴露出来了——序列化慢、连接开销大、没有强类型约束。嗯,这时候 gRPC 就站出来了。

gRPC 是 Google 开源的高性能 RPC 框架。它基于 HTTP/2 协议,默认用 Protobuf 做序列化。说白了,它让你像调用本地函数一样调用远程服务。你想想看,客户端写一个 userService.getUser(id),背后就完成了网络请求、数据解析、错误处理,是不是很爽?

核心特点:

  • 基于 HTTP/2:多路复用、头部压缩、服务端推送
  • 默认 Protobuf:二进制编码,体积小、速度快
  • 强类型接口:.proto 文件即契约,客户端和服务端严格对齐
  • 支持双向流、客户端流、服务端流

我个人习惯把 gRPC 看作「跨语言的函数调用」。你在 Kotlin 里定义好接口,Java、Go、Python 都能直接生成客户端代码。这在微服务架构里特别实用。

二、Protobuf 定义:先写契约

gRPC 的核心是 .proto 文件。它定义了服务接口和消息结构。我建议你把它当作团队里的「API 合同」——谁都不能私自改,改了就要重新生成代码。

2.1 一个简单的例子

假设我们要做一个用户信息服务。先定义消息结构:

syntax = "proto3";

package com.example.user;

message UserRequest {
  int32 user_id = 1;
}

message UserResponse {
  int32 user_id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;
}

service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
}

这里要注意几点:

  • syntax = "proto3" 表示使用 proto3 语法,字段默认 optional
  • 每个字段后面的数字是字段编号,1 到 15 占 1 字节,16 以上占 2 字节
  • 服务定义用 service 关键字,里面写 RPC 方法

我的经验:字段编号别乱跳。我曾经接手一个项目,字段编号从 1 跳到 100,中间留了一大堆空号。结果每次序列化都多出很多冗余字节。建议按使用频率排序,高频字段用小编号。

2.2 生成 Kotlin 代码

定义好 .proto 文件后,用 protoc 编译器生成代码。在 Android 项目中,我推荐用 Gradle 插件自动生成:

// build.gradle.kts
plugins {
  id("com.google.protobuf") version "0.9.4"
}

protobuf {
  protoc {
    artifact = "com.google.protobuf:protoc:3.25.0"
  }
  plugins {
    id("grpc") {
      artifact = "io.grpc:protoc-gen-grpc-java:1.60.0"
    }
  }
  generateProtoTasks {
    all().forEach { task ->
      task.plugins {
        id("grpc")
      }
    }
  }
}

运行 ./gradlew generateProto,就会在 build/generated/source/proto 下生成 Kotlin 代码。你会看到 UserServiceGrpc 这样的类,里面包含了 Stub(存根)类。

三、Kotlin gRPC 客户端实现

好了,契约有了,代码生成了,接下来就是写客户端。在 Android 里,gRPC 客户端通常跑在后台线程,因为它是阻塞式的。不过配合 Kotlin 协程,我们可以把它包装成挂起函数。

3.1 创建 Channel 和 Stub

val channel = ManagedChannelBuilder.forAddress("api.example.com", 443)
    .useTransportSecurity()  // 生产环境必须用 TLS
    .build()

val stub = UserServiceGrpc.newBlockingStub(channel)

这里我用了 newBlockingStub,它会阻塞当前线程。在 Android 主线程调用会崩,所以一定要切到 IO 线程。

注意:千万不要在主线程创建 Channel。Channel 是线程安全的,可以复用。我见过有人每次请求都 new 一个 Channel,结果内存泄漏了。建议用单例或者依赖注入管理。

3.2 用协程封装

我个人习惯写一个扩展函数,把阻塞调用转成挂起函数:

suspend fun UserServiceGrpc.UserServiceBlockingStub.getUserSuspend(
    request: UserRequest
): UserResponse = withContext(Dispatchers.IO) {
    getUser(request)
}

然后在 ViewModel 里调用:

class UserViewModel : ViewModel() {
    private val stub = UserServiceGrpc.newBlockingStub(channel)

    fun loadUser(userId: Int): Flow<UiState<UserResponse>> = flow {
        emit(UiState.Loading)
        val response = stub.getUserSuspend(
            UserRequest.newBuilder().setUserId(userId).build()
        )
        emit(UiState.Success(response))
    }.catch { e ->
        emit(UiState.Error(e.message ?: "Unknown error"))
    }
}

你看,这样就和 Retrofit + Flow 的用法几乎一样了。gRPC 的强类型在这里体现得很明显——你不可能传错字段名,因为编译器会检查。

3.3 流式调用

gRPC 支持四种调用模式。除了上面的一元调用(Unary),还有服务端流、客户端流、双向流。在 Kotlin 里,服务端流特别适合用 Flow 接收:

// proto 定义
service UserService {
  rpc ListUsers (UserListRequest) returns (stream UserResponse);
}

// 客户端代码
fun listUsers(): Flow<UserResponse> = callbackFlow {
    val stub = UserServiceGrpc.newStub(channel)
    val responseStream = stub.listUsers(
        UserListRequest.getDefaultInstance(),
        object : StreamObserver<UserResponse> {
            override fun onNext(value: UserResponse) {
                trySend(value)
            }
            override fun onError(t: Throwable) {
                close(t)
            }
            override fun onCompleted() {
                close()
            }
        }
    )
    awaitClose { /* 清理资源 */ }
}

这里用 callbackFlow 把 gRPC 的流式回调转成 Flow。这样在 UI 层就可以用 collect 来接收数据流了。

四、gRPC 与 Retrofit 的对比

维度 gRPC Retrofit
协议 HTTP/2 HTTP/1.1 或 HTTP/2
序列化 Protobuf(二进制) JSON(文本)
性能 高(体积小、解析快) 中等(JSON 解析开销大)
流式支持 原生支持 需配合 SSE 或 WebSocket
调试难度 较高(二进制不可读) 低(JSON 可直接看)
生态 Google 系,跨语言 Android 主流,社区丰富

我个人觉得,如果你的 App 需要和后端高频通信、或者有流式推送场景,gRPC 是更好的选择。但如果只是简单的 CRUD,Retrofit 的易用性更胜一筹。

五、避坑指南

我曾经踩过的坑:

  • TLS 配置:Android 上 gRPC 默认用 Conscrypt 作为 SSL 引擎。如果你用 OkHttp 作为传输层,记得添加 okhttp-tls 依赖。
  • 超时设置:gRPC 默认没有超时。我建议在 Stub 上设置 withDeadlineAfter(5, TimeUnit.SECONDS),防止网络问题导致线程卡死。
  • Channel 复用:一个 Channel 可以处理多个并发请求,不要为每个请求创建新 Channel。
  • Protobuf 字段顺序:客户端和服务端的 .proto 文件必须完全一致,包括字段编号。否则会解析失败。

六、知识体系总览

下面这张图总结了 gRPC 在 Android 中的核心链路:

Android gRPC 核心链路 .proto 文件定义 protoc 生成 Kotlin Stub ManagedChannel + Stub Unary | Server Streaming | Client Streaming | Bidirectional 协程 + Flow 封装

从 .proto 文件开始,到生成 Stub,再到配置 Channel,最后选择调用模式。每一步都有讲究。我个人建议初学者先从一元调用入手,跑通后再尝试流式调用。

七、总结

gRPC 在 Android 里不是银弹,但它在特定场景下确实比 REST 强太多。尤其是微服务间通信、实时数据推送、跨语言协作这些场景。嗯,如果你团队里后端也是用 gRPC,那客户端接入的成本其实很低——定义好 .proto,生成代码,然后像调用本地函数一样调用远程服务。

最后提醒一句:生产环境一定要用 TLS,不要图省事用明文。我曾经在测试环境忘了开 TLS,结果被安全团队通报了……

公众号:蓝海资料掘金营,微信deep3321