Kotlin 协程原理与 Android 中的最佳实践
一句话收益:读完本文,你将彻底理解协程的挂起/恢复机制、结构化并发模型,并掌握在 Android 实际工程中避开常见陷阱的完整方案。
适用版本:Kotlin 1.7+,kotlinx-coroutines-core 1.6+,Android API 21+
阅读时长:约 20 分钟

1. 从一个真实问题切入
你接手了一个列表页,点击刷新按钮后:
- 发起网络请求获取数据
- 解析后写入数据库
- 从数据库读取并更新 UI
用回调嵌套写完,代码层数已经超过屏幕宽度,测试同学反馈偶发 NetworkOnMainThreadException,内存泄漏工具提示 Activity 未回收。
这是协程真正解决的问题域。
2. 协程核心原理:挂起与恢复
2.1 CPS 变换------编译器做了什么
Kotlin 协程在语言层面的核心是 Continuation Passing Style (CPS) 变换。编译器将 suspend 函数改写为带 Continuation 参数的普通函数:
kotlin
// 你写的代码
suspend fun fetchUser(id: Int): User
// 编译器实际生成(伪代码)
fun fetchUser(id: Int, continuation: Continuation<User>): Any?
Continuation<T> 接口定义在 kotlin.coroutines 包:
kotlin
// kotlin/coroutines/Continuation.kt(AOSP 对应 kotlinx.coroutines 库)
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
当挂起发生时,函数返回特殊标记 COROUTINE_SUSPENDED(kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED);恢复时由调度器调用 continuation.resumeWith(),从上次挂起点继续执行。
2.2 状态机------挂起点切割
每个 suspend 函数会被编译为一个状态机 ,每个 suspend 调用点对应一个状态:
fetchUser() 状态机示意
┌──────────────────────────────────────┐
│ State 0: 初始状态 │
│ → 调用 apiService.getUser() │
│ → 返回 COROUTINE_SUSPENDED │
│ → 保存局部变量到 Continuation │
├──────────────────────────────────────┤
│ State 1: getUser 恢复后 │
│ → 调用 userDao.insert(user) │
│ → 返回 COROUTINE_SUSPENDED │
├──────────────────────────────────────┤
│ State 2: insert 恢复后 │
│ → 返回最终结果 User │
└──────────────────────────────────────┘
关键类路径:kotlinx/coroutines/internal/CoroutineStackFrame.kt、kotlin/coroutines/jvm/internal/BaseContinuationImpl.kt
2.3 调度器本质:线程池封装
CoroutineDispatcher 继承关系
CoroutineDispatcher
│
┌────────────┼───────────────────┐
│ │ │
Dispatchers.Main Dispatchers.IO Dispatchers.Default
(主线程 Handler) (IO线程池,64线程上限) (CPU线程池,核心数线程)
Dispatchers.IO 底层使用 LimitingDispatcher(kotlinx/coroutines/scheduling/LimitingDispatcher.kt),线程数上限由 systemProp.kotlinx.coroutines.io.parallelism 控制,默认 max(64, CPU核心数)。
Dispatchers.Main 在 Android 中由 HandlerContext(kotlinx/coroutines/android/HandlerContext.kt)实现,本质是将任务 post 到主线程 Looper。
3. 结构化并发:协程作用域模型
3.1 CoroutineScope 与父子关系
Job 父子树示意
viewModelScope (SupervisorJob)
│
├── launch { fetchUserList() } ← Job A
│ └── async { parseData() } ← Job B(A的子Job)
│
└── launch { loadBanner() } ← Job C
规则:
- 父 Job 取消 → 所有子 Job 取消
- 子 Job 异常(非
CancellationException)→ 默认传播到父 Job(SupervisorJob例外) - 父 Job 等待所有子 Job 完成才算完成
关键实现:kotlinx/coroutines/JobSupport.kt 中的 childCancelled()、notifyParentChildException()
3.2 Android 内置 Scope
| Scope | Job 类型 | 取消时机 | 适用场景 |
|---|---|---|---|
viewModelScope |
SupervisorJob |
ViewModel onCleared | 业务数据加载 |
lifecycleScope |
SupervisorJob |
Lifecycle DESTROYED | UI 绑定的操作 |
rememberCoroutineScope() |
Job |
Composable 离开 Composition | Compose 内事件处理 |
GlobalScope |
⚠️ 无父级 | 永不自动取消 | 慎用! |
4. 代码实战:错误写法 → 问题 → 正确写法
4.1 在 ViewModel 中发起网络请求
❌ 错误写法
kotlin
class UserViewModel : ViewModel() {
fun loadUser(id: Int) {
// 错误:直接使用 GlobalScope,ViewModel 销毁后协程仍在运行
GlobalScope.launch {
val user = repository.fetchUser(id)
// 切回主线程更新 UI
withContext(Dispatchers.Main) {
_user.value = user
}
}
}
}
问题 :GlobalScope 的协程不与 ViewModel 生命周期绑定,ViewModel 已 onCleared() 后协程仍可能持有旧引用导致内存泄漏;_user.value 赋值时 ViewModel 可能已销毁。
✅ 正确写法
kotlin
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user.asStateFlow()
fun loadUser(id: Int) {
viewModelScope.launch { // 绑定 ViewModel 生命周期
_user.value = repository.fetchUser(id) // StateFlow 线程安全
}
}
}
// Repository 层:使用 withContext 切换线程,而非 Dispatchers 参数
class UserRepository(private val api: UserApi, private val dao: UserDao) {
suspend fun fetchUser(id: Int): User = withContext(Dispatchers.IO) {
val user = api.getUser(id) // 网络请求(已在 IO 线程)
dao.insert(user) // 写数据库
dao.getUser(id) // 读数据库返回
}
}
4.2 并行请求的正确姿势
❌ 错误写法
kotlin
// 两个请求串行执行,总耗时 = t1 + t2
viewModelScope.launch {
val user = repository.fetchUser(id)
val posts = repository.fetchPosts(id)
updateUI(user, posts)
}
✅ 正确写法(并行)
kotlin
viewModelScope.launch {
// async 立即启动,不等待结果
val userDeferred = async { repository.fetchUser(id) }
val postsDeferred = async { repository.fetchPosts(id) }
// await() 才真正挂起等待
val user = userDeferred.await() // 等待完成
val posts = postsDeferred.await() // 等待完成
// 总耗时 = max(t1, t2)
updateUI(user, posts)
}
4.3 异常处理:async 与 launch 的差异
关键区别 :launch 的异常直接传播到 CoroutineExceptionHandler;async 的异常在 await() 时抛出。
kotlin
// launch 异常处理
val handler = CoroutineExceptionHandler { _, throwable ->
Log.e("TAG", "Coroutine failed", throwable)
}
viewModelScope.launch(handler) {
throw RuntimeException("launch error") // 立即触发 handler
}
// async 异常处理(必须 try-catch await)
viewModelScope.launch {
val deferred = async {
throw RuntimeException("async error") // 这里不会立即崩溃
}
try {
deferred.await() // 异常在此处抛出
} catch (e: RuntimeException) {
Log.e("TAG", "Async failed", e)
}
}
4.4 取消协程:协作式取消
协程取消是协作式的,IO 阻塞操作默认不响应取消:
kotlin
// ❌ 问题写法:Thread.sleep 不响应取消
viewModelScope.launch {
Thread.sleep(5000) // 即使 ViewModel 销毁,这里依然阻塞线程
}
// ✅ 正确写法:使用 delay(可取消的挂起函数)
viewModelScope.launch {
delay(5000) // 收到取消信号立即抛出 CancellationException
}
// ✅ 在循环中手动检查取消状态
viewModelScope.launch {
val items = heavyList()
for (item in items) {
ensureActive() // 等价于 if (!isActive) throw CancellationException()
processItem(item) // CPU 密集操作
}
}
5. Android 最佳实践
5.1 Repository 层:suspend + withContext
做法 :Repository 的所有挂起函数用 withContext(Dispatchers.IO) 包裹,调用方无需关心线程切换。
原因:Dispatcher 决策应在最接近 IO 操作的地方发生,ViewModel 不应关心"这个操作在哪个线程"。
对比 :若不这样做,ViewModel 需要自行处理 withContext,导致业务逻辑与线程调度耦合,单元测试时难以用 TestCoroutineDispatcher 替换。
5.2 Flow:冷流与热流的选择
做法 :数据库查询返回 Flow<T>,UI 层用 repeatOnLifecycle(Lifecycle.State.STARTED) 收集。
kotlin
// ViewModel
val users: StateFlow<List<User>> = userRepository
.getAllUsers() // 返回 Flow<List<User>>(Room 自动发射更新)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000), // 停止收集5秒后取消上游
initialValue = emptyList()
)
// Fragment/Activity
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { // 前台时收集,后台自动暂停
viewModel.users.collect { users ->
adapter.submitList(users)
}
}
}
原因 :SharingStarted.WhileSubscribed(5000) 在 UI 进入后台 5 秒后取消上游 Flow,防止后台无效订阅消耗资源;repeatOnLifecycle 保证仅在 STARTED 状态收集,避免在 STOPPED 状态更新 UI 导致崩溃。
对比 :lifecycleScope.launchWhenStarted 已被弃用(Deprecated),因为它只暂停协程但不取消上游 Flow,仍会持续消耗资源。
5.3 测试:TestCoroutineDispatcher
kotlin
@OptIn(ExperimentalCoroutinesApi::class)
class UserViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule() // 替换 Dispatchers.Main
@Test
fun `loadUser updates user state`() = runTest {
val fakeRepo = FakeUserRepository()
val viewModel = UserViewModel(fakeRepo)
viewModel.loadUser(1)
advanceUntilIdle() // 执行所有挂起的协程
assertEquals(expectedUser, viewModel.user.value)
}
}
// MainDispatcherRule 实现
class MainDispatcherRule(
private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
6. 常见坑点
坑 1:在 suspend 函数中错误捕获 CancellationException
现象 :协程明明取消了,但相关逻辑还在执行,cancel() 无效。
原因 :CancellationException 是协程取消的信号,如果被 catch (e: Exception) 吞掉,取消信号丢失。
复现:
kotlin
viewModelScope.launch {
try {
delay(10000)
} catch (e: Exception) {
// 错误!CancellationException 被吞掉
Log.e("TAG", "error: $e")
// 后续逻辑仍然执行
}
}
解决:
kotlin
try {
delay(10000)
} catch (e: CancellationException) {
throw e // 必须重新抛出,让取消信号传播
} catch (e: Exception) {
Log.e("TAG", "real error: $e")
}
// 或者只捕获具体异常
try {
delay(10000)
} catch (e: IOException) {
// 只处理 IO 异常
}
坑 2:在非挂起函数中调用 runBlocking 阻塞主线程
现象:主线程 ANR,UI 冻结。
原因 :runBlocking 阻塞调用线程直到协程完成,在主线程调用等于将异步操作变回同步。
复现:
kotlin
// Activity/Fragment 中
fun onButtonClick() {
val user = runBlocking { repository.fetchUser(id) } // 阻塞主线程!
updateUI(user)
}
解决:
kotlin
// 改用 lifecycleScope
fun onButtonClick() {
lifecycleScope.launch {
val user = repository.fetchUser(id) // 在协程内挂起等待,主线程不阻塞
updateUI(user)
}
}
坑 3:viewModelScope.launch 后立即销毁 ViewModel,异常无处处理
现象 :ViewModel.onCleared() 后协程内的异常 Crash。
原因 :viewModelScope 默认不安装 CoroutineExceptionHandler,未处理的异常会走 Thread.uncaughtExceptionHandler。
解决:
kotlin
viewModelScope.launch(
CoroutineExceptionHandler { _, throwable ->
_errorEvent.value = throwable.message
}
) {
repository.fetchUser(id)
}
坑 4:collect 未在 repeatOnLifecycle 中使用
现象 :App 进入后台后 Flow 仍然触发 UI 更新,偶发 IllegalStateException: Can not perform this action after onSaveInstanceState。
原因 :直接在 lifecycleScope.launch 中 collect 不会在 UI 不可见时自动停止。
解决 :见第 5.2 节 repeatOnLifecycle 写法。
7. 总结
- CPS 变换 + 状态机 :协程挂起/恢复的本质是编译器自动生成的状态机,
COROUTINE_SUSPENDED是暂停信号。 - 结构化并发 :用
viewModelScope/lifecycleScope替代GlobalScope,让协程与生命周期绑定。 - Dispatcher 就近原则 :在 Repository 层用
withContext(Dispatchers.IO)切换线程,ViewModel 保持干净。 - Flow + repeatOnLifecycle :冷热流选型合理,
SharingStarted.WhileSubscribed平衡性能与实时性。 - CancellationException 不能吞:取消是协作式的,捕获后必须重新抛出。
核心结论:协程不是魔法,而是编译器辅助的 CPS 变换 + 调度器封装;掌握结构化并发模型,才能在 Android 复杂生命周期场景下写出真正健壮的异步代码。
参考资料
- Kotlin 协程官方文档 - 取消与超时
- Android 开发者 - 在 Android 上使用协程的最佳做法
- Android 开发者 - 使用 Flow 管理 UI 状态
- AOSP 协程源码路径:
external/kotlinx.coroutines/kotlinx-coroutines-core/common/src/ BaseContinuationImpl:libraries/stdlib/jvm/src/kotlin/coroutines/jvm/internal/BaseContinuationImpl.ktHandlerContext(Android 主线程调度器):external/kotlinx.coroutines/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt