在学习Kotlin协程反常处理的时分,官方文档推荐的计划有两种 :

1.在协程体内运用 try { .. } catch(e: Exception) {...}

launch {
    try {
        // 协程代码
    } catch (e: Exception) {
    // 反常处理
    }
}

2. 运用CoroutineExceptionHandler来界说协程的反常处理器。当协程中产生反常时,它会被传递给handler函数进行处理。

val handler = CoroutineExceptionHandler { _, exception ->
    // 反常处理
}
val scope = CoroutineScope(Dispatchers.IO + handler)
scope.launch {
    // 协程代码
}

协程反常处理测验

那么下面的对协程的一些测验case,那些以反常会被成功catch ,那些会呈现crash ?

// 不会呈现crash
private fun test01() {
    val async = lifecycleScope.async {
        throw RuntimeException()
    }
}
// 不会呈现crash ,反常会被成功catch并打印
private fun test02() {
    val async = lifecycleScope.async {
        throw RuntimeException()
    }
    lifecycleScope.launch {
        try {
            async.await()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}
// 不呈现crash
private fun test03() {
    lifecycleScope.async {
        async {
            throw RuntimeException()
        }
    }
}
// 呈现crash
private fun test04() {
    lifecycleScope.launch {
        async {
            throw RuntimeException()
        }
    }
}
// 不会呈现crash
private fun test05() {
    lifecycleScope.async {
        launch {
            throw RuntimeException()
        }
    }
}
// 呈现crash
private fun test06() {
    lifecycleScope.launch {
        val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
            Log.d(TAG, "test02: ${throwable.message}")
        }
        launch(coroutineExceptionHandler) {
            throw Exception("test02")
        }
    }
}
// 不会呈现 crash , 反常被 coroutineExceptionHandler 处理
private fun test07() {
    lifecycleScope.launch {
        val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
            Log.d(TAG, "test02: ${throwable.message}")
        }
        launch(SupervisorJob() + coroutineExceptionHandler) {
            throw Exception("test02")
        }
    }
}
// 不会crash , 可是 coroutineExceptionHandler 未输出反常信息
private fun test08() {
    lifecycleScope.launch {
        val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
            Log.d(TAG, "test02: ${throwable.message}")
        }
        async(SupervisorJob() + coroutineExceptionHandler) {
            throw Exception("test02")
        }
    }
}

场景源码剖析

通过上面的示例后,能够看到协程的反常处理并没有这么简单,可是假如咱们了解源码今后,就会清晰很多。通过检查源码,当协程内一个反常产生时 , 最终会走到 JobSupport 类内部, 假如能理清楚下面的流程将会对咱们了解协程反常处理流程很有帮助。

彻底掌握kotlin 协程异常处理


// JobSupport 类
private fun finalizeFinishingState(state: Finishing, proposedUpdate: Any?): Any? {
    ...
    // Now handle the final exception
    if (finalException != null) {
        val handled = cancelParent(finalException) || handleJobException(finalException)
        if (handled) (finalState as CompletedExceptionally).makeHandled()
    }
    ...
}
private fun cancelParent(cause: Throwable): Boolean {
    val isCancellation = cause is CancellationException
    val parent = parentHandle
    // No parent -- ignore CE, report other exceptions.
    if (parent === null || parent === NonDisposableHandle) {
        return isCancellation
    }
    // Notify parent but don't forget to check cancellation
    return parent.childCancelled(cause) || isCancellation
}
protected open fun handleJobException(exception: Throwable): Boolean = false

isCancellation = false,cancelParent 办法内的 parent.childCancelled(cause) 办法回来为true 能够了解为父协程能够接收或许处理子协程抛出的反常,false表示父协程不处理子协程相关反常 。 parent 的一般为 ChildHandleNode,ChildHandleNode.cancelParent(finalException) 的最终调用完成为

internal class ChildHandleNode(
    @JvmField val childJob: ChildJob
) : JobCancellingNode(), ChildHandle {
    override val parent: Job get() = job
    override fun invoke(cause: Throwable?) = childJob.parentCancelled(job)
    override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
}

job为创立协程时launch(coroutineContext)设置coroutineContext参数的coroutineContext[Job]目标,一般为父协程目标。

假如cancelParent 回来为false,则尝试调用 handleJobException 办法 。 之前了解到 launch {} 发动一个协程实践上是创立了一个 StandaloneCoroutine 目标 , 而 StandaloneCoroutineJobSupport 之类,也就是说最终会履行 StandaloneCoroutine类的handleJobException 办法, 通过检查源码发现,StandaloneCoroutine 完成了handleJobException 且回来值固定为true ,表示反常已被处理。

彻底掌握kotlin 协程异常处理

