
前言
合体大圆满,你已将协程与 Android 四大组件完美融合。CoroutineWorker、Room、Retrofit、Compose 皆与协程浑然一体,你的 Android 协程修为已臻化境。
然而,一个全新的世界正在向你招手:
公司决定采用 Kotlin Multiplatform(KMM)统一 iOS 和 Android 的业务逻辑。你需要在 iOS 上也能运行协程 ?Swift 的异步怎么办? 后端团队开始用 Ktor 搭建服务,他们也在用协程处理高并发。后端协程和 Android 协程是一回事吗? 你听说协程可以编译成 WebAssembly ,甚至能跑在 JavaScript 引擎上。协程的"挂起"魔法在单线程的 JS 里如何实现?
这些问题指向协程的更高境界------跨平台与后端开发。协程从来不只是 Android 的专属武器。它是 Kotlin 语言级的并发框架,理论上可以运行在任何支持 Kotlin 的平台上。理解协程的跨平台本质,你才能真正跳出 Android 的桎梏,看到协程设计的大道真意。
本讲是大乘境的初阶修炼。你将跳出 Android 三界,俯瞰协程的跨平台天地:
- 理解 Kotlin 协程的多平台架构:
common、jvm、js、native模块的分工。 - 掌握 KMM 中如何共享协程代码,以及在 iOS 上的实际运行机制。
- 对比 Android 协程与后端 Ktor 协程的异同------线程池、调度器、并发模型。
- 了解协程在 JavaScript 单线程环境中的实现原理(
Promise转换)。 - 展望协程在未来的演进方向(虚拟线程、结构化并发推广)。
准备好跳出三界,以更高维度审视协程了吗?我们开始。
操千曲 而后晓声,观千剑 而后识器。虐它千百遍 方能通晓其真意。
协程的多平台架构:一套代码,四处运行
KMP 的协程模块
Kotlin 协程库从设计之初就考虑了多平台支持。打开协程的源码仓库,你会看到这样的模块结构:
bash
kotlinx.coroutines/
├── common/ # 跨平台公共代码
├── jvm/ # JVM 平台特定实现
├── js/ # JavaScript 平台特定实现
├── native/ # Native (iOS/macOS/Linux) 平台特定实现
└── android/ # Android 平台扩展
common 模块定义了协程的核心抽象:suspend 关键字、CoroutineScope、Job、Channel、Flow 等 API 的接口和部分实现。各平台模块则负责实现与底层线程/调度系统的对接。
核心设计哲学:协程的"挂起与恢复"是平台无关的(通过状态机和 Continuation),但"挂起后线程的调度"是平台相关的。这种分离使得协程能横跨 JVM、iOS、JS 三大生态。
在 KMM 中共享协程代码
KMM 项目的典型结构:
bash
shared/
├── commonMain/ # 公共业务逻辑,包含 suspend 函数、Flow 等
├── androidMain/ # Android 特定实现(如 Dispatchers.Main)
└── iosMain/ # iOS 特定实现
在 commonMain 中,你可以像在 Android 中一样写协程代码:
kotlin
// shared/commonMain/kotlin/UserRepository.kt
expect class PlatformDispatcher {
val io: CoroutineDispatcher
val main: CoroutineDispatcher
}
class UserRepository(private val api: UserApi) {
suspend fun fetchUser(id: String): User {
return withContext(PlatformDispatcher.io) {
api.getUser(id)
}
}
}
在 androidMain 和 iosMain 中分别提供 actual 实现:
kotlin
// androidMain
actual class PlatformDispatcher {
actual val io = Dispatchers.IO
actual val main = Dispatchers.Main
}
// iosMain
actual class PlatformDispatcher {
actual val io = Dispatchers.Default // iOS 上通常用 Default
actual val main = Dispatchers.Main
}
这样,核心业务逻辑的协程代码就可以一次编写,双端共享。
iOS 上的协程:GCD 与 Worker 的幕后功臣
协程如何在 iOS 上运行?
Kotlin/Native 将 Kotlin 代码编译为 LLVM 字节码,最终生成 iOS 可执行的二进制。协程在 iOS 上的调度器,底层绑定的是 Apple 的 Grand Central Dispatch(GCD) 和 Worker 线程机制。
Dispatchers.Main:绑定到主队列dispatch_get_main_queue()。Dispatchers.Default:使用 CPU 核心数个 Worker 线程的池。Dispatchers.IO:在 Kotlin/Native 中目前与Default相同(未来可能优化)。
与 Swift 异步的互操作
Swift 5.5 引入了 async/await,其底层同样是基于 GCD 的协程(Swift 称为"任务")。Kotlin 协程与 Swift 异步可以通过回调或 suspend 函数的导出进行互操作。例如,你可以将一个 Kotlin 的 suspend 函数导出为 Swift 的 async 函数,让 iOS 开发者像调用原生异步函数一样使用。
kotlin
// Kotlin 侧
@Throws(IOException::class)
suspend fun fetchData(): String = ...
// 导出后,Swift 侧可以这样调用:
// Task {
// let data = try await FetchDataKt.fetchData()
// }
这种互操作能力,使得协程成为 KMM 中统一异步逻辑的最佳选择。
后端协程:Ktor 与高并发场景的协程实践
后端协程与 Android 协程的异同
| 对比维度 | Android 协程 | 后端协程(Ktor/Spring) |
|---|---|---|
| 主线程调度器 | Dispatchers.Main(UI 线程) |
通常无主线程概念 |
| IO 调度器 | 弹性线程池(最大 64) | 同样弹性,但可配置更大 |
| 典型并发模型 | 少量协程处理 UI 事件 | 海量协程处理网络请求(C10K) |
| 生命周期管理 | 依赖 viewModelScope 等 Android 组件 |
依赖请求上下文(如 Ktor 的 call) |
| 结构化并发 | 同样遵循 | 同样遵循,但更强调请求边界 |
Ktor 中的协程:请求即协程
Ktor 是 Kotlin 官方推出的异步 Web 框架,它从设计之初就完全基于协程。每一个 HTTP 请求都在一个协程中处理,框架自动管理协程的生命周期。
kotlin
// Ktor Application.kt
fun Application.module() {
routing {
get("/user/{id}") {
val id = call.parameters["id"] ?: throw BadRequestException("Missing id")
val user = userRepository.findById(id) // suspend 函数
call.respond(user)
}
}
}
在 Ktor 中,你可以直接调用 suspend 函数进行数据库查询、网络调用,而无需关心线程池的细节。框架使用 Dispatchers.IO 或自定义调度器处理阻塞操作,并通过结构化并发确保请求结束时所有子协程被自动取消。
高并发下的协程优势
传统 Servlet 容器(如 Tomcat)为每个请求分配一个线程。当并发请求达到数千时,线程上下文切换的开销会急剧上升,内存也会被线程栈耗尽。
协程后端框架(Ktor、Spring WebFlux with coroutines)则可以为每个请求分配一个协程。协程的内存占用极小(几百字节),且能在 I/O 等待时让出线程,使得少量线程就能支撑海量并发。
JavaScript 单线程上的协程:Promise 的华丽转身
JS 平台的协程实现
Kotlin/JS 将 Kotlin 代码编译为 JavaScript,运行在浏览器或 Node.js 的单线程事件循环上。协程的"挂起"在 JS 中如何实现不阻塞事件循环?
答案在于 Promise 。Kotlin/JS 的协程编译器会将 suspend 函数转换为返回 Promise 的 JavaScript 函数。状态机的挂起与恢复,最终映射为 Promise.then() 的链式调用。
javascript
// Kotlin suspend fun fetchData(): String
// 编译后大致等价于:
function fetchData(continuation) {
return new Promise((resolve, reject) => {
// 状态机逻辑...
});
}
与 JavaScript 异步互操作
你可以直接在 Kotlin/JS 中调用返回 Promise 的 JavaScript 函数,Kotlin 提供了 Promise<T>.await() 扩展函数,将其转换为 suspend 函数:
kotlin
suspend fun fetchFromJsApi(): String {
val promise = js("fetch('/api/data').then(res => res.text())")
return promise.await()
}
同样,Kotlin 的 suspend 函数也可以导出为返回 Promise 的函数,供 JavaScript 代码调用。
协程的未来:虚拟线程与结构化并发的推广
Project Loom 与协程的殊途同归
Java 19 引入了虚拟线程,其设计目标与 Kotlin 协程高度相似:让开发者以同步代码风格编写高并发程序,且不阻塞平台线程。
| 对比项 | Kotlin 协程 | Java 虚拟线程 |
|---|---|---|
| 挂起机制 | 编译器生成状态机 | JVM 底层支持,可挂起任意栈帧 |
| 语言关键字 | suspend |
无需关键字,普通方法即可 |
| 调度器 | 用户态调度器 | JVM 内置调度器 |
| 成熟度 | 生产可用多年 | 较新,生态仍在建设 |
虚拟线程的普及可能会让协程在后端领域面临竞争,但在 Android 和 KMM 领域,协程的地位仍然不可撼动。
结构化并发的跨语言影响
Kotlin 协程提出的结构化并发 理念,正在影响其他语言的异步设计。Swift 的 Task 和 TaskGroup、Rust 的 async 作用域、甚至 Java 的 StructuredTaskScope,都在不同程度上借鉴了"父子协程生命周期约束"的思想。
协程的修仙之路,不仅在 Kotlin 内部延续,更在塑造整个异步编程的未来。
实战:KMM 共享 ViewModel 中的协程
让我们构建一个 KMM 项目中的共享 ViewModel,使用协程加载数据。
kotlin
// commonMain/kotlin/SharedViewModel.kt
expect class SharedViewModel() {
val uiState: StateFlow<UiState>
fun loadData()
}
// androidMain/kotlin/SharedViewModel.kt
actual class SharedViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
actual val uiState: StateFlow<UiState> = _uiState.asStateFlow()
actual fun loadData() {
viewModelScope.launch {
_uiState.value = try {
val data = repository.fetchData()
UiState.Success(data)
} catch (e: Exception) {
UiState.Error(e.message)
}
}
}
}
// iosMain/kotlin/SharedViewModel.kt
actual class SharedViewModel {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
actual val uiState: StateFlow<UiState> = _uiState.asStateFlow()
actual fun loadData() {
scope.launch {
_uiState.value = try {
val data = repository.fetchData()
UiState.Success(data)
} catch (e: Exception) {
UiState.Error(e.message)
}
}
}
fun clear() {
scope.cancel()
}
}
在 iOS 侧,你需要在合适的时机调用 clear() 来取消协程(例如在 deinit 中)。
常见误区与避坑指南
误区 1:认为 KMM 中 Dispatchers.Main 在 iOS 上不可用
Dispatchers.Main 在 Kotlin/Native 的 iOS 目标上是完全可用 的,它绑定到主线程的 GCD 队列。你可以在 iosMain 中安全地使用它来更新 UI(通过 Compose Multiplatform 或其他 UI 框架)。
误区 2:在后端中滥用 GlobalScope
kotlin
// 后端错误示例
fun handleRequest() {
GlobalScope.launch {
// 请求结束时协程不会自动取消
process()
}
}
正确做法 :使用请求级别的 CoroutineScope,或框架提供的 Scope。
误区 3:在 JS 平台使用 runBlocking
runBlocking 在 JS 平台上不可用 ,因为 JS 是单线程、无法阻塞事件循环。所有协程启动必须使用 launch 或 async 配合回调/Promise。
最佳实践
- 在 KMM 中,将
suspend函数和Flow定义在commonMain:最大化代码复用。 - 使用
expect/actual为不同平台提供调度器配置 :而不是在公共代码中硬编码Dispatchers.IO。 - 后端开发中,始终使用请求级或应用级的
CoroutineScope:避免协程泄漏。 - 在 JS 平台,优先使用
Promise.await()与 JavaScript 异步互操作。 - 关注 Project Loom 与协程的演进,适时调整技术选型。
九、总结与下回预告
恭喜,你已跳出 Android 三界,看到了协程在跨平台与后端开发的广袤天地!大乘境初阶修炼完成!
本讲核心收获:
- Kotlin 协程通过
common模块定义抽象,各平台负责底层实现。 - iOS 上的协程基于 GCD 和 Worker 线程,可与 Swift 异步互操作。
- 后端协程(Ktor)以请求为 Scope,少量线程支撑海量并发。
- JS 平台将协程编译为
Promise,运行于单线程事件循环。 - 结构化并发的理念正在影响 Swift、Java、Rust 等语言。
在下一讲 【大乘境·中阶】 中,我们将深入协程的源码核心:CoroutineScheduler 的任务分发算法、DispatchedContinuation 的拦截机制、以及协程的启动流程全解析。届时你将真正看透协程的每一行源码。
【当前境界修为面板】
| 当前境界 | 修炼技能 | 修炼进度 | 修炼心得 |
|---|---|---|---|
| 大乘境 · 初阶 | 1、KMM 跨平台协程诀 2、Ktor 后端协程术 3、JS Promise 转换法 |
当前进度 :35% 修为 :350/1000 下一突破 :[大乘境 · 中阶] (需领悟:CoroutineScheduler 源码、DispatchedContinuation 拦截机制、协程启动流程) |
协程不限于Android。一套suspend代码,横跨JVM、iOS、JS三界。 |
【本讲思考题】
-
表象题 :Kotlin 协程在 iOS 上的
Dispatchers.Main底层绑定的是什么? -
场景题 :你需要设计一个 KMM 共享的
ImageLoader,它使用协程加载网络图片,并需要在 Android 和 iOS 上都能限制最大并发数为 5。如何设计这个类的expect/actual结构? -
原理题 :为什么
runBlocking在 Kotlin/JS 平台上不可用?请从 JavaScript 事件循环的特性解释。
道友,大乘境的大门已敞开。协程的跨平台之道,让你不再局限于 Android 一隅。下一讲,我们将潜入协程源码的最深处,与 CoroutineScheduler 和 DispatchedContinuation 正面交锋。大乘境·中阶见。
欢迎一键四连 (
关注+点赞+收藏+评论)