在 Android 开发中,Jetpack 提供的 CoroutineScope(如 viewModelScope 或 lifecycleScope)提供了便捷的生命周期管理。当 Activity、Fragment 或 Lifecycle 结束时,这些 Scope 会自动取消所有正在运行的协程。如果你手动创建 CoroutineScope,务必保存 Job 实例并在不需要时调用 cancel 方法。
然而,在某些场景下,我们需要确保某个操作能够完整执行,即使用户已离开当前界面。例如写入数据库或向服务器发起关键的网络请求。如果此时 viewModelScope 或 lifecycleScope 被销毁并取消了协程,可能会导致数据不一致或服务中断。虽然 cancel 动作本身不会立即停止协程代码的执行(需要协程内部配合),但会抛出 CancellationException 中断流程。那么,如何确保重要任务不被意外取消呢?
协程与 WorkManager 的选择
如果你的操作生命周期长于 App 进程(例如后台发送日志),应优先使用 WorkManager。WorkManager 是用于调度在未来某个时间点执行的关键操作的库,即使应用被杀死也能保证执行。
只要 App 进程存活,协程就可以持续运行。对于那些需要在当前进程生命周期内有效,但在用户杀掉 App 时可以取消的操作(例如获取新闻列表),则适合使用协程。
协程中不应取消的操作场景
假设应用中有一个 ViewModel 和一个 Repository,逻辑如下:
class MyViewModel(private val repo: Repository) : ViewModel() {
fun callRepo() {
viewModelScope.launch {
repo.doWork()
}
}
}
class Repository(private val ioDispatcher: CoroutineDispatcher) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
veryImportantOperation() // 此操作不应被取消,至关重要
}
}
}
我们不希望 veryImportantOperation() 受 viewModelScope 控制,因为它可能在任何时候被取消。我们希望该操作的生命周期比 ViewModel 更长。如何实现这一点?
解决方案:在 Application 类中创建自定义 Scope
请在 Application 类中创建自己的 Scope,并在由它启动的协程中调用这些重要的操作。其他类(如 Repository)可以直接从 Application 中获取该 Scope。
与 GlobalScope 相比,创建自定义 CoroutineScope 的好处是可以灵活配置。例如,你可以配置 CoroutineExceptionHandler,指定线程池 Dispatcher,并将所有常见配置放在 CoroutineContext 中。
建议将其命名为 applicationScope,并且必须包含 SupervisorJob(),以防止协程中的异常在层次结构中传播(参考本系列关于异常处理的讨论)。
class MyApplication : Application() {
// 无需取消该 Scope,它会随进程终止而自动清理
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
}
我们不需要取消该 Scope,因为希望它在应用程序进程存活期间保持活跃。因此不持有对 SupervisorJob 的引用。我们可以使用这个 Scope 来运行协程,这些协程通常需要比调用处(如 ViewModel、Activity、Fragment)更长的生命周期。
最佳实践:对于不应取消的操作,请从 Application 中创建 CoroutineScope,并用该 Scope 创建协程。
每当你创建新的 Repository 实例时,传入我们在上面创建的 applicationScope。
launch 还是 async?
根据 veryImportantOperation() 的行为,你需要选择合适的构造器:
- launch:如果你不需要返回结果,或者结果不重要。等待完成可使用 join()。注意必须在 launch 块内手动处理异常。
- async:如果你需要返回结果,需调用 await() 等待完成。
以下是使用 launch 的方式:
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
externalScope.launch {
try {
veryImportantOperation()
} catch (e: Exception) {
// 在此处记录日志或处理异常
e.printStackTrace()
}
}.join()
}
}
}
或者使用 async:
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork(): Any {
withContext(ioDispatcher) {
doSomeOtherWork()
return externalScope.async {
veryImportantOperation()
}.await()
}
}
}
在 ViewModel 中用 viewModelScope 调用上面的 doWork() 后,无论外部 scope 状态如何,都不会影响 externalScope 的执行,即使 viewModelScope 被破坏。此外,doWork() 会在 veryImportantOperation() 完成之前阻塞返回,如同普通 suspend 函数调用。
使用 withContext 的替代方案
另一种方式是将 veryImportantOperation() 包在 externalScope 的 context 中:
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
withContext(externalScope.coroutineContext) {
veryImportantOperation()
}
}
}
}
这种方式需要注意以下风险:
- 取消行为:如果调用 doWork 的协程在执行 veryImportantOperation() 时被取消,它将一直执行到下一个退出节点,而不是在完成之后。
- 异常处理:当在 withContext 中使用 context 时,externalScope 中的 CoroutineExceptionHandler 可能失效,异常将被重新抛出。
替代方案分析
除了上述方案,还有其他实现方式,但各有优劣。
❌ GlobalScope
不建议直接使用 GlobalScope,原因如下:
- 硬编码风险:容易写出硬编码的 Dispatchers,这是不良实践。
- 测试困难:代码在不受控制的 Scope 中执行,难以管理协程执行和模拟。
- 上下文限制:无法像 applicationScope 那样为所有协程建立通用的 CoroutineContext。
建议:不要直接使用 GlobalScope。
❌ ProcessLifecycleOwner scope in Android
在 Android 中,androidx.lifecycle:lifecycle-process 库提供了 applicationScope,可通过 ProcessLifecycleOwner.get().lifecycleScope 访问。
在这种模式下,你需要传入 LifecycleOwner 而非 CoroutineScope。生产环境中需传入 ProcessLifecycleOwner.get()。
注意:此 Scope 默认使用 Dispatchers.Main.immediate,不适合后台工作。且必须将公共 CoroutineContext 传递给启动的所有协程。
相比在 Application 类中创建 CoroutineScope,这种方法更繁琐,且增加了 ViewModel 层与 Android Lifecycle 的耦合。
建议:不要直接使用它。
✅ NonCancellable
你可以使用 withContext(NonCancellable) 在被取消的协程中调用 suspend 函数。这通常用于清理资源。
class Repository(private val ioDispatcher: CoroutineDispatcher) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
withContext(NonCancellable) {
veryImportantOperation()
}
}
}
}
尽管简洁,但风险很高:
- 不可控性:你无法控制协程的执行流。
- 测试问题:在测试中难以结束这些操作。
- 死循环风险:使用延迟的无限循环将永远无法被取消。
- Flow 污染:从其中收集 Flow 会导致 Flow 也变得无法从外部取消。
建议:仅用它来挂起清理操作相关的代码。
小结
- 自定义 Scope:每当你需要执行超出当前作用域范围的工作时,建议在 Application 类中创建自定义作用域,并在此作用域中执行协程。
- 避免滥用:在执行这类任务时,避免使用 GlobalScope、ProcessLifecycleOwner 作用域或滥用 NonCancellable。
- 异常处理:务必在自定义 Scope 中配置 CoroutineExceptionHandler,防止未捕获异常导致整个 Scope 崩溃。
- 测试策略:在单元测试中,可以通过注入 Mock Scope 或手动控制 Scope 生命周期来验证逻辑。
- 内存管理:虽然 Application Scope 随进程死亡而终止,但仍需注意避免持有 Activity/Fragment 的强引用以防内存泄漏。

