原文作者 :Manuel Vivo
原文地址: Exceptions in coroutines
译者 : 京平城
异常处理得当对提升用户体验是十分重要的,本文将阐述异常在coroutines是如何传播的以及各种处理方案。
A coroutine suddenly failed! What now? 😱
当一个coroutine抛出异常的时候,会传播给它的父coroutine!父coroutine会
- 取消剩余子coroutine的执行
- 取消自己的执行
- 继续传播异常给它的父coroutine
异常会一路传播到coroutines的根节点,并且所有该CoroutineScope启动的coroutines都会被取消。
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
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方法coroutineScope和supervisorScope。它们将创建一个子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失败了,是不会影响到整个scope和child#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在这个例子中什么作用都没有!
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类型(SupervisorJob或Job)。