美文网首页
kotlin协程五

kotlin协程五

作者: crossroads | 来源:发表于2021-02-04 20:20 被阅读0次

    前言

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

    背景

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

    协程或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协程五

          本文链接:https://www.haomeiwen.com/subject/jtwdzktx.html