协程异常处理(二)

总结

协程中发生的异常都会先沿着 job 体系往上传播,不过不同的 job 有不同的处理逻辑,一般有三种:

  1. 继续往上传递:launch 与 async 属于此咱类型,因此在 launch/async 外面使用 try-catch 无法捕获任何异常。原因是协程内部会 try-catch 住代码中的异常,然后将异常当作参数沿 job 往上传递

    • async 的异常处理方式,跟它们是否是顶层协程有关,具体见示例
  2. 自己处理:supervisorScope 属于此种类型,其收到异常后交由子协程处理,不会往上传递给自己的父协程。可以简单理解为supervisorScope 下的 job 是一个独立的子体系,出异常时完全内部消化

  3. 重新抛出:coroutineScope 属于此种类型,所以可以在 coroutineScope 外面使用 try-catch 捕获其内部所有协程抛出的异常

coroutineScope

其后代协程抛出的异常,都可以在外层使用 try-catch 捕获。如下

kotlin 复制代码
fun test() {
    viewModelScope.launch {
        launch {
            e("launch1")
            try {
                coroutineScope {
                    e("scope1")
                    launch {
                        e("launch2")
                        launch {
                            e("launch3")
                            throw IllegalStateException()
                        }
                    }
                }
            } catch (e:Exception){
                // 可以捕获 launch3 抛出的异常
                e("catch $e")
            }
        }
    }
}
// output
launch1
scope1
launch2
launch3
catch java.lang.IllegalStateException

supervisorScope

阻止异常沿着 Job 体系往上传播,将异常信息交由子协程处理。如下,supervisorScope 后代协程抛出异常会一直传递到 supervisorScope,它会直接交由自己的子协程 launch2 处理,launch2 配置有 handler,所以最终由 handler2 处理。

要注意:supervisorScope 只保存其直接子协程不相互影响,但不能保存所有后代子协程在有异常发生时都不会被取消。如下 launch2 的所有子协程都被取消,但 launch2 的兄弟协程 launch21 并没有。

scss 复制代码
fun test() {
    viewModelScope.launch {
        launch {
            e("launch1")
            try {
                supervisorScope {
                    e("scope1")
                    launch(handler) {
                        e("launch2")
                        launch {
                            e("launch3")
                            launch {
                                e("launch4")
                                throw IllegalStateException()
                            }
                        }
                    }
                    launch {
                        delay(100)
                        e("launch21")
                    }
                }
            } catch (e: Exception) {
                e("catch $e")
            }
        }
    }
}
// output
launch1
scope1
launch2
launch3
launch4
handle java.lang.IllegalStateException
launch21

launch

launch 对异常的处理并没有任何特殊之处,就是往上传播

async

async 对异常的处理分两种情况:

  1. async 启动的是最顶层协程,则会在 await() 中重新抛出,可使用 try-catch 进行捕获
  2. 非顶层协程,会在 await 中抛出(可使用 try-catch 捕获),同时异常会继续往上抛,如果没有对异常进行处理,也会发生崩溃。如下所示,会 catch 住异常,但应用一样会崩溃
kotlin 复制代码
viewModelScope.launch {
    val job = async {
        launch {
            e("launch1")
            throw IllegalStateException()
        }
    }
    try {
        job.await()
    } catch (e: Exception) {
        // 此处会有异常输出,但应用还是会发生 crash,并不能阻止应用崩溃
        e(e.toString())
    }
}

SupervisorJob 与 CoroutineExceptionHandler

子协程会从父协程继承除 job 外的所有 element,包括 excepthonHandler。因此在任何地方添加 handler,其子协程都会拥有 handler,这是理解 exceptionHandler 发生作用的关键。

另外,如果异常传播到最顶层,且最顶层有 exceptionHandler,则异常会交由 handler 处理。

SupervisorJob 的唯一作用就是阻断异常向上传播,让子协程自己处理异常。如果子协程有 exceptionHander,异常就会被交由 exceptionHandler 处理。

