Kotlin Coroutines(协程) 完全解析系列:
Kotlin Coroutines(协程) 完全解析(一),协程简介
Kotlin Coroutines(协程) 完全解析(二),深入理解协程的挂起、恢复与调度
Kotlin Coroutines(协程) 完全解析(三),封装异步回调、协程间关系及协程的取消
Kotlin Coroutines(协程) 完全解析(四),协程的异常处理
Kotlin Coroutines(协程) 完全解析(五),协程的并发
本文基于 Kotlin v1.3.0-rc-146,Kotlin-Coroutines v1.0.0-RC1
在上一篇文章中提到子协程抛出未捕获的异常时默认会取消其父协程,而抛出CancellationException
却会当作正常的协程结束不会取消其父协程。本文来详细解析协程中的异常处理,抛出未捕获异常后协程结束后运行会不会崩溃,可以拦截协程的未捕获异常吗,如何让子协程的异常不影响父协程。
Kotlin 官网文档中有关于协程异常处理的文章,里面的内容本文就不再重复,所以读者们先阅读官方文档:
看完官方文档后,可能还是会有一些疑问:
launch
式协程的未捕获异常为什么会自动传播到父协程,为什么对异常只是在控制台打印而已?async
式协程的未捕获异常为什么需要依赖用户来最终消耗异常?自定义的
CoroutineExceptionHandler
的是如何生效的?异常的聚合是怎么处理的?
SupervisorJob
和supervisorScope
实现异常单向传播的原理是什么?
这些疑问在本文逐步解析协程中异常处理的流程时,会一一解答。
1. 协程中异常处理的流程
从抛出异常的地方开始跟踪协程中异常处理的流程,抛出异常时一般都在协程的运算逻辑中。而在第二篇深入理解协程的挂起、恢复与调度中提到在协程的三层包装中,运算逻辑在第二层BaseContinuationImpl
的resumeWith()
函数中的invokeSuspend
运行,所以再来看一次:
|
|
从上面源码的try {} catch {}
语句来看,首先协程运算过程中所有未捕获异常其实都会在第二层包装中被捕获,然后会通过AbstractCoroutine.resumeWith(Result.failure(exception))
进入到第三层包装中,所以协程的第三层包装不仅维护协程的状态,还处理协程运算中的未捕获异常。这在第三篇分析子协程抛出未捕获异常,默认情况会取消其父线程时也提到过。
继续跟踪 AbstractCoroutine.resumeWith(Result.failure(exception)) -> JobSupport.makeCompletingOnce(CompletedExceptionally(exception), defaultResumeMode) -> JobSupport.tryMakeCompleting(state, CompletedExceptionally(exception), defaultResumeMode),在最后tryMakeCompleting()
过程中部分关键代码:
|
|
先看notifyCancelling(state.list, exception)
函数:
|
|
所以出现未捕获异常时,首先会取消所有子协程,然后可能会取消父协程。而有些情况下并不会取消父协程,一是当异常属于 CancellationException 时,而是使用SupervisorJob
和supervisorScope
时,子协程出现未捕获异常时也不会影响父协程,它们的原理是重写 childCancelled() 为override fun childCancelled(cause: Throwable): Boolean = false
。
launch
式协程和async
式协程都会自动向上传播异常,取消父协程。
接下来再看tryFinalizeFinishingState
的实现:
|
|
上面代码中if (finalException != null && !cancelParent(finalException))
语句可以看出,除非是 SupervisorJob 和 supervisorScope,一般协程出现未捕获异常时,不仅会取消父协程,一步步取消到最根部的协程,而且最后还由最根部的协程(Root Coroutine)处理协程。下面继续看处理异常的handleJobException
的实现:
|
|
默认的handleJobException
的实现为空,所以如果 Root Coroutine 为async
式协程,不会有任何异常打印操作,也不会 crash,但是为launch
式协程或者actor
式协程的话,会调用handleExceptionViaHandler()
处理异常。
下面接着看handleExceptionViaHandler()
的实现:
|
|
所以默认情况下,launch
式协程对未捕获的异常只是打印异常堆栈信息,如果在 Android 中还会调用uncaughtExceptionPreHandler
处理异常。但是如果使用了 CoroutineExceptionHandler 的话,只会使用自定义的 CoroutineExceptionHandler 处理异常。
到这里协程的异常处理流程就走完了,但是还有一个问题还没解答,async
式协程的未捕获异常只会导致取消自己和取消父协程,又是如何依赖用户来最终消耗异常呢?
|
|
看看反编译的 class 文件就明白了:
|
|
所以async
式协程只有通过await()
将异常重新抛出,不过可以可以通过try { deffered.await() } catch () { ... }
来捕获异常。
2. 小结
分析完协程的异常处理流程,其中需要注意的问题有下面这些:
抛出 CancellationException 或者调用
cancel()
只会取消当前协程和子协程,不会取消父协程,也不会其他例如打印堆栈信息等的异常处理操作。抛出未捕获的非 CancellationException 异常会取消子协程和自己,也会取消父协程,一直取消 root 协程,异常也会由 root 协程处理。
如果使用了 SupervisorJob 或 supervisorScope,子协程抛出未捕获的非 CancellationException 异常不会取消父协程,异常也会由子协程自己处理。
launch
式协程和actor
式协程默认处理异常的方式只是打印堆栈信息,可以自定义 CoroutineExceptionHandler 来处理异常。async
式协程本身不会处理异常,自定义 CoroutineExceptionHandler 也无效,但是会在await()
恢复调用者协程时重新抛出异常。