在 Android 开发中,及时取消不再需要的协程对于节约设备电量和内存至关重要。协程的取消并非“强制杀死”,而是一种协作式(Cooperative)的过程。
1. 取消的层级特性
取消作用域会取消其所有子协程
当你取消一个 CoroutineScope 时,它会传播并取消该作用域下启动的所有子协程。这在 Activity 销毁时批量清理任务非常有用。
1 | // 假设 scope 是一个已经定义好的作用域 |
单独取消某个协程不影响兄弟协程
如果你只想停止某个特定的任务,可以单独调用该 Job 的 cancel() 方法,这不会影响同一个作用域下的其他协程。
1 | val job1 = scope.launch { /* ... */ } |
2. 取消的底层机制:CancellationException
协程通过抛出一个特殊的异常 CancellationException 来处理取消。
- 调用
cancel()时可以传入原因。 - 这是一个静默异常,不会导致应用崩溃,但它会沿着协程树向上通知父协程,向下取消子协程。
1 | // 源码逻辑片段 |
3. 为什么我的协程调用了 cancel 却没有停止?
这是初学者最常遇到的坑。协程的取消是协作式的。如果你的代码正在执行密集的计算任务且没有检查取消状态,它会一直运行直到任务结束。
错误示例:不可取消的协程
1 | val job = launch(Dispatchers.Default) { |
4. 如何让协程变得“可取消”?
要让协程支持取消,你需要定期检查其活跃状态。
方法 A:检查 isActive 或 ensureActive()
在循环体中检查 isActive 状态。ensureActive() 的好处是它会在已取消时直接抛出异常。
1 | val job = launch(Dispatchers.Default) { |
方法 B:使用 yield()
yield() 会检查当前协程是否已取消。如果是,则抛出 CancellationException。此外,它还会让出 CPU 资源,让其他协程有机会运行。
方法 C:调用挂起函数
绝大多数官方挂起函数(如 delay(), withContext(), await())都是可取消的。它们在执行前都会自动检查取消状态。
5. 处理取消后的清理工作
当协程被取消时,你可能需要释放资源或打印日志。
使用 try-finally
由于取消会抛出异常,finally 块是执行清理逻辑的最佳场所。
1 | val job = launch { |
特殊场景:在取消后仍需执行挂起函数
默认情况下,已经取消的协程无法再次挂起。如果你必须在 finally 中调用挂起函数(例如关闭数据库连接),需要将其包装在 NonCancellable 上下文中:
1 | finally { |
6. 核心面试考点总结
- 取消是协作的:协程不会被强行停止,必须在代码中检查
isActive或调用挂起函数。 - join vs cancel:调用
cancel()只是发出取消指令,协程可能还在运行;调用cancelAndJoin()则会挂起当前协程,直到目标协程彻底结束。 - NonCancellable 的用途:专门用于清理逻辑中需要调用挂起函数的场景。