[译] 如何优雅的处理协程的异常?

原文做者:Manuel Vivo html

原文地址:Exceptions in Coroutinesandroid

译者:秉心说git

本文是 协程的取消和异常 系列的第三篇,往期目录以下:github

Coroutines: First things firstweb

如何优雅的处理协程的取消?app

在阅读本文以前,强烈建议回顾一下以前两篇文章。实在没有时间的话,至少读一下第一篇文章。async

下面开始正文。编辑器


做为开发者,咱们一般会花费大量时间来完善咱们的应用。可是,当发生异常致使应用不按预期执行时尽量的提供良好的用户体验也是一样重要的。一方面,应用 Crash 对用户来讲是很糟糕的体验;另外一方面,当用户操做失败时,提供正确的信息也是必不可少的。函数

优雅的异常处理对用户来讲是很重要的。在这篇文章中,我会介绍在协程中异常是怎么传播的,以及如何使用各类方式控制异常的传播。测试

若是你更喜欢视频,能够观看 Florina Muntenescu 和我 在 KotlinConf'19 上的演讲,地址以下:

https://www.youtube.com/watch?v=w0kfnydnFWI&feature=emb_logo

为了帮你更好的理解本文的剩余内容,建议首先阅读该系列的第一篇文章 Coroutines: First things first

协程忽然失败了?怎么办?😱

当一个协程发生了异常,它将把异常传播给它的父协程,父协程会作如下几件事:

  1. 取消其余子协程
  2. 取消本身
  3. 将异常传播给本身的父协程

异常最终将传播至继承结构的根部。经过该 CoroutineScope 建立的全部协程都将被取消。

在某些场景下,这样的异常传播是适用的。可是,也有一些场景并不合适。

想象一个 UI 相关的 CoroutineScope ,它负责处理用户交互。若是它的一个子协程抛出了异常,那么这个 UI Scope 将被取消。因为被取消的做用域没法启动更多协程,整个 UI 组件将没法响应用户交互。

若是你不想要这样怎么办?或许,在建立协程做用域的 CoroutineContext 时,你能够选择不同的 Job 实现 —— SupervisorJob

让 SupervisorJob 拯救你

经过 SupervisorJob,子协程的失败不会影响其余的子协程。此外,SupervisorJob 也不会传播异常,而是让子协程本身处理。

你能够这样建立协程做用域 val uiScope = CoroutineScope(SupervisorJob()) ,来保证不传播异常。

若是异常没有被处理,CoroutineContext 也没有提供异常处理器 CoroutineExceptionHandler (稍后会介绍),将会使用默认的异常处理器。在 JVM 上,异常会被打印到控制台;在 Android 上,不管发生在什么调度器上,你的应用都会崩溃。

💥 不管你使用哪一种类型的 Job,未捕获异常最终都会被抛出。

一样的行为准则也适用于协程做用域构建器 coroutineScopesupervisorScope 。它们都会建立一个子做用域(以 Job 或者 SupervisorJob 做为 Parent),来帮助你给协程从逻辑上分组(若是你想进行并行计算,或者它们是否会相互影响)。

警告:SupervisorJob 仅在属于下面两种做用域时才起做用:使用 supervisorScope 或者 CoroutineScope(SupervisorJob()) 建立的做用域。

Job 仍是 SupervisorJob ?🤔

何时使用 Job ?何时使用 SupervisorJob

当你不想让异常致使父协程和兄弟协程被取消时,使用 SupervisorJob 或者 supervisorScope

看看下面这个示例:

