[译]Coroutines的取消和异常处理(Part 4)

811 阅读6分钟

原文作者 :Manuel Vivo

原文地址: Exceptions in coroutines

译者 : 京平城

Part 2中我们已经了解了及时取消Coroutines的重要性。在Android中,你可以使用Jetpack提供的CoroutineScopes,比如viewModelScope或者是lifecycleScope。与之关联的Activity/Fragment/Lifecycle在结束的时候,会自动取消正在运行着的Coroutines。如果你要使用自定义CoroutineScope,那么一定要注意使用Job句柄来控制取消。

但是在某些情况下,即使用户离开了所在的页面,我们也不希望正在运行着的Coroutines被取消。(比如正在往数据库中写入数据或者正在发送数据给服务器的时候)

本文将介绍如何处理这种情况。

使用Coroutines还是WorkManager?

Coroutines的生命周期和你的应用一样长。但当你希望需要执行的操作拥有更久的生命周期的时候(比如给你的服务器发送日志,译者注:一般发送日志会选在用户不使用App的时候,减少对用户体验可能造成的不好的影响),请使用WorkManager
WorkManager是一个可以保证某些关键操作一定能如预期执行的工具。
Coroutines在当前进程内可以有效的执行操作和取消,即使用户强行关闭了你的应用。

译者注:一般我建议执行某些重要的操作的时候,比如发送内购信息给服务器的时候,请使用WorkManager

Coroutines最佳实践

1. Inject Dispatchers into classes

创建新的coroutines或者调用withContext的时候,不要在代码中写死Dispatchers,可以使用注入的方式。

✅ Benefits: ease of testing as you can easily replace them for both unit and instrumentation tests.

2. The ViewModel/Presenter layer should create coroutines

译者注:原文有点绕,我总结一下作者的意思是UI层应该只处理和UI相关的逻辑,不应该创建任何coroutines,coroutines应该只属于ViewModel/Presenter层。

✅ Benefits: UI层不应该涉及业务层的逻辑,ViewModel/Presenter层是用来处理业务逻辑的。测试UI层需要模拟器来运行instrumentation(插桩)测试。

3. The layers below the ViewModel/Presenter layer should expose suspend functions and Flows

请使用coroutineScope或者supervisorScope创建coroutines。 如果你需要不一样的scope,那么本文会介绍解决方案,请读下去!

✅ Benefits: The caller (generally the ViewModel layer) can control the execution and lifecycle of the work happening in those layers, being able to cancel when needed.

Operations that shouldn’t be cancelled in Coroutines

假设我们有一个ViewModel和一个Repository,代码如下:

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控制,因为viewModelScope随时都都可能被取消。

我们可以使用一个和应用生命周期一样长的自定义的CoroutineScope解决这个问题,自定义CoroutineScope的优势在于可以使用自定义的CoroutineExceptionHandler和自定义的thread pool译者注:你可以定制一个完全符合自己需求的CoroutineContext)。

我们使用applicationScope来命名这个自定义的CoroutineScope,并且它必须包含一个SupervisorJob(),这样任何子coroutine的执行失败不会影响到该scope下其他的子coroutines,也不会导致applicationScope本身的退出。(见Part 3

class MyApplication : Application() {
  // No need to cancel this scope as it'll be torn down with the process
  val applicationScope = CoroutineScope(SupervisorJob() + otherConfig)
}

我们不需要处理applicationScope的退出,因为它的生命周期和我们的应用一样长,所以我们也不需要持有SupervisorJob()的句柄。我们可以使用applicationScope来启动那些需要和我们App生命周期一样长的coroutines。

对于那些我们不希望被取消执行的coroutines,我们都可以使用applicationScope

当你创建一个新的Repository实例的时候,就可以传入applicationScope

我们该使用哪个coroutine builder呢?

取决于veryImportantOperation的操作,你可以使用launch或者async来启动一个新的coroutine:

  • 如果需要返回值,那么请使用async启动并且调用await方法。
  • 如果不需要返回值,请使用launch启动并且调用join方法。注意处理异常,见part 3

代码如下:

class Repository(
  private val externalScope: CoroutineScope,
  private val ioDispatcher: CoroutineDispatcher
) {
  suspend fun doWork() {
    withContext(ioDispatcher) {
      doSomeOtherWork()
      externalScope.launch {
        // if this can throw an exception, wrap inside try/catch
        // or rely on a CoroutineExceptionHandler installed
        // in the externalScope's CoroutineScope
        veryImportantOperation()
      }.join()
    }
  }
}

译者注Repository的构造方法里面的externalScope参数就是一个生命周期和App一样长的自定义CoroutineScope,比如我们刚才创建的applicationScope
另外一个参数ioDispatcher就是在本文的Coroutines最佳实践第一条中提到的,总是用注入的形式使用CoroutineDispatcher而不是写死在代码里。
对于veryImportantOperation()可能抛出的异常,我们要使用try/catch或者使用externalScope中设置的CoroutineExceptionHandler(前提是你需要创建CoroutineExceptionHandler实例并且设置到applicationScope中去)。

使用async代码如下:

class Repository(
  private val externalScope: CoroutineScope,
  private val ioDispatcher: CoroutineDispatcher
) {
  suspend fun doWork(): Any { // 实际使用中请将Any改成自己需要的类型
    withContext(ioDispatcher) {
      doSomeOtherWork()
      return externalScope.async {
        // Exceptions are exposed when calling await, they will be
        // propagated in the coroutine that called doWork. Watch
        // out! They will be ignored if the calling context cancels.
        veryImportantOperation()
      }.await()
    }
  }
}

译者注:这里要注意veryImportantOperation()中可能会抛出异常,并且该异常会传播到调用 doWork()方法的coroutine去,如果该coroutine取消的话,那么异常可能会被忽略!
我记得在part 3中我们已经学过了,如果async是根coroutine的话(scope.async),那么我们可以在调用await()方法的时候执行try/catch就可以正确捕获异常了,搞不懂原文作者为啥没这样做。

使用上面的写法,我们的ViewModel层的代码都不需要任何变动,即使viewModelScope销毁了, externalScope中正在运行的coroutines也会一直运行直到结束。实际上,doWork()会一直挂起直到veryImportantOperation()执行完毕。

有没有更简单的写法?

另外一种实现方式是使用withContext

class Repository(
  private val externalScope: CoroutineScope,
  private val ioDispatcher: CoroutineDispatcher
) {
  suspend fun doWork() {
    withContext(ioDispatcher) {
      doSomeOtherWork()
      withContext(externalScope.coroutineContext) {
        veryImportantOperation()
      }
    }
  }
}

但是这种实现会有如下的弊端:

  • 如果调用doWork的coroutine取消了,那么veryImportantOperation会一直运行,直到下一个cancellation point(译者注:不清楚这里的cancellation point是什么意思),而不是veryImportantOperation执行完毕就取消。

  • CoroutineExceptionHandler也不能正常工作,因为在withContext中,异常会被重新抛出。

译者注:那么多弊端,我们还是别用这种实现方式了。

后面还有关于测试的,和为什么不要使用GlobalScopeProcessLifecycleOwner的scope和NonCancellabl类型(只能用来做清理工作)的,大家直接看原文吧(老外写东西太长了!)