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。

后记

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容