// Scope handling coroutines for a particular layer of my app
val scope = CoroutineScope(SupervisorJob()) scope.launch {  // Child 1 } scope.launch {  // Child 2 } 复制代码

在这样的状况下,child#1 失败了,scopechild#2 都不会被取消。

另外一个示例:

// Scope handling coroutines for a particular layer of my app
val scope = CoroutineScope(Job()) scope.launch {  supervisorScope {  launch {  // Child 1  }  launch {  // Child 2  }  } } 复制代码

在这种状况下,supervisorScope 建立了一个携带 SupervisorJob 的子做用域。若是 child#1 失败,child#2 也不会被取消。可是若是使用 coroutineScope 来代替 supervisorScope 的话,异常将会传播并取消做用域。

测试!谁是个人父亲 ?🎯

经过下面的代码段,你能肯定 child#1 的父级是哪种 Job 吗?

val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {  // new coroutine -> can suspend  launch {  // Child 1  }  launch {  // Child 2  } } 复制代码

child#1 的父 Job 是 Job 类型 !但愿你回答正确!尽管第一眼看上去,你可能认为是 SupervisorJob,但并非。由于在这种状况下,每一个新的协程老是被分配一个新的 Job,这个新的 Job 覆盖了 SupervisorJobSupervisorJob 是父协程经过 scope.launch 建立的。也就是说,在上面的例子中,SupervisorJob 没有发挥任何做用。

The parent of child#1 and child#2 is of type Job, not SupervisorJob
The parent of child#1 and child#2 is of type Job, not SupervisorJob

因此,不管是 child#1 仍是 child#2 发生了异常,都将传播到 scope,并致使全部由其启动的协程被取消。

记住 SupervisorJob 仅在属于下面两种做用域时才起做用:使用 supervisorScope 或者 CoroutineScope(SupervisorJob()) 建立的做用域。SupervisorJob 做为参数传递给协程构建器并不会产生你所预期的效果。

关于异常,若是子协程抛出了异常,SupervisorJob 不会进行传播并让子协程本身去处理。

原理

若是你好奇 Job 的工做原理,能够在 JobSupport.kt 文件中查看 childCancellednotifyCancelling 这两个函数的实现。

对于 SupervisorJob 的实现,childCancelled() 方法仅仅只是返回 false ,表示它不会传播异常,同时也不会处理异常。

异常的处理 👩‍🚒

在协程中,可使用常规语法来处理异常:try/catch 或者内置的函数 runCatching (内部使用了 try/catch) 。

咱们以前说过 未捕获的异常始终会被抛出 。可是不一样的协程构建器对于异常有不一样的处理方式。

Launch

在 launch 中,异常一旦发生就会立马被抛出 。所以,你可使用 try/catch 包裹会发生异常的代码。以下所示:

scope.launch {
 try {  codeThatCanThrowExceptions()  } catch(e: Exception) {  // Handle exception  } } 复制代码

在 launch 中,异常一旦发生就会立马被抛出 。

Async

async 在根协程 (CoroutineScope 实例或者 supervisorJob 的直接子协程) 使用时,异常不会被自动抛出,而是直到你调用 .await() 时才抛出。

为了处理 async 抛出的异常,你能够在 try/catch 中调用 await

supervisorScope {
 val deferred = async {  codeThatCanThrowExceptions()  }  try {  deferred.await()  } catch(e: Exception) {  // Handle exception thrown in async  } } 复制代码

在上面的例子中,async 的调用处永远不会抛出异常,因此这里并不须要包裹 try/catchawait() 方法将会抛出 async 内部发生的异常。

注意上面的代码中咱们使用的是 supervisorScope 来调用 asyncawait 。就像以前说过的那样,SupervisorJob 让协程本身处理异常。与之相反的,Job 会传播异常,因此 catch 代码块不会被调用。

coroutineScope {
 try {  val deferred = async {  codeThatCanThrowExceptions()  }  deferred.await()  } catch(e: Exception) {  // Exception thrown in async WILL NOT be caught here   // but propagated up to the scope  } } 复制代码

此外,由其余协程建立的协程若是发生了异常,也将会自动传播,不管你的协程构建器是什么。

举个例子:

val scope = CoroutineScope(Job())
scope.launch {  async {  // If async throws, launch throws without calling .await()  } } 复制代码

在上面的例子中,若是 async 发生了异常,会当即被抛出。由于 scope 的直接子协程是由 scope.launch 启动的,async 继承了协程上下文中的 Job ,致使它会自动向父级传播异常。

⚠️ 经过 coroutineScope 构建器或者由其余协程启动的协程抛出的异常,不会被 try/catch 捕获!

SupervisorJob 那一节,咱们提到了 CoroutineExceptionHandler 。如今让咱们来深刻了解它。

CoroutineExceptionHandler

协程异常处理器 CoroutineExceptionHandler 是 CoroutineContext 中的一个可选元素,它能够帮助你 处理未捕获异常

下面的代码展现了如何定义一个 CoroutineExceptionHandler 。不管异常什么时候被捕获,你都会获得关于发生异常的 CoroutineContext 的信息,和异常自己的信息。

val handler = CoroutineExceptionHandler {
 context, exception -> println("Caught $exception") } 复制代码

若是知足如下要求,异常将会被捕获:

  • 什么时候⏰ :是被能够自动抛异常的协程抛出的( launch,而不是 async
  • 何地🌍 :在 CoroutineScope 或者根协程的协程上下文中( CoroutineScope 的直接子协程或者 supervisorScope

让咱们看两个 CoroutineExceptionHandler 的使用例子。

在下面的例子中,异常会被 handler 捕获:

val scope = CoroutineScope(Job())
scope.launch(handler) {  launch {  throw Exception("Failed coroutine")  } } 复制代码

下面的另外一个例子中,handler 在一个内部协程中使用,它不会捕获异常:

val scope = CoroutineScope(Job())
scope.launch {  launch(handler) {  throw Exception("Failed coroutine")  } } 复制代码

因为 handler 没有在正确的协程上下文中使用,因此异常没有被捕获。内部 launch 启动的协程一旦发生异常会自动传播到父协程,而父协程并不知道 handler 的存在,因此异常会被直接抛出。


即便你的应用由于异常没有按照预期执行,优雅的异常处理对于良好的用户体验也是很重要的。

当你要避免因异常自动传播形成的协程取消时,记住使用 SupervisorJob ,不然请使用 Job

未捕获异常将会被传播,捕获它们,提供良好的用户体验!


这篇文章就到这里了,这个系列还剩最后一篇了。

在以前提到协程的取消时,介绍了 viewModelScope 等跟随生命周期自动取消的协程做用域。可是不想取消时,应该怎么作?下一篇将会为你解答。

我是秉心说,关注我,不迷路!

本文使用 mdnice 排版

相关文章
相关标签/搜索