使用 gRPC 和 Swift 构建实时 App 及服务
Build real-time apps and services with gRPC and Swift
2026年6月10日
一句话判断
gRPC Swift 终于补齐了现代 Swift 并发(Swift Concurrency)的最后一块拼图,全栈 Swift 开发者现在可以用一套 .proto 文件无缝搞定 iOS 和后端的双向实时流,不用再捏着鼻子手写 WebSocket 状态机了。
这场 Session 讲了什么
以前在 iOS 端做实时双向通信简直是受罪。手写 URLSession 配 Codable 做 RESTful 已经够烦了,要是碰上直播弹幕、多人协作这种需要 WebSocket 的场景,粘包、心跳、重连逻辑能把人逼疯。Apple 这次把 gRPC Swift 彻底翻新,底层换成了基于 SwiftNIO 的 grpc-swift-nio-transport,完美契合 async/await 和 AsyncSequence。
最大的改变是工程体验。你不再需要配置恶心的 protoc 命令行工具,直接通过 Xcode 原生的 Build Plugin(构建插件)编译 .proto 文件。iOS 端和 Swift Server 端共享同一份接口定义,强类型的 RPC 调用和双向流直接生成。这对做全栈 Swift 或者需要高频实时通信的团队来说,是实打实的降本增效。
值得深挖的点
Xcode Build Plugin 终结了 proto 编译噩梦
用过 gRPC 的人都知道,配置 protoc 和 Swift 插件是一场环境灾难。每个新同事入职或者换台 CI 机器,都要折腾半天 Makefile 和 Build Phase 脚本。Apple 这次直接拥抱 Swift Package Manager 的 Build Plugin 机制(GRPCProtobufGenerator)。你只需要在 Xcode 里加个包,配个简单的 JSON 文件指定生成 Client 还是 Server,Xcode 就会在编译时自动处理代码生成。
这种设计的 trade-off 是牺牲了一点灵活性,换取了极致的开箱即用。坑在于当你的项目有多个 SPM Target 共享 proto 文件时,Build Plugin 的缓存机制有时会抽风,导致生成的代码找不到。实战中建议把 .proto 文件和生成配置单独抽成一个基础的 SPM Target,让其他业务 Target 去依赖它,避免重复生成。
双向流与 AsyncSequence 的降维打击
Session 里最惊艳的是 FollowRace 这个双向流(Bidirectional streaming)演示。传统的 WebSocket 是基于无类型文本或二进制的,你需要自己写 Router 分发协议。而 gRPC 的双向流在 Swift 里被直接映射为输入和输出的 AsyncSequence。
在 Server 端,你可以用 TaskGroup 并发处理多个客户端的流;在 Client 端,一个 for await 循环就能优雅地消费实时数据。这种设计让业务代码极其干净。但必须警惕生命周期管理:HTTP/2 的多路复用意味着一个物理连接上可以跑无数个流,如果 App 进后台时你没有主动掐断这些 Task,底层的连接和流会一直挂起,造成严重的内存和电量泄漏。这也是为什么 Session 里特意花篇幅讲 ClientManager 的原因。
代码片段
1. 配置 Build Plugin 精准生成代码
场景:在 Xcode 中通过 JSON 配置文件,控制 gRPC 插件只生成客户端代码,避免在 iOS 端打包冗余的 Server 代码。
// grpc-swift-proto-generator-config.json
{
"generate": {
"clients": true, // 只生成客户端调用代码
"servers": false, // iOS 端不需要服务端桩代码
"messages": true // 生成 Protobuf 消息模型
}
}
坑:这个 JSON 文件必须放在包含 .proto 文件的 Target 根目录下,且文件名必须严格匹配插件的默认要求,否则插件会静默失败,不生成任何代码。
2. 使用 withGRPCClient 发起强类型请求
场景:在 SwiftUI 的 .task 修饰符中,快速建立一个安全的 HTTP/2 连接并发起 Unary(一元)RPC 调用。
.task {
do {
// 使用 SwiftNIO 传输层,默认开启 TLS 保证安全
try await withGRPCClient(
transport: .http2NIOTS(
address: .ipv4(host: "127.0.0.1", port: 8080),
transportSecurity: .tls
)
) { client in
// 自动生成的强类型 Client 包装器
let kartService = SwiftKartService.Client(wrapping: client)
let request = ListRacesRequest.with { $0.limit = 50 }
// 像调用本地函数一样调用远程接口
let response = try await kartService.listRaces(request)
self.races = response.races
}
} catch {
print("gRPC 调用失败: \(error)")
}
}
坑:withGRPCClient 会在闭包结束时自动关闭连接。千万不要在每个 View 里都这么干,频繁握手会抵消 HTTP/2 的性能优势,必须复用 Client 实例。
3. 全局 ClientManager 管理连接生命周期
场景:使用 Swift 6 的 Mutex 和 @Observable 构建一个线程安全的连接管理器,并在 App 进入后台时主动断开连接。
@Observable
final class ClientManager: Sendable {
// 使用 Mutex 保护内部状态,符合 Swift 6 严格并发检查
fileprivate let state = Mutex<State>(.disconnected)
func disconnect() {
state.withLock { currentState in
if case .connected(let client) = currentState {
// 主动关闭底层的 HTTP/2 连接
client.close()
currentState = .disconnected
}
}
}
}
// 在 App 根节点监听场景生命周期
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .background {
manager.disconnect() // 进后台立刻断开,省电省连接
}
}
坑:在 Swift 6 环境下,网络客户端的状态管理极易触发 Data Race 警告。使用 Mutex(来自 Synchronization 库)比传统的 NSLock 或 actor 更适合这种高频同步读写的底层状态管理。
最佳实践
老项目迁移不要想着一步到位把 REST 全换成 gRPC。挑出那些痛点最大的实时接口(比如 IM 聊天、实时位置共享、协同编辑),用 gRPC 的双向流重构。普通的 CRUD 接口继续用 OpenAPI 生成器或者手写,两者在同一个 App 里共存完全没问题。
新项目如果后端也是 Swift(比如用 Hummingbird 或 Vapor),无脑上 gRPC。一套 .proto 文件前后端共享,类型安全能帮你省掉 80% 的联调时间。如果后端是 Go 或 Java,gRPC 也是首选,它的跨语言生态比 Apple 官方的 OpenAPI 生成器成熟得多。
实战中最容易踩的坑是连接复用和后台保活。HTTP/2 的精髓在于多路复用,一个物理连接就能承载无数个并发流。务必在全局维护一个 ClientManager 单例,通过 SwiftUI 的 environment 传递。同时,iOS 系统对后台网络极其敏感,进后台必须调用 disconnect(),否则不仅会被系统杀进程,还会导致服务端的连接池被僵尸连接耗尽。
还有什么值得关注
- Protobuf Edition 2024:示例代码中
.proto文件头部变成了edition = "2024",这标志着 Protobuf 告别了proto3语法,迎来了更现代化的版本控制机制。 - 二进制体积红利:Protobuf 序列化时只传输字段编号(Field numbers)而不传输字段名,同等数据下体积大约是 JSON 的一半,对蜂窝网络环境下的移动端 App 省流量效果显著。
- 强制 TLS 最佳实践:新的
http2NIOTS传输层默认推荐使用.tls,只有在本地开发调试时才允许传入.plaintext,从 API 层面逼着你遵守安全规范。