彻底掌握kotlin 协程异常处理

作者:Lstone7364 链接:https://juejin.cn/post/7270478168758829111

在学习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 类内部, 如果能理清楚下面的流程将会对我们理解协程异常处理流程很有帮助。

fba66978b89241173297877dfbd87819.jpeg

// 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 ,表示异常已被处理。

74cb551a7a7d631e39c316c8172005ce.jpeg

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()
    }
}

abfcb0146b771c00dcb38c11afa16623.jpeg

异常发生后,会在当前协程上下文内传递给父协程或者自己处理 , 父协程也是如此 。不同的协程对异常的处理方式不同,例如当一个协程的不对子协程做处理时,既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 为水平传递,我觉得这种说法不太合适, 从源码角度来看,异常一直都是垂直传递的,只不过过程中有可能会被拦截处理。

关注我获取更多知识或者投稿

a27679d85a1e4e920305dc8ac90a3e33.jpeg

b8d2917da7ec7311f20923ba72d0489c.jpeg

猜你喜欢

转载自blog.csdn.net/c6E5UlI1N/article/details/132550679
今日推荐