前言
翻译自不应该被取消的工作
背景
有时候,即使退出屏幕也想将一个操作完成,这种场景下,不想工作被取消(例如,写入数据库或向服务器发送一个网络请求)
协程或workmanager?
协程会运行的和你的应用程序一样久,如果要让一些操作超出运行的范围时长(例如传log给服务器),使用workmanager。woekmanger是用来在未来某个特定时间运行的关键操作的库。协程是用于在运行期间且在杀死APP时要被取消的工作(例如缓存网络请求)。触发这些操作的方式是什么?
协程最佳实践
因为这种模式基于其他协程的最好实践,让我们回顾下:
- 将dispatchers注入类
创建新的协程或使用withcontext,不要使用硬编码,这样可以轻松地将它们替换,便于测试。 - 在viewmodel或者Presenter层创建协程
- 在viewmodel或者Presenter层之下的层应该暴露suspend函数和Flows,好处是调用者(通常是ViewModel层)可以控制在这些层中进行的工作的执行和生命周期,并可以在需要时取消。
携程中不应该取消的操作
- 如果有这样一种情况
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() // This shouldn’t be cancelled
}
}
}
我们不想要veryImportantOperation()在任意时刻被取消。想要它超出viewmodelScope的生命周期,该如何实现呢?
在application类创建你自己的scope,并且通过它在协程开始的时候调用这些操作。这个scope应该被注入需要它的类中。
创建自己的CoroutineScope的好处 vs 其他解决方案(例如GlobalScope)是你可以按自己想要的方式配置。例入是否需要CoroutineExceptionHandler,是否需要单独的线程池作为Diapatcher?将所有配置放在CoroutineContext中.
可以调用applicationScope,它必须包含SupervisorJob(),这样协程的失败不会传递。
class MyApplication : Application() {
// 无需取消这个scope,因为会随着程序拆除
val applicationScope = CoroutineScope(SupervisorJob() + otherConfig)
}
对于不应该被取消的操作,通过application CoroutineScope创建的协程调用。
无论何时,当你创建Repository的实例时,将上面的applicationscope传过去。
用哪种构建器?
基于veryImportantOperation的行为,你需要使用launch或async开启一个新的协程:
- 如果需要返回结果,用async,调用await等待完成。
- 如果不需要结果,用launch,用join阻塞,直到结束。你可以在launch块中处理异常
下面是如何用launch触发协程:
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
externalScope.launch {
//如果会抛出异常,用try/catch包裹或依赖externalScope的CoroutineExceptionHandler
veryImportantOperation()
}.join()
}
}
}
如果用async
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork(): Any { // Use a specific type in Result
withContext(ioDispatcher) {
doSomeOtherWork()
return externalScope.async {
// 异常在调用await时抛出,会在协程中传递到doWork.注意:如果调用的context取消,会被忽略。
veryImportantOperation()
}.await()
}
}
}
在上面的示例中,即使viewmodelScope销毁,使用externalScope的任务仍会运行。此外,dowork()直到 veryImportantOperation()完成后才会返回。
那更简单的事情呢?
另一种模式可以服务于多种情况(也可能是可以拿出的第一种选择),那就是在externalScope的context中包裹veryImportantOperation.
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
withContext(externalScope.coroutineContext) {
veryImportantOperation()
}
}
}
}
然而,这种方式需要你注意:
- 如果在执行veryImportantOperation时取消调用doWork的协程,则它将继续执行直到下一个取消点,而不是在veryImportantOperation完成执行之后。
- CoroutineExceptionHandlers不会像你期待的那样工作,因为当context在withContext中使用时,异常会被重新抛出。
测试
备选方案
这有一些其他的方式去用协程实现这些行为。但是,这些方案不能再所有的用例中系统的使用。让我们看这些备选方案以及是否应该用他们。
❌ GlobalScope
这里有很多不可以用GloableScope的理由:
- 如果你直接用GloableScope,写死Dispatchers也许是很诱人的,但这是一个坏尝试。
- 让测试变得困难
- 您不能像我们使用applicationScope那样为scope中的所有协程提供通用的CoroutineContext。相反,您必须将通用的CoroutineContext传递给GlobalScope启动的所有协程。
建议:不要直接使用它
❌ ProcessLifecycleOwner scope in Android
在Android中,androidx.lifecycle:lifecycle-process库提供了applicationScope,通过ProcessLifecycleOwner.get().lifecycleScope获得。
在这种情况下,应该注入LifecycleOwner而不是像我们之前做的注入CoroutineScope。在生产中,您需要传递ProcessLifecycleOwner.get(),在单元测试中,您可以使用LifecycleRegistry创建假的LifecycleOwner。
注意这里scope默认的CoroutineContext使用Dispatchers.Main.immediate,对于后台任务是不可取的。与GlobalScope一样,您必须将通用的CoroutineContext传递给GlobalScope启动的所有协程。
基于以上原因,备选方案比在application中创建一个CoroutineScope要做更多的工作。
建议:不要直接使用它
⚠️ 免责声明
如果事实证明,您的applicationScope的CoroutineContext与GlobalScope或ProcessLifecycleOwner.get().lifecycleScope匹配,则可以按如下所示直接分配它们
class MyApplication : Application() {
val applicationScope = GlobalScope
}
这样仍然拥有以上提到的好处,并可以在未来方便的更改它。
❌ ✅ 使用 NonCancellable
正如之前所述,您可以使用withContext(NonCancellable)来在已取消的协程中调用suspend函数。我们建议用它去调用可以suspend的清理代码。然而,你不应该滥用它。
这样做会带来很大的风险,因为您无法控制协程的执行。它可以使代码更简洁,更易于阅读,但将来可能引起的问题是无法预测的。
举个例子:
class Repository(
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
withContext(NonCancellable) {
veryImportantOperation()
}
}
}
}
会导致什么问题呢?
- 无法在测试中停止这些操作
- 使用延迟的死循环将无法被取消
- 在它内部的流在外部无法被取消
等等
这些问题可能会导致难以捉摸的bug调试.
建议:仅在清除代码中使用它
无论何时你需要超出当前scope做一些工作,我们都建议在application类中创建一个自定义scope,并在它里面运行协程。对于这种类型,避免使用GlobalScope、ProcessLifecycleOwner scope、NonCancellable。
后记
翻译完了,感叹纸上得来终觉浅,绝知此事要躬行。还是得实践出真知~