private open class StandaloneCoroutine(
    parentContext: CoroutineContext,
    active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) {
    override fun handleJobException(exception: Throwable): Boolean {
        handleCoroutineException(context, exception)
        return true
    }
}

检查 handleCoroutineException 具体完成 , context[CoroutineExceptionHandler] 假如不为空,将会调用当时协程设置 CoroutineExceptionHandler 进行反常传递和处理

public fun handleCoroutineException(context: CoroutineContext, exception: Throwable) {
    // Invoke an exception handler from the context if present
    try {
        context[CoroutineExceptionHandler]?.let {
            it.handleException(context, exception)
            return
        }
    } catch (t: Throwable) {
        handleCoroutineExceptionImpl(context, handlerException(exception, t))
        return
    }
    // If a handler is not present in the context or an exception was thrown, fallback to the global handler
    handleCoroutineExceptionImpl(context, exception)
}

在之前的学习资料中,咱们学习到 CoroutineExceptionHandler 只需被设置到根协程才会有用 ,实践情况真的是这样吗? 通过检查源码发现, 只需保证 cancelParent(finalException) 回来值为false 和协程 handleJobException 办法完成内调用协程设置的 CoroutineExceptionHandler, 即可满意子协程也能够运用CoroutineExceptionHandler. 咱们能够自界说job完成反常的处理,伪代码如下 :

val job = object: JobSupport {
    override fun childCancelled(cause: Throwable): Boolean {
        return false
    }
}
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
    Log.d(TAG, "onCreate: ${throwable.message}")
}
lifecycleScope.launch(job + exceptionHandler) { 
    throw Exception("test")
}

cancelParent 办法最终调用到 jobchildCancelled 办法,childCancelled 回来 false,最终则会履行协程的handleJobException办法做反常处理。 官方也供给了一个类似的完成类SupervisorJobImpl

public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)
private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
    override fun childCancelled(cause: Throwable): Boolean = false
}

看到 SupervisorJob有没有很了解,本来它仅仅一个函数 ,真正的完成是SupervisorJobImpl,既运用 SupervisorJob 也能够到达类似效果

private fun test08() {
   lifecycleScope.launch {
        launch {
            val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
                Log.d(TAG, "test08: ${throwable.message}")
            }
            launch(SupervisorJob() + coroutineExceptionHandler) {
                throw Exception("test08")
            }
        }
    }
}

假如咱们给launch 指定了一个SupervisorJob() ,能够成功catch住了反常,可是现已违背了协程结构化并发原则 。 因为指定SupervisorJob()现已脱离了当时上下文协程作用域, 既当时页面的封闭时,该协程并不会随着页面封闭而撤销。正确做法如下:

private fun test09() {
    lifecycleScope.launch {
        val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
            Log.d(TAG, "test09: ${throwable.message}")
        }
        async(SupervisorJob(coroutineContext[Job]) + coroutineExceptionHandler) {
            throw Exception("test09")
        }
    }
}

挂起办法

挂起办法对反常的特别处理

挂起办法会对反常进行一次catch,并在协程的 afterResume() 内进行重抛。

private suspend fun testTrySuspend() {
    try {
        // 只需时
        trySuspend()
    } catch (e: Exception) { }
}
private suspend fun trySuspend() {
    // 办法1 抛出反常
    // throw RuntimeException()
    // 办法2 切换协程
    //withContext(Dispatchers.IO) {
    //    throw RuntimeException()
    //}
    // 办法3 调用其他挂起办法
    // invokeOtherSuspend()
}

咱们以withContext()为例 ,会开启一个新的 UndispatchedCoroutine 协程,该类继承于 ScopeCoroutine类,该类中有如下代码很要害

final override val isScopedCoroutine: Boolean get() = true

变量在以下当地被运用

 private fun cancelParent(cause: Throwable): Boolean {
    // 注释也做了说明
    // Is scoped coroutine -- don't propagate, will be rethrown
    if (isScopedCoroutine) return true
    /* CancellationException is considered "normal" and parent usually is not cancelled when child produces it.
     * This allow parent to cancel its children (normally) without being cancelled itself, unless
     * child crashes and produce some other exception during its completion.
    */
    val isCancellation = cause is CancellationException
    val parent = parentHandle
    // No parent -- ignore CE, report other exceptions.
    if (parent === null || parent === NonDisposableHandle) {
        return isCancellation
    }
    // Notify parent but don't forget to check cancellation
    return parent.childCancelled(cause) || isCancellation
}

看到这儿是不是很了解, 当反常产生的时分,就会调用该办法, 能够看到当isScopedCoroutine == true时,直接回来了true , 即表示当时协程无需处理反常 , 既反常会被挂起办法阻拦。