如下面代码 scope 中并未定义 handler,但 launch2 中定义有,所以 launch2 的子协程(包括 luanch3) 都会具有 exceptionHandler。在 launch3 使用 SupervisorJob 时后,launch3 的子协程抛出的异常都会被 handler 处理 ------ launch3 继承了 launch2 的 handler

kotlin 复制代码
scope.launch {
    launch(h1) {
        val job = SupervisorJob()
        e("launch2")
        launch(job) {
            e("launch3")
            launch {
                throw IllegalArgumentException()
            }
        }
    }
}

Job 与 CoroutineExceptionHandler

如果在协程中直接 new Job() 并当作参数传递给协程,则该协程及其子协程就是一个完全独立的协程体系,并不受原来的协程体系管理 ------ 也就是说即使旧协程已经取消了,新协程也不会取消,只不过新协程一样会从原来的协程中继承 handler

kotlin 复制代码
val j = viewModelScope.launch {
    launch(handler) {
        val job = Job()
        e("launch2")
        // 使用新 job 构建协程,则新协程是一个完全独立的体系
        // 当 j 取消时,并没有影响到该体系
        // 只不过它依旧继承了 handler,所以发生异常时会触发 handler
        launch(job) {
            e("launch3")
            delay(1000)
            e("launch4")
            launch {
                throw IllegalArgumentException()
            }
        }
    }
    launch {
        e("launch5")
        delay(400)
        e("launch6")
    }
}
Thread.sleep(10)
j.cancel()

// output
launch2
launch5
launch3
launch4
handle java.lang.IllegalArgumentException

Job 与 SupervisorJob

从异常处理来看,Job 与 SupervisorJob 并没有太大区别:都会新成新的独立的协程体系,异常传递到它们后都会调用 handler 对异常进行处理。两者的最大区别在于:子协程是否会被取消

如下 Scope 使用 SupervisorJob 会输出 launch5,使用 Job 则不会输出,这就是 SupervisorJob 的作用:阻止直接子协程被取消

kotlin 复制代码
scope.launch {
    launch {
        delay(10)
        e("launch2 ${System.identityHashCode(handler)}")
        throw IllegalStateException()
    }
    launch {
        e("launch3 ${System.identityHashCode(handler)}")
        delay(1000)
        e("launch4 ${System.identityHashCode(handler)}")
    }
}

scope.launch {
    delay(100)
    e("launch5 ${System.identityHashCode(handler)}")
}

下面是一个易错点:launch 中的 Job 的直接子 Job 是 lambda 对应的 Job,并不是 launch2 对应的 Job

kotlin 复制代码
// launch 中使用 Job 与 SupervisorJob 效果完全一样,launch4 都不会输出
launch(SupervisorJob()) {
    launch {
        delay(10)
        e("launch2")
        throw IllegalStateException()
    }
    launch {
        e("launch3")
        delay(1000)
        e("launch4")
    }
}

// output
launch3
launch2
相关推荐
QING6189 分钟前
Android Executor 与 Executors 详解 —— 新手指南
android·ai编程·trae
QING61824 分钟前
Android跨进程通信中的关键字详解:in、out、inout、oneway
android·ai编程·trae
_小马快跑_1 小时前
Android Xfermode应用:实现炫酷刮刮卡效果
android
_小马快跑_10 小时前
Android | 利用ItemDecoration绘制RecyclerView分割线
android
_小马快跑_10 小时前
别再手写 if/else 判断了!赶紧来掌握 Kotlin 的 coerce 三兄弟吧
android
_小马快跑_10 小时前
Android Xfermode应用:实现圆角矩形、圆形等图片裁切
android
怀旧,10 小时前
【数据结构】4.单链表实现通讯录
android·服务器·数据结构
yechaoa11 小时前
Widget开发实践指南
android·前端
顾林海12 小时前
Flutter 图标和按钮组件
android·开发语言·前端·flutter·面试