协程中的取消与异常:取消操作详解

在 Android 开发中,及时取消不再需要的协程对于节约设备电量和内存至关重要。协程的取消并非“强制杀死”,而是一种协作式(Cooperative)的过程。


1. 取消的层级特性

取消作用域会取消其所有子协程

当你取消一个 CoroutineScope 时,它会传播并取消该作用域下启动的所有子协程。这在 Activity 销毁时批量清理任务非常有用。

1
2
3
4
5
6
// 假设 scope 是一个已经定义好的作用域
val job1 = scope.launch { /* ... */ }
val job2 = scope.launch { /* ... */ }

// 取消作用域,job1 和 job2 都会被取消
scope.cancel()

单独取消某个协程不影响兄弟协程

如果你只想停止某个特定的任务,可以单独调用该 Job 的 cancel() 方法,这不会影响同一个作用域下的其他协程。

1
2
3
4
5
val job1 = scope.launch { /* ... */ }
val job2 = scope.launch { /* ... */ }

// 只取消第一个协程,第二个不受影响
job1.cancel()

2. 取消的底层机制:CancellationException

协程通过抛出一个特殊的异常 CancellationException 来处理取消。

  • 调用 cancel() 时可以传入原因。
  • 这是一个静默异常,不会导致应用崩溃,但它会沿着协程树向上通知父协程,向下取消子协程。
1
2
3
4
// 源码逻辑片段
public override fun cancel(cause: CancellationException?) {
cancelInternal(cause ?: defaultCancellationException())
}

3. 为什么我的协程调用了 cancel 却没有停止?

这是初学者最常遇到的坑。协程的取消是协作式的。如果你的代码正在执行密集的计算任务且没有检查取消状态,它会一直运行直到任务结束。

错误示例:不可取消的协程

1
2
3
4
5
6
7
8
9
10
11
12
val job = launch(Dispatchers.Default) {
var i = 0
while (i < 5) { // 没有任何地方检查取消状态
if (System.currentTimeMillis() >= nextTime) {
println("这是第${i}次")
i++
nextTime += 1000
}
}
}
delay(1000)
job.cancel() // 调用了取消,但上面的 while 循环依然会跑完

4. 如何让协程变得“可取消”?

要让协程支持取消,你需要定期检查其活跃状态。

方法 A:检查 isActiveensureActive()

在循环体中检查 isActive 状态。ensureActive() 的好处是它会在已取消时直接抛出异常。

1
2
3
4
5
val job = launch(Dispatchers.Default) {
while (i < 5 && isActive) { // 检查 isActive
// 执行任务...
}
}

方法 B:使用 yield()

yield() 会检查当前协程是否已取消。如果是,则抛出 CancellationException。此外,它还会让出 CPU 资源,让其他协程有机会运行。

方法 C:调用挂起函数

绝大多数官方挂起函数(如 delay(), withContext(), await())都是可取消的。它们在执行前都会自动检查取消状态。


5. 处理取消后的清理工作

当协程被取消时,你可能需要释放资源或打印日志。

使用 try-finally

由于取消会抛出异常,finally 块是执行清理逻辑的最佳场所。

1
2
3
4
5
6
7
8
val job = launch {
try {
doWork()
} finally {
// 无论是否被取消,这里都会执行
println("清理资源...")
}
}

特殊场景:在取消后仍需执行挂起函数

默认情况下,已经取消的协程无法再次挂起。如果你必须在 finally 中调用挂起函数(例如关闭数据库连接),需要将其包装在 NonCancellable 上下文中:

1
2
3
4
5
6
finally {
withContext(NonCancellable) {
delay(1000L) // 在已取消的协程中仍能正常挂起
println("清理完成")
}
}

6. 核心面试考点总结

  1. 取消是协作的:协程不会被强行停止,必须在代码中检查 isActive 或调用挂起函数。
  2. join vs cancel:调用 cancel() 只是发出取消指令,协程可能还在运行;调用 cancelAndJoin() 则会挂起当前协程,直到目标协程彻底结束。
  3. NonCancellable 的用途:专门用于清理逻辑中需要调用挂起函数的场景。
,