第3章:协程取消基础 — 协作式取消、isActive、ensureActive 与 CancellationException
3.1 为何必须协作?
与 Java 原生线程的 stop() 不同,Kotlin 协程的取消是 协作式 的。这意味着运行时不会强行终止正在运行的协程,而是设置取消标志,由协程代码自身在合适的时机配合停止。
ViewModelScope 场景: 当
viewModelScope.onCleared() 被调用时,Job 层级中的取消标志被置为 false,并抛出 CancellationException。如果协程代码不主动检查取消,它将“免疫”取消,继续执行,造成资源浪费甚至对已销毁的 View 进行操作。
3.2 基础检查:isActive
isActive 是 CoroutineScope 的一个扩展属性。只要当前协程的 Job 处于活跃状态,它就返回 true。通常用于计算密集型的循环中。
// 在 ViewModelScope 中使用 isActive 跳出循环
viewModelScope.launch {
var i = 0
while (isActive) { // 每次迭代前检查取消标志
processData(i++)
}
}
特点:
isActive 只是读取一个 volatile 变量,开销极低。你需要手动处理退出逻辑(如 return 或 break)。3.3 简洁检查:ensureActive()
ensureActive() 是 kotlinx.coroutines 提供的便捷扩展函数。如果 Job 不再活跃,它会立即抛出 CancellationException,比手动检查 isActive 更简洁。
// 使用 ensureActive 减少样板代码
viewModelScope.launch {
repeat(10_000) { index ->
ensureActive() // 如果已取消,这里直接抛出 CancellationException
compute(index)
}
}
对比差异:
| 特性 | isActive | ensureActive() |
|---|---|---|
| 类型 | 属性(读布尔值) | 扩展函数 |
| 取消后行为 | 返回 false,需手动 return/break | 自动抛出 CancellationException |
| 性能损耗 | 几乎为零 | 极低(一次条件跳转) |
| 推荐场景 | 需要保存进度或分段退出的复杂循环 | 挂起点或循环中快速失败检查 |
3.4 CancellationException 的本质与处理
当协程被取消时,内部的 Job 会在 ensureActive() 或挂起点(如 delay()、withContext)抛出 JobCancellationException(CancellationException 的子类)。
核心原则: CancellationException 不是业务异常,它代表协程的“正常停止信号”。不应捕获后吞掉。
// 反模式:吞噬 CancellationException
viewModelScope.launch {
try {
delay(1000)
} catch (e: CancellationException) {
// 什么都不做 —— 取消被吞掉,协程状态不一致!
}
}
// 正确模式1:清理后重新抛出
viewModelScope.launch {
try {
delay(1000)
} catch (e: CancellationException) {
releaseLock()
throw e // 必须重新抛出以向上传播取消
}
}
// 正确模式2:使用 finally(官方推荐)
viewModelScope.launch {
try {
delay(1000)
} finally {
releaseLock() // 无论正常结束还是取消,都会执行
}
}
3.5 在 ViewModelScope 中的实际运行流程
当 ViewModel 被清除时,viewModelScope 的 Job 立即变成 取消中 状态。这个信号会沿着 Job 树向下传播到每个子协程。
下面的图展示了这一协作式取消的完整决策链路:
3.6 常见陷阱与调试方法
陷阱1:runCatching 吞噬取消
runCatching 会捕获所有异常,包括 CancellationException。这会导致协程无法正常结束,父协程认为任务已完成。
// 错误:取消被 runCatching 吞掉
viewModelScope.launch {
runCatching {
delay(1000)
}
// 协程继续运行,但 Job 已被取消
dangerousOperation() // 可能引发 IllegalStateException
}
陷阱2:CPU 密集循环中不检查取消
如果一个协程在做大量计算且没有挂起点(如
如果一个协程在做大量计算且没有挂起点(如
delay、withContext),它必须手动调用 ensureActive() 或检查 isActive。否则即使 ViewModel 已销毁,它仍然会阻塞线程。
调试工具: 在 Android Studio 中启用 -Dkotlinx.coroutines.debug 系统属性,可以在日志中看到协程的名称、状态和取消信息。结合 Job.cancel(cause: CancellationException) 自定义原因,可以更轻松地追踪取消来源。
3.7 性能与稳定性要点
- 开销对比:
isActive≈ 读 volatile 变量(<0.1ns);ensureActive()≈ volatile 读 + 一次条件分支(<0.5ns)。两者对性能影响极小。 - 稳定性收益: 不检查取消的协程可能导致
IllegalStateException(操作已销毁的视图)、内存泄漏(匿名内部类持有 Activity 引用)甚至 ANR(线程持续被占用)。 - 权威建议: 在 ViewModelScope 中,如果循环包含挂起点,优先使用
ensureActive();如果是纯 CPU 密集型且无挂起调用,使用while (isActive)结构。