kotlin协程五

前言

翻译自不应该被取消的工作

背景

有时候,即使退出屏幕也想将一个操作完成,这种场景下,不想工作被取消(例如,写入数据库或向服务器发送一个网络请求)

协程或workmanager?

协程会运行的和你的应用程序一样久,如果要让一些操作超出运行的范围时长(例如传log给服务器),使用workmanager。woekmanger是用来在未来某个特定时间运行的关键操作的库。协程是用于在运行期间且在杀死APP时要被取消的工作(例如缓存网络请求)。触发这些操作的方式是什么?

协程最佳实践

因为这种模式基于其他协程的最好实践,让我们回顾下:

  1. 将dispatchers注入类
    创建新的协程或使用withcontext,不要使用硬编码,这样可以轻松地将它们替换,便于测试。
  2. 在viewmodel或者Presenter层创建协程
  3. 在viewmodel或者Presenter层之下的层应该暴露suspend函数和Flows,好处是调用者(通常是ViewModel层)可以控制在这些层中进行的工作的执行和生命周期,并可以在需要时取消。

携程中不应该取消的操作

  1. 如果有这样一种情况
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开启一个新的协程:

  1. 如果需要返回结果,用async,调用await等待完成。
  2. 如果不需要结果,用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()
      }
    }
  }
}

然而,这种方式需要你注意:

  1. 如果在执行veryImportantOperation时取消调用doWork的协程,则它将继续执行直到下一个取消点,而不是在veryImportantOperation完成执行之后。
  2. CoroutineExceptionHandlers不会像你期待的那样工作,因为当context在withContext中使用时,异常会被重新抛出。

测试

注入

备选方案

这有一些其他的方式去用协程实现这些行为。但是,这些方案不能再所有的用例中系统的使用。让我们看这些备选方案以及是否应该用他们。

❌ GlobalScope

这里有很多不可以用GloableScope的理由:

  1. 如果你直接用GloableScope,写死Dispatchers也许是很诱人的,但这是一个坏尝试。
  2. 让测试变得困难
  3. 您不能像我们使用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()
      }
    }
  }
}

会导致什么问题呢?

  1. 无法在测试中停止这些操作
  2. 使用延迟的死循环将无法被取消
  3. 在它内部的流在外部无法被取消
    等等
    这些问题可能会导致难以捉摸的bug调试.
    建议:仅在清除代码中使用它

无论何时你需要超出当前scope做一些工作,我们都建议在application类中创建一个自定义scope,并在它里面运行协程。对于这种类型,避免使用GlobalScope、ProcessLifecycleOwner scope、NonCancellable。

后记

翻译完了,感叹纸上得来终觉浅,绝知此事要躬行。还是得实践出真知~

你可能感兴趣的:(kotlin协程五)