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

495 阅读6分钟

原文作者 :Manuel Vivo

原文地址: Exceptions in coroutines

译者 : 京平城

异常处理得当对提升用户体验是十分重要的,本文将阐述异常在coroutines是如何传播的以及各种处理方案。

A coroutine suddenly failed! What now? 😱

当一个coroutine抛出异常的时候,会传播给它的父coroutine!父coroutine会

  1. 取消剩余子coroutine的执行
  2. 取消自己的执行
  3. 继续传播异常给它的父coroutine

异常会一路传播到coroutines的根节点,并且所有该CoroutineScope启动的coroutines都会被取消。

0_UcEpsF2X-ihztU2Z.gif An exception in a coroutine will be propagated throughout the coroutines hierarchy

这种递归传播异常的行为在某些场景下是适用的,但不是所有场景。比如一个处理用户交互的CoroutineScope,当它的子coroutine抛出了异常会导致整个UI scope被取消,UI组件无响应。原因是一个取消的scope是无法再启动新的coroutines的。

如果你不希望上述递归传播异常的行为发生,可以试着使用SupervisorJob

SupervisorJob to the rescue

当使用SupervisorJob的时候,一个子coroutine执行失败了不会影响到其他的子coroutines。
一个SupervisorJob不会取消自己和自己的子coroutines,并且在SupervisorJob中异常不会被传播,而是让发生异常的子coroutine自己来处理。

使用val uiScope = CoroutineScope(SupervisorJob())来创建一个SupervisorJob

0_Mrf17HLbWQPfTt1I.png A SupervisorJob won’t cancel itself or the rest of its children

如果异常没有被处理或者没有在CoroutineContext中设置一个CoroutineExceptionHandler,那么这个异常将会被默认线程的ExceptionHandler处理。这意味着在JVM中,异常将会被打印到控制台;在Android中,你的App将会crash。

💥 Uncaught exceptions will always be thrown regardless of the kind of Job you use

同样的行为也适用于使用scope的build方法coroutineScopesupervisorScope。它们将创建一个子scope并且覆盖他们继承的coroutineContext里的Job属性。
译者注:原文是 The same behavior applies to the scope builders coroutineScope and supervisorScope. These will create a sub-scope (with a Job or a SupervisorJob accordingly as a parent) with which you can logically group coroutines (e.g. if you want to do parallel computations or you want them to be or not be affected by each other). 感觉比较难抓住原文作者想表达的意思。

警告:如果想让SupervisorJob如预期般的正常工作,必须使用supervisorScope方法或者CoroutineScope(SupervisorJob())方法。

Job or SupervisorJob? 🤔

我们应该什么时候使用Job,什么时候使用SupervisorJob呢?
当你不想coroutine的执行失败影响到它的父coroutine和其他子coroutines的时候,就可以使用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方法创建了一个子scope包含了一个SupervisorJob
那么如果child#1失败了,child#2是不会受到影响的。如果supervisorScope方法改成coroutineScope方法,那么情况就不一样了,如果child#1失败了,child#2也会被取消,并且整个scope都会被取消。

Watch out quiz! Who’s my parent? 🎯

下面例子中,你能猜得出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!希望你猜对了!第一眼看上去,可能会觉得child#1的父Job是SupervisorJob,但其实不是!因为scope.launch会创建一个新的Job并且会覆盖scope.launch中传入的SupervisorJob。实际上SupervisorJob在这个例子中什么作用都没有!

0_CB9c0_BAhlSJpC7w.png

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

因此不管是child#1还是child#2失败了,都会导致整个scope被取消。

Under the hood

如果读者对SupervisorJob的原理感兴趣的话,可以在JobSupport.kt找一下SupervisorJob的实现,childCancelled方法直接返回了false, 表明它既不会传播异常,也不会处理异常。

Dealing with Exceptions 👩‍🚒

Coroutines可以使用try/catch或者内建的helper方法runCatching(内部实现也是try/catch)处理异常。

我们之前说过未捕获的异常总是会被抛出。但是不同的coroutines builders处理异常的方式也不一样。

Launch

使用launch一旦异常发生立即会被抛出。
所以我们只需要在异常可能发生的代码处使用try/catch,见下面例子:

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

With launch, exceptions will be thrown as soon as they happen

Async

译者注:原文这一段写的有点混乱,我用自己比较容易理解的方式重写这一段的内容,不放心的读者可以直接去读原文。

使用async的时候,可以在await方法调用的时候进行try/catch

例子1:

val deferred = scope.async {
    throw IllegalArgumentException("This is a exception")
}
try {
    println("Deferred job is awaiting")
    deferred.await()
} catch (e: Exception) {
    println("Exception is caught")
}

// 依次输出:
Deferred job is awaiting
Exception is caught

处理async里抛出的异常,只需对await进行try/catch即可。

例子2:

supervisorScope {
    val deferred = async {
        throw IllegalArgumentException("This is a exception")
    }
    try {
        println("Deferred job is awaiting")
        deferred.await()
    } catch (e: Exception) {
        println("Exception is caught")
    }
}

// 依次输出:
Deferred job is awaiting
Exception is caught

和例子1的区别是最外面的scope换成了supervisorScope,都是对只需对await进行try/catch即可。
还有一点要注意的是,上述两个例子如果不调用await,即使有异常也是不会导致App crash的。

我们再来看一下另外两个例子:

例子3:

coroutineScope {
    val deferred = async {
        // If async throws, launch throws without calling .await()
        throw IllegalArgumentException("This is a exception")
    }

例子4:

val scope = CoroutineScope(Job())
scope.launch {
    async {
        // If async throws, launch throws without calling .await()
        throw IllegalArgumentException("This is a exception")
    }
}

例子3和4中,async既不是根coroutine(直接调用scope.async),也不是supervisorScope创建的直接子coroutine,即使不调用await,也会抛出异常并且导致App crash。

CoroutineExceptionHandler

在创建CoroutineContext的时候我们可以传入一个CoroutineExceptionHandler来处理异常。
下面代码创建了一个可以打印出异常信息的CoroutineExceptionHandler

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

要注意的是CoroutineExceptionHandler必须传递给正确的CoroutineContext,下面我们来看两个例子:

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

在这个例子中,异常会被CoroutineExceptionHandler处理,App不会crash。

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

在这个例子中,异常会被传到到launch(handler)的父coroutine,也就是scope.launch创建的coroutine,所以异常不会被CoroutineExceptionHandler处理,App会crash。

结束语:想要好的用户体检就必须做好coroutines的异常处理并根据自己的需求选择正确的Job类型(SupervisorJobJob)。