协程中的取消和异常 | 驻留任务详解
viewModelScope https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/package-summary#(androidx.lifecycle.ViewModel).viewModelScope:kotlinx.coroutines.CoroutineScope lifecycleScope https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/package-summary#lifecyclescope
协程还是 WorkManager?
WorkManager https://developer.android.google.cn/topic/libraries/architecture/workmanager
协程的最佳实践
由于本文所介绍的模式是在协程的其它最佳实践的基础之上实现的,我们可以借此机会回顾一下:
1. 将调度器注入到类中
2. 应当在 ViewModel 或 Presenter 层创建协程
3. ViewModel 或 Presenter 以下的层级,应当暴露挂起函数与 Flow
coroutineScope https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html supervisorScope https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html
协程中那些不应当被取消的操作
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() // 它不应当被取消
}
}
}
CoroutineExceptionHandler https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/
class MyApplication : Application() {
// 不需要取消这个作用域,因为它会随着进程结束而结束
val applicationScope = CoroutineScope(SupervisorJob() + otherConfig)
}
从 application CoroutineScope 创建的协程中调用那些不应当被取消的操作
应该使用哪种协程构造器?
如果需要返回结果,请使用 async 并调用 await 来等待其完成;
如果不是,请使用 launch 并调用 join 来等待其完成。请注意,如本系列第三部分所述,您必须在 launch 块内部手动处理异常。
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
externalScope.launch {
//如果这里会抛出异常,那么要将其包裹进 try/catch 中;
//或者依赖 externalScope 的 CoroutineScope 中的 CoroutineExceptionHandler
veryImportantOperation()
}.join()
}
}
}
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork(): Any { // 在结果中使用特定类型
withContext(ioDispatcher) {
doSomeOtherWork()
return externalScope.async {
// 异常会在调用 await 时暴露,它们会在调用了 doWork 的协程中传播。
// 注意,如果正在调用的上下文被取消,那么异常将会被忽略。
veryImportantOperation()
}.await()
}
}
}
有没有更简单的解决方案呢?
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
withContext(externalScope.coroutineContext) {
veryImportantOperation()
}
}
}
}
但是,此方法有下面几个注意事项,使用的时候需要注意:
如果调用 doWork() 的协程在 veryImportantOperation 开始执行时被退出,它将继续执行直到下一个退出节点,而不是在 veryImportantOperation 结束后退出;
CoroutineExceptionHandler 不会如您预期般工作,这是因为在 withContext 中使用上下文时,异常会被重新抛出。
测试
测试时要注入什么
🔖 说明文档:
TestCoroutineDispatcher https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-dispatcher/index.html MainCoroutineRule https://github.com/android/plaid/blob/master/test_shared/src/main/java/io/plaidapp/test/shared/MainCoroutineRule.kt TestCoroutineScope https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-coroutine-scope/ AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher() https://github.com/google/iosched/blob/adssched/mobile/src/androidTest/java/com/google/samples/apps/iosched/tests/di/TestCoroutinesModule.kt#L36
替代方案
❌ GlobalScope
下面是几个不应该使用 GlobalScope 的理由:
诱导我们写出硬编码值。直接使用 GlobalScope 可能会让我们倾向于写出硬编码的调度器,这是一种很差的实践方式。
导致测试非常困难。由于您的代码会在一个不受控制的作用域中执行,您将无法对从中启动的任务进行管理。
就如同我们对 applicationScope 所做的那样,您无法为所有协程都提供一个通用的、内建于作用域中的 CoroutineContext。相反,您必须传递一个通用的 CoroutineContext 给 GlobalScope 启动的所有协程。
GlobalScope https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/
⚠️ 特别说明
class MyApplication : Application() {
val applicationScope = GlobalScope
}
❌ ✅ 使用 NonCancellable
class Repository(
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
withContext(NonCancellable){
veryImportantOperation()
}
}
}
}
尽管这个方案很有诱惑力,但是您可能无法总是知道 someImportantOperation() 背后有什么逻辑。它可能是一个扩展库;也可能是一个接口背后的实现。它可能会导致各种各样的问题:
您将无法在测试中结束这些操作;
使用延迟的无限循环将永远无法被取消;
从其中收集 Flow 会导致 Flow 也变得无法从外部取消;
…...
推荐阅读