Kotlin协程:如何优雅封装异常
前言
本文主要包括:
1.协程的3种作用域以及异常的传播方式
2.协程异常的两种捕获方式及对比
3.协程异常的优雅封装
协程的异常是怎么传播的?
首先了解下协程作用域
协程作用域分为顶级作用域,协同作用域与主从作用域,分别对应GlobalScope、coroutineScope、supervisorScope
作用分析:
说明:
• C2-1发生异常的时候,C2-1->C2->C2-2->C2->C1->C3(包括里面的子协程)->C4
• C3-1-1发生异常的时候,C3-1-1->C3-1-1-1,其他不受影响
• C3-1-1-1发生异常的时候,C3-1-1-1->C3-1-1,其他不受影响
举个例子
1、C1和C2没有关系
GlobalScope.launch { //协程C1
GlobalScope.launch {//协程C2
//...
}
}
C1,C2不会互相影响,完全独立
2.C2和C3是C1的子协程,C2和C3异常会取消C1
GlobalScope.launch { //协程C1
coroutineScoope {
launch{}//协程C2
launch{}//协程C3
}
}
3.C2和C3是C1的子协程,C2和C3异常不会取消C1
GlobalScope.launch { //协程C1
supervisorScope {
launch{}//协程C2
launch{}//协程C3
}
}
如何捕获异常
1.直接用Try,Catch会有什么问题?
在java与Kotlin中,我们一般直接try,catch捕获异常
fun main() {
val topLevelScope = CoroutineScope(Job())
topLevelScope.launch {
try {
throw RuntimeException("RuntimeException in coroutine")
} catch (exception: Exception) {
println("Handle $exception")
}
}
Thread.sleep(100)
}
// 输出
// Handle java.lang.RuntimeException: RuntimeException in coroutine
但是当我们在try模块中launch
一个新的协程时,会有一个意外的发现
fun main() {
val topLevelScope = CoroutineScope(Job())
topLevelScope.launch {
try {
launch {
throw RuntimeException("RuntimeException in nested coroutine")
}
} catch (exception: Exception) {
println("Handle $exception")
}
}
Thread.sleep(100)
}
你会发现捕获失效了,并且app crash了
我们发现try,catch无法catch住子协程的异常
发生了什么
在协程中未捕获的异常会发生什么呢?协程最创新的功能之一就是结构化并发。为了使结构化并发的所有功能成为可能,CoroutineScope的Job对象以及Coroutines和Child-Coroutines的Job对象形成了父子关系的层次结构。未传播的异常(而不是重新抛出)是“在工作层次结构中传播”。这种异常传播会导致父Job的失败,进而导致其子级所有Job的取消。
上面示例代码的job层次大概如下所示:
子协程的异常传播到协程(1)的Job,然后传播到topLevelScope(2)的Job。
传播的异常可以通过CoroutineExceptionHandler来捕获,如果没有设置,则将调用线程的未捕获异常处理程序,可能会导致退出应用
我们看出,协程有两种异常处理机制,这也是协程的异常处理比较复杂的原因
小结1
如果协程本身不使用try-catch子句自行处理异常,则不会重新抛出该异常,因此无法通过外部try-catch子句进行处理。
异常会在“Job层次结构中传播”,可以由已设置的CoroutineExceptionHandler处理。如果未设置,则调用该线程的未捕获异常处理程序。
2.CoroutineExceptionHandler
现在我们知道,如果我们在try块中launch失败的协程,try-catch是没有用的。
因此,我们需要配置一个CoroutineExceptionHandler,我们可以将context传递给启动协程生成器。
由于CoroutineExceptionHandler是一个ContextElement,因此我们可以通过在启动子协程时将其传递给launch:
fun main() {
val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
println("Handle $exception in CoroutineExceptionHandler")
}
val topLevelScope = CoroutineScope(Job())
topLevelScope.launch {
launch(coroutineExceptionHandler) {
throw RuntimeException("RuntimeException in nested coroutine")
}
}
Thread.sleep(100)
}
// 输出
// Exception in thread "DefaultDispatcher-worker-2" java.lang.RuntimeException: RuntimeException in nested coroutine
可以发现程序还是crash了
为什么不生效?
这是因为给子协程设置CoroutineExceptionHandler是没有效果的,我们必须给顶级协程设置,或者初始化Scope时设置才有效
// ...
val topLevelScope = CoroutineScope(Job() + coroutineExceptionHandler)
// ...
// ...
topLevelScope.launch(coroutineExceptionHandler) {
// ...
小结2
为了使CoroutineExceptionHandler起作用,必须将其设置在CoroutineScope或顶级协程中。
3.Try,Catch与CoroutineExceptionHandler对比
如上面介绍的,协程支持两种异常处理机制,那么我们应该选择哪种呢?
CoroutineExceptionHandler的官方文档提供了一些很好的答案:
“ CoroutineExceptionHandler是用于全局“全部捕获”行为的最后手段。您无法从CoroutineExceptionHandler中的异常中恢复。当调用处理程序时,协程已经完成,并带有相应的异常。通常,处理程序用于记录异常,显示某种错误消息,终止和/或重新启动应用程序。
如果需要在代码的特定部分处理异常,建议在协程内部的相应代码周围使用try / catch。这样,您可以防止协程异常完成(现在已捕获异常),重试该操作和/或采取其他任意操作:”
小结3
如果要在协程完成之前重试该操作或执行其他操作,请使用try / catch。
请记住,通过直接在协同程序中捕获异常,该异常不会在Job层次结构中传播,也不会利用结构化并发的取消功能。
而使用CoroutineExceptionHandler处理应该在协程完成后发生的逻辑。
可以看出,我们绝大多数时候应该使用CoroutineExceptionHandler
4.launch{} vs async{}
我们上面的例子都是使用launch启动协程的异常,但是launch与async的协常处理是完全不同的
下面看个例子
fun main() {
val topLevelScope = CoroutineScope(SupervisorJob())
topLevelScope.async {
throw RuntimeException("RuntimeException in async coroutine")
}
Thread.sleep(100)
}
// 没有输出
为什么这里不会抛出异常?
我们先要了解下launch与async的区别
从launch开始的协程的返回类型是Job,它只是协程的一种表示形式,没有返回值。
如果我们需要协程的某些结果,则必须使用async,它返回Deferred,这是一种特殊的Job,另外还保存一个结果值。如果异步协程失败,则将该异常封装在Deferred返回类型中,并在我们调用suspend函数.await()来检索其结果值时将其重新抛出。
因此,我们可以使用try-catch子句将对.await()的调用括起来。
fun main() {
val topLevelScope = CoroutineScope(SupervisorJob())
val deferredResult = topLevelScope.async {
throw RuntimeException("RuntimeException in async coroutine")
}
topLevelScope.launch {
try {
deferredResult.await()
} catch (exception: Exception) {
println("Handle $exception in try/catch")
}
}
Thread.sleep(100)
}
// 输出
// Handle java.lang.RuntimeException: RuntimeException in async coroutine in try/catch
注意:如果async协程是顶级协程,则会将异常封装在Deferred中,等待调用await才会抛出异常。
否则,该异常将立即传播到Job层次结构中,并由CoroutineExceptionHandler处理,甚至传递给线程的未捕获异常处理程序,即使不对其调用.await(),如以下示例所示:
fun main() {
val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
println("Handle $exception in CoroutineExceptionHandler")
}
val topLevelScope = CoroutineScope(SupervisorJob() + coroutineExceptionHandler)
topLevelScope.launch {
async {
throw RuntimeException("RuntimeException in async coroutine")
}
}
Thread.sleep(100)
}
// 输出
// Handle java.lang.RuntimeException: RuntimeException in async coroutine in CoroutineExceptionHandler
小结4
launch
和async
协程中未捕获的异常会立即在作业层次结构中传播。
但是,如果顶层Coroutine是从launch启动的,则异常将由CoroutineExceptionHandler处理或传递给线程的未捕获异常处理程序。
如果顶级协程以async方式启动,则异常封装在Deferred返回类型中,并在调用.await()时重新抛出。
5.coroutineScope异常处理特性
文章开头我们举了个例子,失败的协程将其异常传播到Job
层次结构中,而不是重新抛出该异常,因此,外部try-catch无效。
但是,当我们用coroutineScope {}
作用域函数将失败的协程包围起来时,会发生一些有趣的事情:
fun main() {
val topLevelScope = CoroutineScope(Job())
topLevelScope.launch {
try {
coroutineScope {
launch {
throw RuntimeException("RuntimeException in nested coroutine")
}
}
} catch (exception: Exception) {
println("Handle $exception in try/catch")
}
}
Thread.sleep(100)
}
// 输出
// Handle java.lang.RuntimeException: RuntimeException in nested coroutine in try/catch
现在,我们可以使用try-catch
子句处理异常。
可以看出,范围函数coroutineScope {}
重新抛出其失败子项的异常,而不是将其传播到Job层次结构中。
coroutineScope {}
主要用于suspend
函数中以实现“并行分解”。这些suspend
函数将重新抛出其失败的协程的异常,因此我们可以相应地设置异常处理逻辑。
5.小结5
范围函数coroutineScope {}
重新抛出其失败的子协程的异常,而不是将其传播到Job
层次结构中,这使我们能够使用try-catch
处理失败的协程的异常
6.supervisorScope异常处理特性
通过使用作用域函数supervisorScope {}
,我们将在Job层次结构中添加一个新的,独立的嵌套作用域,并将SupervisorJob
作为其Job
。
代码如下:
fun main() {
val topLevelScope = CoroutineScope(Job())
topLevelScope.launch {
val job1 = launch {
println("starting Coroutine 1")
}
supervisorScope {
val job2 = launch {
println("starting Coroutine 2")
}
val job3 = launch {
println("starting Coroutine 3")
}
}
}
Thread.sleep(100)
}
现在,在这里了解异常处理至关重要的一点是,supervisorScope是一个必须独立处理异常的新的独立子域。
它不会像coroutineScope那样重新抛出失败的协程的异常,也不会将异常传播到其父级– topLevelScope作业。
要理解的另一件至关重要的事情是,异常只会向上传播,直到它们到达顶级范围或SupervisorJob。这意味着job2和job3现在是顶级协程。
这也意味着我们可以为它们添加CoroutineExceptionHandler
fun main() {
val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, exception ->
println("Handle $exception in CoroutineExceptionHandler")
}
val topLevelScope = CoroutineScope(Job())
topLevelScope.launch {
val job1 = launch {
println("starting Coroutine 1")
}
supervisorScope {
val job2 = launch(coroutineExceptionHandler) {
println("starting Coroutine 2")
throw RuntimeException("Exception in Coroutine 2")
}
val job3 = launch {
println("starting Coroutine 3")
}
}
}
Thread.sleep(100)
}
// 输出
// starting Coroutine 1
// starting Coroutine 2
// Handle java.lang.RuntimeException: Exception in Coroutine 2 in CoroutineExceptionHandler
// starting Coroutine 3
直接在supervisorScope
中启动的协程是顶级协程,这也意味着async
协程现在将其异常封装在其Deferred
对象中,并且仅在调用.await()
时被重新抛出
这也是为什么viewModelScope
中的async
需要调用await
才会抛出异常的原因
小结6
范围函数supervisorScope {}
在Job
层次结构中添加了一个新的独立子范围,并将SupervisorJob
作为这个scope
的'job'。
这个新作用域不会在“Job层次结构”中传播其异常,因此它必须自行处理其异常。
直接从supervisorScope
启动的协程是顶级协程。
顶级协程与子协程在使用launch()
或async()
启动时的行为有所不同,此外,还可以在它们中安装CoroutineExceptionHandlers
。
协程异常处理封装
如上文所说,在大多数时候,CoroutineExceptionHandler是一个更好的选择
如我们所知,协程最大的优点是可以使用同步的方法写异步代码,CoroutineExceptionHandler有以下缺点
1.将异常处理代码与协程代码分隔开了,看上去不是同步代码
2.每次使用都要新建局部变量,不够优雅
我们可以对CoroutineExceptionHandler进行封装,利用kotlin扩展函数,实现类似RxJava的调用效果
最后调用效果如下
fun fetch() {
viewModelScope.rxLaunch<String> {
onRequest = {
//网络请求
resposity.getData()
}
onSuccess = {
//成功回调
}
onError = {
//失败回调
}
}
}
代码实现
主要利用kotlin扩展函数及DSL语法,封装协程异常处理,达到类似RxJava调用的效果
fun <T> CoroutineScope.rxLaunch(init: CoroutineBuilder<T>.() -> Unit) {
val result = CoroutineBuilder<T>().apply(init)
val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
result.onError?.invoke(exception)
}
launch(coroutineExceptionHandler) {
val res: T? = result.onRequest?.invoke()
res?.let {
result.onSuccess?.invoke(it)
}
}
}
class CoroutineBuilder<T> {
var onRequest: (suspend () -> T)? = null
var onSuccess: ((T) -> Unit)? = null
var onError: ((Throwable) -> Unit)? = null
}
如上即是一个简单封装,可实现上面演示的目标效果
将请示,成功,失败分类展示,结构更加清晰,同时不需要写CoroutineExceptionHandler局部变量,更为优雅简洁
投稿作者:程序员江同学
原文🔗:https://juejin.cn/post/6935472332735512606
「点击关注,Carson每天带你学习一个Android知识点。」
最后福利:学习资料赠送
福利:本人亲自整理的「Android学习资料」 数量:10名 参与方式:「点击右下角”在看“并回复截图到公众号,随机抽取」 点击就能升职、加薪水!