前言
我们通过上一篇文章知道,在 Android 中,假如线程发生了异常,会导致 App 直接退出,那我们来看看,协程发生异常后,现象会怎么样。
示例代码:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch {
throw NullPointerException()
}
}
日志输出:
2022-05-09 10:48:34.529 3946-3978/com.bjsdm.testkotlin E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
Process: com.bjsdm.testkotlin, PID: 3946
java.lang.NullPointerException
at com.bjsdm.testkotlin.MainActivity$onCreate$1.invokeSuspend(MainActivity.kt:17)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
em…也是会导致 App 崩溃。
那我们接下来就分析下,对于协程发生了异常,我们该如何进行处理。
(说句题外话,其实,相对于 Kotlin,个人还是比较建议先熟悉下 Java,所以,由于协程是基于线程,针对于协程的异常处理,其实我们可以先看看线程异常该怎么样处理)
异常捕获
CoroutineExceptionHandler
最先打头阵的是 CoroutineExceptionHandler,CoroutineExceptionHandler 其实相当于 UncaughtExceptionHandler,可以针对性的给某个或某些协程进行异常捕获。具体用法如下:
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("捕获到异常: $throwable")
}
GlobalScope.launch(exceptionHandler){
throw NullPointerException()
}
日志输出:
I/System.out: 捕获到异常: java.lang.NullPointerException
这样就能够捕获到相应的协程异常了,并且捕获异常后,App 不会崩溃。与此相应的,还可以设置一个全局的协程异常捕获。
全局 CoroutineExceptionHandler
首先,我们需要写个异常的集成类,这里我命名为 GlobalCoroutineExceptionHandler
:
class GlobalCoroutineExceptionHandler: CoroutineExceptionHandler {
override val key: CoroutineContext.Key<*> = CoroutineExceptionHandler
override fun handleException(context: CoroutineContext, throwable: Throwable) {
println("全局异常捕获: $throwable")
}
}
在 main 的目录下新建目录 resources/META-INF/services
,然后创建 kotlinx.coroutines.CoroutineExceptionHandler
文件,再在文件里面填写刚刚新建的 GlobalCoroutineExceptionHandler 类的全路径。具体可以看下图:
写个异常试试:
GlobalScope.launch{
throw NullPointerException()
}
日志输出:
I/System.out: 全局异常捕获: java.lang.NullPointerException
不过,这里有一点要注意,这里的全局异常捕获,其实并不算捕获,App 还是会崩溃的,只不过可以在发生协程异常的时候,可以进行一些日志记录等。
try catch
没想到吧?协程异常也是可以 try catch
的,不过,这个只针对于具有返回值的协程操作,具体的实现可以看看 这篇文章,若你对于为什么能够 try catch
有疑惑,可以看看 这篇文章 作为参考。
另外,我们也可以利用扩展函数进行 try catch 操作:
fun <T> CoroutineScope.catchLaunch(
dispatcher: CoroutineDispatcher = Dispatchers.Default,
block: suspend CoroutineScope.() -> T
) = launch(dispatcher) {
try {
block()
} catch (e: Exception) {
if (e !is CancellationException) {
println("捕获到异常:$e")
}
}
}
使用方式:
GlobalScope.catchLaunch {
throw NullPointerException()
}
日志输出:
I/System.out: 捕获到异常:java.lang.NullPointerException
在这里,做了一个小优化,就是 e !is CancellationException
的时候才进行异常输出,这是因为当协程在调用挂起函数的时候,若已调用 cancel()
,就会抛出 CancellationException
,只是协程对于该异常静默处理了。
?:
val job = GlobalScope.catchLaunch {
try {
delay(10000)
} catch (e: Exception) {
println("捕获到异常:$e")
}
}
job.cancel()
DefaultUncaughtExceptionHandler
相信大家都知道,可以使用 DefaultUncaughtExceptionHandler 捕获全部线程的异常,而协程是基于线程的,所以,
DefaultUncaughtExceptionHandler 也是可以捕获协程的异常:
Thread.setDefaultUncaughtExceptionHandler { t, e -> println("Thread名称: ${t.name} ,Throwable: $e ") }
GlobalScope.launch {
throw NullPointerException()
}
日志输出:
I/System.out: Thread名称: DefaultDispatcher-worker-1 ,Throwable: java.lang.NullPointerException
以上内容就是对于异常捕获处理的几种方式,但是,与线程不同的是,每个线程的运行都是相对独立的,一个线程发生异常,并不会影响其它线程的运行,但是,协程就不太一样,在协程中,假如有一个协程发生了异常,就有可能会影响到其它协程的运行,请注意,“有可能”,所以,我们还需要分情况进行处理。
异常传播
默认情况
首先,我们还是先看看个栗子:
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("捕获到异常: $throwable")
}
GlobalScope.launch(exceptionHandler){
launch {
println("launch1 正在运行")
delay(1000)
throw NullPointerException()
}
launch {
println("launch2 正在运行")
delay(2000)
println("launch2 运行完成")
}
}
日志输出:
I/System.out: launch1 正在运行
I/System.out: launch2 正在运行
I/System.out: 捕获到异常: java.lang.NullPointerException
这里很明显地看出,launch1 的崩溃会导致 launch2 的退出。
coroutineScope
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("捕获到异常: $throwable")
}
GlobalScope.launch(exceptionHandler){
coroutineScope {
launch {
println("launch1 正在运行")
delay(1000)
throw NullPointerException()
}
launch {
println("launch2 正在运行")
delay(2000)
println("launch2 运行完成")
}
}
}
结论跟默认的情况一致。
supervisorScope
将 coroutineScope 替换成 supervisorScope。
日志输出:
I/System.out: launch1 正在运行
I/System.out: launch2 正在运行
I/System.out: 捕获到异常: java.lang.NullPointerException
I/System.out: launch2 运行完成
在 supervisorScope 中,假如一个协程发生了异常,并不会影响到其它协程的运行。
上下传递
刚刚我们探究的协程基本上都是同级的,那我们来看看,上下级的协程的异常又是怎么传播的:
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("exceptionHandler: 捕获到异常: $throwable")
}
val childExceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("childExceptionHandler: 捕获到异常: $throwable")
}
GlobalScope.launch(exceptionHandler) {
println("launch1 正在执行")
launch (childExceptionHandler){
println("launch2 正在执行")
throw NullPointerException()
}
delay(2000)
println("launch1 执行完成")
}
日志输出:
I/System.out: launch1 正在执行
I/System.out: launch2 正在执行
I/System.out: exceptionHandler: 捕获到异常: java.lang.NullPointerException
我们初步得到以下结论:
- 子协程的异常崩溃会影响到父协程的运行。
- 当子线程发生异常的时候,其异常不会被子协程的 CoroutineExceptionHandler 进行捕获,而是会被父协程的 CoroutineExceptionHandler 捕获。