协程的几个验证程序

167 阅读4分钟

异常处理

正常情况下,父协程收到子协程的异常通知后,首先会取消所有其它子协程,取消自己,然后往上抛异常

下图为了方便,将 launch 与内部代码统一算作一个协程,实际上两者有区别,不应该算做一个,但不影响对问题的说明,特殊场所会单独说明。

【协一一】出现异常,同样会导致【协二】不执行。也就是说handler 并不能阻止异常的传播,它只是个处理程序。前者抛出异常时,不断往上传播,最终到达最外层 scope,而 scope 定义了 handle,所以该 handle 被触发。

bd3cf176-416b-4f0a-9ecf-9c36e58391f0.png

如果 scope 没有定义 handler 最终处理逻辑就是调用线程的 ExceptionHandler,android 中默认就是导致应用崩溃。

If the exception is not handled and the CoroutineContext doesn’t have a CoroutineExceptionHandler (as we’ll see later), it will reach the default thread’s ExceptionHandler. In the JVM, the exception will be logged to console; and in Android, it will make your app crash regardless of the Dispatcher this happens on.

同样的代码,只是向【协一一】添加 SupervisorJob,结果就由【协一一】中的 handle 处理。因为【协一一】在往上传播时,它的父协程(此处指 launch 参数构成的协程)使用了 SupervisorJob,它会将异常拦截下来同时交由【协一一】处理,而【协一一】又继承了它的 handle,所以会触发 handle 的执行

image.png

SupervisorJob 拦截异常原理

注意上面第二个分析时,说的是 由交【协一一】处理,而不是由自己的 handle 处理,这就是 SupervisorJob 的原理:拦截异常,并交由子协程处理。这个可以由 SupervisorJob 的源码实现看出

private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
    // 直接返回,异常不会往上传播,也不会引起兄弟协程的取消
    // 返回 false,说明交由它自己的子协程处理,而不是自己的 handle 处理
    override fun childCancelled(cause: Throwable): Boolean = false
}

也可通过代码验证一下,代码跟上面一样只不过通过反射在【协一一】内部修改 scope 对应的 CoroutineExceptionHandler

image.png

launch+SupervisorJob 捕获异常的原理

以下代码可以在 handler 中接收到抛出的异常信息,主要原因是 lambda 会形成单独协程,它的父类是 launch() 参数构成的另一个协程。

当异常发生时,lambda 会往上传递,被参数协程打回( SupervisorJob 的作用),由它自己处理。而它又继承了参数协程的 handler,所以最终异常交给了 handler 处理。

launch(SupervisorJob() + handler) {
    // lambda 表达式是一个单独的协程
    throw IllegalStateException()
}

为验证上面逻辑,可以修改 lambda 的逻辑,通过反射修改 CoroutineExceptionHandler

// lambda 表达式内部代码

// 使用 handler2 替换掉 coroutineContext 中旧的 handler
// 此时会生成一个新的 context,需要通过反射将该 context 设置给 Scope
val ctx = this.coroutineContext + handler2

// this 指的是 CoroutineScope,lambda 是它的一个扩展函数
updateCoroutineExceptionHandler(this, ctx)
throw IllegalStateException()

// updateCoroutineExceptionHandler() 通过反射修改

private fun updateCoroutineExceptionHandler(scope: CoroutineScope, context: CoroutineContext) {
    val coroutineContextField = scope::class.java.superclass.getDeclaredField("context")
    coroutineContextField.isAccessible = true
    coroutineContextField.set(scope, context)
}

launch 启动协程的父子关系

协程的父子关系实质是其关联的 Job 父子关系。而 launch 比较特殊,一次调用形成了两级 scope

image.png

知道了上面逻辑,下图输出的内容就很好理解

image.png

【first】抛出异常会传播到它父类【协一】,【协一】接着往上传播只不过它的父协程是 SupervisorJob,所以异常处理会被打回到由【协一】处理。而【协一】从其父协程继承 handle,所以会输出 first child。同样,由于【协一】是普通的 Job,在收到【first】的异常后会取消其余子协程,所以【second】无法执行到。对于【third】,由于【协一】的异常没有往外传递到 scope 中,也就影响不到它,所以它会正常执行。

这也就是下面这张图的道理

0_CB9c0_BAhlSJpC7w.png

async 异常逻辑

正常情况下,async 协程抛出的异常会在 await() 时重新抛出,可以被 try-catch 住。而且 async 协程异常也会往上传播,这就会取消一系列祖先协程。如下图,async 抛出异常会一直往上传播,直到遇到 SupervisorJob 被打回,交由继承到的 handle2 处理,同时会也导致第一个 launch 的子协程全部被取消,所以 【second】与 【third】都无法执行到,但这并不影响对 await 的调用,只不过调用时会重新抛出异常进面被 catch 住

也就是说 async 协程导致的异常可以被抓住两次,神奇。

image.png

handler 的触发时机

handle 一共有三种情况下会被触发:

  1. 作为顶层 scope 的 context。所有异常传播到顶层 scope 后,顶层 scope 就会调用自己的 handle 进行处理。要注意 coroutineScope() 生成的 scope 不是顶层 scope,它会与外界形成父子关系
  2. CoroutineScope 的直接子协程。如下,下面也说明了 handle 的两种触发时间
    • 定义在 launch 中才行,定义在 async 的不行

image.png

  1. SupervisorJob 的直接子协程。这个上面说过。

coroutineScope 与 supervisorScope

此两者并不会生成独立的 scope,它们生成的 scope 依旧是继承于父协程,通过反射拿到 job 对象进行验证。此两者主要用在处理一系列相关的子协程