Cursor blinking

Kotlin 协程异常机制

Android 基础|协程|字数 787|阅读时长≈ 2 分钟
Kotlin 协程异常机制

异常的传播

由于 CoroutineScope 可以创建协程,并且您可以在协程内创建更多协程,因此会创建隐式任务层次结构。在任务层次结构中,每个协程都有一个父级,可以是 CoroutineScope 或另一个协程。新生成的协程的 CoroutineContext 与父级的 CoroutineContext 有继承关系。每个任务 Job 都有自己的生命周期,子任务的生命周期受父任务的生命周期控制,比如如果父级 job 关闭,子协程 job 也会被取消。

Code
// jobA.cancel() 不影响 jobB 的执行,而 scope.cancel() jobA,jobB 都会被取消。  val scope = CoroutineScope(Job())        val jobA = scope.launch(CoroutineName("A")) {            println("Name:${this.coroutineContext[CoroutineName]} Job:${this.coroutineContext[Job]}#${this.coroutineContext[Job]?.parent?.javaClass?.name} start")            delay(100)            println("${this.coroutineContext[CoroutineName]} end")        }        val jobB = scope.launch(CoroutineName("B")) {            println("Name:${this.coroutineContext[CoroutineName]} Job:${this.coroutineContext[Job]}#${this.coroutineContext[Job]?.parent?.javaClass?.name} start")            delay(500)            println("${this.coroutineContext[CoroutineName]} end")        }//        jobA.cancel()//        scope.cancel()        // 保证执行完后再退出        Thread.sleep(5000)        println("......")

当协程因异常而失败时,它会优先将异常传播到其父级!然后,父级将 1)取消其其余子级,2) 取消自身,以及 3) 将异常传播到其父级。异常将到达层次结构的根部,并且 CoroutineScope 启动的所有协程也将被取消。

0_UcEpsF2X-ihztU2Z (2).gif
0_UcEpsF2X-ihztU2Z (2).gif

CoroutineExceptionHandler

其是用于在协程中全局捕获异常行为的最后一种机制,你可以理解为,类似 Thread.uncaughtExceptionHandler 一样。

但需要注意的是,CoroutineExceptionHandler 仅在未捕获的异常上调用,也即这个异常没有任何方式处理时(比如在源头tryCatch了),由于协程是结构化的,当子协程发生异常时,它会优先将异常委托给父协程区处理,以此类推 直到根协程作用域或者顶级协程 。因此其永远不会使用我们子协程 CoroutineContext 传递的 CoroutineExceptionHandler(SupervisorJob 除外),对于 async 这种,而是直接向用户直接暴漏该异常,所以我们在具体调用处直接处理就行。

Code
 	     // 2. 设置 CoroutineExceptionHandler 给 A 父级协程        val handler = CoroutineExceptionHandler { _, exception ->            println("CoroutineExceptionHandler got $exception")        }        // 3. 传递给默认线程的 ExceptionHandler        Thread.setDefaultUncaughtExceptionHandler(object : Thread.UncaughtExceptionHandler {            override fun uncaughtException(t: Thread, e: Throwable) {                println("t.name=${t.name}" + " error:" + e.toString())            }        })         val scope = CoroutineScope(Job())        val jobA = scope.launch(CoroutineName("A")) {            println("Name:${this.coroutineContext[CoroutineName]} Job:${this.coroutineContext[Job]}#${this.coroutineContext[Job]?.parent?.javaClass?.name} start")            delay(100)            //            1. 主动处理异常            //            try {            //                throw NullPointerException()            //            } catch (e: Exception) {            //                e.printStackTrace()            //            }            throw NullPointerException()            println("${this.coroutineContext[CoroutineName]} end")        }        val jobB = scope.launch(CoroutineName("B")) {            println("Name:${this.coroutineContext[CoroutineName]} Job:${this.coroutineContext[Job]}#${this.coroutineContext[Job]?.parent?.javaClass?.name} start")            delay(500)            println("${this.coroutineContext[CoroutineName]} end")        }        // 保证执行完后再退出        Thread.sleep(5000)        println("......")

如果异常没有被处理,而且顶级协程 CoroutineContext 中没有携带 CoroutineExceptionHandler ,则异常会传递给默认线程的 ExceptionHandler 。在 Android 中,如果没有设置 Thread.setDefaultUncaughtExceptionHandler , 这个异常将立即被抛出,从而导致引发App崩溃。

Launch vs Async

未捕获的异常总是会被抛出。然而,不同的协程构建器以不同的方式处理异常。

  • Launch

启动后,异常一发生就会被抛出。因此,您可以将可以抛出异常的代码包装在 try/catch 中,如下例所示

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

当 async 用作根协程(协程是 CoroutineScope 实例或supervisorScope 的直接子级)时,不会自动抛出异常,而是在调用 .await() 时抛出异常。 要处理异步抛出的异常(只要它是根协程),您可以将 .await() 调用包装在 try/catch 中:

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

SupervisorJob

使用 SupervisorJob,一个孩子的失败不会影响其他孩子。 SupervisorJob 不会取消自身或其其余子级。此外,SupervisorJob 也不会传播异常,而是让子协程处理它。

Code
// 使用 SupervisorJob,A 协程异常不会影响到 B 协程 val scope = CoroutineScope(SupervisorJob())        val jobA = scope.launch(CoroutineName("A")) {            println("A start")            delay(100)            throw NullPointerException()            println("A end")        }        val jobB = scope.launch(CoroutineName("B")) {            println("B start")            delay(500)            println("B end")        }        // 保证执行完后再退出        Thread.sleep(5000)        println("......")

与 SupervisorJob 不同,Job 会自动在层次结构中向上传播,因此下面的示例不会调用 catch 块:

Code
// job 在层次结构中优先向上传播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    }}

参考文档

https://book.kotlincn.net/text/exception-handling.html

https://medium.com/androiddevelopers/exceptions-in-coroutines-ce8da1ec060c