挂起办法对反常的重抛
 public final override fun resumeWith(result: Result<T>) {
    val state = makeCompletingOnce(result.toState())
    if (state === COMPLETING_WAITING_CHILDREN) return
    afterResume(state)
}
override fun afterResume(state: Any?) {
    threadStateToRecover.get()?.let { (ctx, value) ->
        restoreThreadContext(ctx, value)
        threadStateToRecover.set(null)
    }
    // resume undispatched -- update context but stay on the same dispatcher
    val result = recoverResult(state, uCont)
    withContinuationContext(uCont, null) {
        // 反常重抛
        uCont.resumeWith(result)
    }
}

通过源码能够看到挂起办法会对反常进行阻拦之后进行,再次将反常信息回来给父协程 。 假如对协程的 CPS 转化了解的话会知道,kotlin 协程是根据状况机机制完成的,即编译时进行CPS转化,这个转化是无感知的。如下代码:


private suspend fun testSuspend() {
    try {
        sum()
    } catch (e: Exception){ }
}
private suspend fun sum(): Int {
    return 0
}

因为挂起办法会对反常进行捕捉,并回来给父协程,编译后实践运行代码了解为如下代码

private suspend fun testSuspend() {
    try {
        val result = sum()
        if (result is Exception) {
             throw e
        }
        next step
    } catch (e: Exception){ }
}

反编译后实践源码流程也是如此 :

  1. 挂起办法通过cps 转化后,回来值类型被修改为Object 类型
  2. 挂起办法被编译器增加了一个 Continuation 类型参数,用于挂起办法成果回来和状况机状况切换(引发挂起点持续履行下一步)
  3. 挂起办法履行完毕后,履行到 case 1: 代码处,进行成果result值检查,假如resultException 类型,进行反常重抛

raw.githubusercontent.com/eric-lian/b…

总结

假如对一个挂起办法进行try ... catch 反常处理,只需反常类型精确,一定能够成功的捕捉反常。

为什么 catch 不住反常

private fun test10() {
    try {
        lifecycleScope.launch {
            // 1. 反常抛出
            // 2. JobSupport 履行 parent.childCancel(e) ||  handleException()
            // 3. 父job不处理, 履行 StandaloneCoroutine.handleException()
            throw RuntimeException()
        }
    } catch (e: Exception) {}
}
private fun test11() {
    try {
        lifecycleScope.async {
            // 1. 反常抛出
            // 2. JobSupport 履行 parent.childCancel(e) ||  handleException()
            // 3. 父job不处理, 履行 DeferredCoroutine.handleException() . 默认未处理
            // 反常 ,  忽略反常。
            throw RuntimeException()
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

彻底掌握kotlin 协程异常处理

反常产生后,会在当时协程上下文内传递给父协程或许自己处理 , 父协程也是如此 。 不同的协程对反常的处理办法不同,例如当一个协程的不对子协程做处理时,既cancelParent() 办法回来false, 子协程launch 发动协程的StandCoroutine.handleException()内的处理办法 会直接杀死应用程序。 而 async 发动的协程DeferredCoroutine内则是把当时反常缓存起来,DeferredCoroutine.handleException() 不会当即处理,当调用 async.await() 获取值的时分进行重抛。

弥补

最近碰到了一些场景下协程反常问题,在此弥补一下 ,总结如下 :

    private fun testCoroutineMultiThrows() {
        lifecycleScope.launch {
            try {
                test()
            } catch (e: Exception) {
                println("==== test exception")
            }
        }
    }
    private suspend fun test() {
        suspendCancellableCoroutine<Unit> {
            // 连续两次调用 resumeWithException 也只会捕捉到一次反常 
            //,这个和 throws 是一个道理,抛出一个反常后,后续代码不再履行
            // 假如调用方不进行 catch 则会crash
            //it.resumeWithException(Exception("test exception"))
            //it.resumeWithException(Exception("test exception"))
            // 抛出 kotlinx.coroutines.CancellationException 
            // 假如增加了try catch 则能够catch到该反常,
            // 不增加try catch 程序也不会溃散,协程框架会忽略该类型反常处理
            //it.cancel(CancellationException("test exception"))
            // 同上
            //throw CancellationException("test exception")
            // 不增加try catch 会导致应用crash
            //throw Exception("test exception")
        }
总结
  • 因此一个协程能否运用自界说的CoroutineExceptionHandler要害点在于,父协程是否能够处理子协程抛出的反常以及协程本身handleJobException办法能否处理该反常。
  • 假如想要判断当时try … catch 能否收效 ,只需要看当时或许抛出反常的当地是不是挂起办法即可,假如是挂起办法,则一定能够catch
  • 看一些文章的时分说 launch 反常为笔直传递, async 为水平传递,我觉得这种说法不太适宜, 从源码角度来看,反常一向都是笔直传递的,只不过进程中有或许会被阻拦处理。

参考

Kotlin | 关于协程反常处理,你想知道的都在这儿