跳到主要内容 Android Kotlin 协程异常处理流程与实战方案 | 极客日志
Kotlin 大前端 java
Android Kotlin 协程异常处理流程与实战方案 Android Kotlin 协程异常处理涉及创建、启动、执行、调度等流程。未捕获异常会导致应用崩溃。处理方案包括 try-catch 局部捕获,但嵌套协程中易遗漏。推荐使用 CoroutineExceptionHandler 统一处理。协同作用域下子协程异常会传播至父协程导致取消;主从(监督)作用域使用 SupervisorJob 或 supervisorScope 可隔离异常,防止级联取消。本文详细分析了异常产生机制及不同场景下的最佳实践。
ServerBase 发布于 2025/2/7 更新于 2026/4/18 0 浏览Kotlin 协程的异常处理
在开发 Android 应用时,出现未捕获的异常就会导致程序退出。同样的协程出现未捕获异常,也会导致应用退出。我们要处理异常,那就得先看看协程中的异常产生的流程是什么样的。
协程异常的产生流程
我们在开发 Android 应用时,出现未捕获的异常就会导致程序退出。同样的协程出现未捕获异常,也会导致应用退出。我们要处理异常,那就得先看看协程中的异常产生的流程是什么样的,协程中出现未捕获的异常时会出现哪些信息,如下:
private fun testCoroutineExceptionHandler () {
GlobalScope.launch {
val job = launch {
Log.d("${Thread.currentThread().name} " , "抛出未捕获异常" )
throw NullPointerException("异常测试" )
}
job.join()
Log.d("${Thread.currentThread().name} " , "end" )
}
}
我们抛出了一个 NullPointerException 异常但没有去捕获,所以会导致了应用崩溃退出。
D/DefaultDispatcher-worker-2 : 抛出未捕获异常
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
Process: com.carman.kotlin.coroutine, PID: 22734
java.lang.NullPointerException: 异常测试
at com.carman.kotlin.coroutine.MainActivity$testException$1 $job$1 .invokeSuspend (MainActivity.kt :251 )
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. : )
at kotlinx.coroutines.scheduling.CoroutineScheduler . (CoroutineScheduler. : )
at kotlinx.coroutines.scheduling.CoroutineScheduler . (CoroutineScheduler. : )
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
kt
750
$Worker
runWorker
kt
678
$Worker
run
kt
665
我们看到这个异常是在 CoroutineScheduler 中产生的。虽然不知道 CoroutineScheduler 是个什么东西,但是我们可以从日志上运行的方法名称先大概的分析一下流程:
它先是创建一个 CoroutineScheduler 的一个 Worker 对象,接着运行 Worker 对象的 run 方法,然后 runWorker 方法调用了 executeTask,紧接着又在 executeTask 里面执行了 runSafely,再接着通过 runSafely 运行了 DispatchedTask 的 run 方法,最后 DispatchedTask.run 调用了 continuation 的 resumeWith 方法,resumeWith 方法中在执行 invokeSuspend 的时候抛出了异常。
再来个通俗一点的,你们应该就能猜出大概意思来。雇主先是找包工头 CoroutineScheduler 要了一个工人 Worker,然后给这个工人安排了一个搬砖任务 DispatchedTask,同时告诉这个工人他要安全 runSafely 的搬砖,然后雇主就让工人 Worker 开始工作 runWorker,工人 Worker 就开始执行 executeTask 雇主吩咐的任务 DispatchedTask,最后通过 resumeWith 来执行 invokeSuspend 的时候告诉雇主出现了问题 (抛出了异常)。
别着急,仔细想一想,有没有发现这个跟 ThreadPoolExecutor 线程池和 Thread 线程的运行很像。包工头就像是 ThreadPoolExecutor 线程池,工人就是 Thread 线程。
我们通过线程池 (CoroutineScheduler) 创建了一个 Thread 线程 (Worker),然后开始执行线程 (runWorker),线程里面通过 executeTask 执行一个任务 DispatchedTask,在执行任务的时候我们通过 try..catch 来保证任务安全执行 runSafely,然后在 DispatchedTask 执行任务的时候,因为运行出现异常,所以在 catch 中通过 resumeWith 来告知结果线程出问题了。咦,逻辑好像突然变得清晰很多。
这么看的话,这个协程异常的产生是不是基本原理就出来了。那么我们接下来看看是不是正如我们所想的,我们先找到 CoroutineScheduler 看看他的实现:
internal class CoroutineScheduler (...) : Executor, Closeable {
@JvmField
val globalBlockingQueue = GlobalQueue()
fun runSafely (task: Task ) {
try {
task.run()
} catch (e: Throwable) {
val thread = Thread.currentThread()
thread.uncaughtExceptionHandler.uncaughtException(thread, e)
} finally {
unTrackTask()
}
}
internal inner class Worker private constructor () : Thread() {
override fun run () = runWorker()
private fun runWorker () {
var rescanned = false
while (!isTerminated && state != WorkerState.TERMINATED) {
val task = findTask(mayHaveLocalTasks)
if (task != null ) {
rescanned = false
minDelayUntilStealableTaskNs = 0L
executeTask(task)
continue
} else {
mayHaveLocalTasks = false
}
continue
}
}
private fun executeTask (task: Task ) {
runSafely(task)
}
fun findTask (scanLocalQueue: Boolean ) : Task? {
if (tryAcquireCpuPermit()) return findAnyTask(scanLocalQueue)
val task = if (scanLocalQueue) {
localQueue.poll() ?: globalBlockingQueue.removeFirstOrNull()
} else {
globalBlockingQueue.removeFirstOrNull()
}
return task ?: trySteal(blockingOnly = true )
}
}
}
哎呀呀,不得了,跟我们上面想的一模一样。CoroutineScheduler 继承 Executor,Worker 继承 Thread,同时 runWorker 也是线程的 run 方法。在 runWorker 执行了 executeTask(task),接着在 executeTask 调用中 runSafely(task),然后我们看到 runSafely 使用 try..catch 了这个 task 任务的执行,最后在 catch 中抛出了未捕获的异常。那么很明显这个 task 肯定就是我们的 DispatchedTask,那就到这里结束了么?
很明显并没有,我们看到 catch 中抛出的是个线程的 uncaughtExceptionHandler,这个我们就很熟了,在 Android 开发中都是通过这个崩溃信息。但是这个明显不是我们这次的目标。
继续往下分析,我们看看这个 task 到底是不是 DispatchedTask。回到 executeTask(task) 的调用出,我们看到这个 task 是通过 findTask 获取的,而这个 task 又是在 findTask 中通过 CoroutineScheduler 线程池中的 globalBlockingQueue 队列中取出的,我们看看这个 GlobalQueue:
internal class GlobalQueue : LockFreeTaskQueue <Task >(singleConsumer = false )
internal actual typealias SchedulerTask = Task
我可以看到这个队列里面存放的就是 Task,又通过 kotlin 语言中的给 Task 取了一个 SchedulerTask 的别名。而 DispatchedTask 继承自 SchedulerTask,那么 DispatchedTask 的来源就解释清楚了。
internal abstract class DispatchedTask <in T >(
@JvmField public var resumeMode: Int
) : SchedulerTask() {
internal open fun getExceptionalResult (state: Any ?) : Throwable? =
(state as ? CompletedExceptionally)?.cause
public final override fun run () {
assert { resumeMode != MODE_UNINITIALIZED }
val taskContext = this .taskContext
var fatalException: Throwable? = null
try {
val delegate = delegate as DispatchedContinuation<T>
val continuation = delegate.continuation
withContinuationContext(continuation, delegate.countOrElement) {
val context = continuation.context
val state = takeState()
val exception = getExceptionalResult(state)
val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null
if (job != null && !job.isActive) {
val cause = job.getCancellationException()
cancelCompletedResult(state, cause)
continuation.resumeWithStackTrace(cause)
} else {
if (exception != null ) {
continuation.resumeWithException(exception)
} else {
continuation.resume(getSuccessfulResult(state))
}
}
}
} catch (e: Throwable) {
fatalException = e
} finally {
val result = runCatching { taskContext.afterTask() }
handleFatalException(fatalException, result.exceptionOrNull())
}
}
}
接着我们继续看 DispatchedTask 的 run 方法,前面怎么获取 exception 的我们先不管,直接看当 exception 不为空时,通过 continuation 的 resumeWithException 返回了异常。我们在上面提到过 continuation,在挂起函数的挂起以后,会通过 Continuation 调用 resumeWith 函数恢复协程的执行,同时返回 Result<T> 类型的成功或者失败。实际上 resumeWithException 调用的是 resumeWith,只是它是个扩展函数,只是它只能返回 Result.failure。同时异常就这么被 Continuation 无情抛出。
public inline fun <T> Continuation<T> .resumeWithException (exception: Throwable ) : Unit =
resumeWith(Result.failure(exception))
诶,不对啊,我们在这里还没有执行 invokeSuspend 啊,你是不是说错了。
是滴,这里只是一种可能,我们现在回到调用 continuation 的地方,这里的 continuation 在前面通过 DispatchedContinuation 得到的,而实际上 DispatchedContinuation 是个 BaseContinuationImpl 对象(这里不扩展它是怎么来的,不然又得从头去找它的来源 )。
val delegate = delegate as DispatchedContinuation<T>
val continuation = delegate.continuation
internal abstract class BaseContinuationImpl (
public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
public final override fun resumeWith (result: Result <Any ?>) {
var current = this
var param = result
while (true ) {
probeCoroutineResumed(current)
with(current) {
val completion = completion!!
val outcome: Result<Any?> =
try {
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
releaseIntercepted()
if (completion is BaseContinuationImpl) {
current = completion
param = outcome
} else {
completion.resumeWith(outcome)
return
}
}
}
}
}
可以看到最终这里面 invokeSuspend 才是真正调用我们协程的地方。最后也是通过 Continuation 调用 resumeWith 函数恢复协程的执行,同时返回 Result<T> 类型的结果。和我们上面说的是一样的,只是他们是在不同阶段。
那、那、那、那下面那个 finally 它又是有啥用,我们都通过 resumeWithException 把异常抛出去了,为啥下面又还有个 handleFatalException,这货又是干啥用的???
handleFatalException 主要是用来处理 kotlinx.coroutines 库的异常,我们这里大致的了解下就行了。主要分为两种:
kotlinx.coroutines 库或编译器有错误,导致的内部错误问题。
ThreadContextElement 也就是协程上下文错误,这是因为我们提供了不正确的 ThreadContextElement 实现,导致协程处于不一致状态。
public interface ThreadContextElement <S > : CoroutineContext.Element {
public fun updateThreadContext (context: CoroutineContext ) : S
public fun restoreThreadContext (context: CoroutineContext , oldState: S )
}
我们看到 handleFatalException 实际是调用了 handleCoroutineException 方法。handleCoroutineException 是 kotlinx.coroutines 库中的顶级函数
public fun handleFatalException (exception: Throwable ?, finallyException: Throwable ?) {
handleCoroutineException(this .delegate.context, reason)
}
public fun handleCoroutineException (context: CoroutineContext , exception: Throwable ) {
try {
context[CoroutineExceptionHandler]?.let {
it.handleException(context, exception)
return
}
} catch (t: Throwable) {
handleCoroutineExceptionImpl(context, handlerException(exception, t))
return
}
handleCoroutineExceptionImpl(context, exception)
}
我们看到 handleCoroutineException 会先从协程上下文拿 CoroutineExceptionHandler,如果我们没有定义的 CoroutineExceptionHandler 话,它将会调用 handleCoroutineExceptionImpl 抛出一个 uncaughtExceptionHandler 导致我们程序崩溃退出。
internal actual fun handleCoroutineExceptionImpl (context: CoroutineContext , exception: Throwable ) {
for (handler in handlers) {
try {
handler.handleException(context, exception)
} catch (t: Throwable) {
val currentThread = Thread.currentThread()
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, handlerException(exception, t))
}
}
val currentThread = Thread.currentThread()
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
}
不知道各位是否理解了上面的流程,笔者最开始的时候也是被这里来来回回的绕着晕乎乎的。如果没看懂的话,可以休息一下,揉揉眼睛,倒杯热水,再回过头捋一捋。
好滴,到此处为止。我们已经大概的了解 kotlin 协程中异常是如何抛出的,下面我们就不再不过多延伸。下面我们来说说异常的处理。
协程的异常处理 kotlin 协程异常处理我们要分成两部分来看,通过上面的分解我们知道一种异常是通过 resumeWithException 抛出的,还有一种异常是直接通过 CoroutineExceptionHandler 抛出,那么我们现在就开始讲讲如何处理异常。
第一种:当然就是我们最常用的 try..catch 大法啦,只要有异常崩溃我就先 try..catch 下,先不管流程对不对,我先保住我的程序不能崩溃。
private fun testException () {
GlobalScope.launch{
launch(start = CoroutineStart.UNDISPATCHED) {
Log.d("${Thread.currentThread().name} " , "我要开始抛异常了" )
try {
throw NullPointerException("异常测试" )
} catch (e: Exception) {
e.printStackTrace()
}
}
Log.d("${Thread.currentThread().name} " , "end" )
}
}
D / DefaultDispatcher - worker- 1 : 我要开始抛异常了
W / System .err: java.lang.NullPointerException : 异常测试
W / System .err: at com.carman.kotlin.coroutine.MainActivity $testException $1 $1 .invokeSuspend(MainActivity .kt:252 )
W / System .err: at com.carman.kotlin.coroutine.MainActivity $testException $1 $1 .invoke(Unknown
D / DefaultDispatcher - worker- 1 : end
诶嘿,这个时候我们程序没有崩溃,只是输出了警告日志而已。那如果遇到 try..catch 搞不定的怎么办,或者遗漏了需要 try..catch 的位置怎么办。比如:
private fun testException () {
var a:MutableList<Int > = mutableListOf(1 ,2 ,3 )
GlobalScope.launch{
launch {
Log.d("${Thread.currentThread().name} " ,"我要开始抛异常了" )
try {
launch{
Log.d("${Thread.currentThread().name} " , "${a[1 ]} " )
}
a.clear()
} catch (e: Exception) {
e.printStackTrace()
}
}
Log.d("${Thread.currentThread().name} " , "end" )
}
}
D/DefaultDispatcher-worker-1 : end
D/DefaultDispatcher-worker-2 : 我要开始抛异常了
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-2
Process: com.carman.kotlin.coroutine, PID: 5394
java.lang.IndexOutOfBoundsException: Index: 1 , Size: 0
at java.util.ArrayList.get (ArrayList.java :437 )
at com.carman.kotlin.coroutine.MainActivity$testException$1 $1 $1 .invokeSuspend (MainActivity.kt :252 )
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 )
当你以为使用 try..catch 就能捕获的时候,然而实际并没有。这是因为我们的 try..catch 使用方式不对,我们必须在使用 a[1] 时候再用 try..catch 捕获才行。那就有人会想那我每次都记得使用 try..catch 就好了。
是,当然没问题。但是你能保证你每次都能记住吗,你的同一战壕里的战友会记住吗。而且当你的逻辑比较复杂的时候,你使用那么多 try..catch 你代码阅读性是不是降低了很多后,你还能记住哪里有可能会出现异常吗。
这个时候就需要使用协程上下文中的 CoroutineExceptionHandler。我们在上一篇文章讲解协程上下文的时候提到过,它是协程上下文中的一个 Element,是用来捕获协程中未处理的异常。
public interface CoroutineExceptionHandler : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
public fun handleException (context: CoroutineContext , exception: Throwable )
}
private fun testException () {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler" , "${coroutineContext[CoroutineName]} :$throwable " )
}
GlobalScope.launch(CoroutineName("异常处理" ) + exceptionHandler){
val job = launch{
Log.d("${Thread.currentThread().name} " ,"我要开始抛异常了" )
throw NullPointerException("异常测试" )
}
Log.d("${Thread.currentThread().name} " , "end" )
}
}
D/DefaultDispatcher-worker-1: 我要开始抛异常了
D/exceptionHandler: CoroutineName(异常处理) :java.lang.NullPointerException: 异常测试
D/DefaultDispatcher-worker-2: end
这个时候即使我们没有使用 try..catch 去捕获异常,但是异常还是被我们捕获处理了。是不是感觉异常处理也没有那么难。那如果按照上面的写,我们是不是得在每次启动协程的时候,也需要跟 try..catch 一样都需要加上一个 CoroutineExceptionHandler 呢? 这个时候我们就看出来,各位是否真的有吸收前面讲解的知识:
第一种:我们上面讲解的 协程作用域 部分你已经消化吸收,那么恭喜你接下来的你可以大概的过一遍或者选择跳过了。因为接下来的部分和 协程作用域 中说到的内容大体一致。
第二种:除第一种的,都是第二种。那你接下来你就得认证仔细的看了。
我们之前在讲到 协同作用域 和 主从 (监督) 作用域 的时候提到过,异常传递的问题。我们先来看看 协同作用域:
协同作用域 如果子协程抛出未捕获的异常时,会将异常传递给父协程处理,如果父协程被取消,则所有子协程同时也会被取消。
默认情况下,当协程因出现异常失败时,它会将异常传播到它的父级,父级会取消其余的子协程,同时取消自身的执行。最后将异常在传播给它的父级。当异常到达当前层次结构的根,在当前协程作用域启动的所有协程都将被取消。
我们在前一个案例的基础上稍作做一下修改,只在父协程上添加 CoroutineExceptionHandler,照例上代码:
private fun testException () {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler" , "${coroutineContext[CoroutineName]} 处理异常 :$throwable " )
}
GlobalScope.launch(CoroutineName("父协程" ) + exceptionHandler){
val job = launch(CoroutineName("子协程" )) {
Log.d("${Thread.currentThread().name} " ,"我要开始抛异常了" )
for (index in 0. .10 ){
launch(CoroutineName("孙子协程$index " )) {
Log.d("${Thread.currentThread().name} " ,"${coroutineContext[CoroutineName]} " )
}
}
throw NullPointerException("空指针异常" )
}
for (index in 0. .10 ){
launch(CoroutineName("子协程$index " )) {
Log.d("${Thread.currentThread().name} " ,"${coroutineContext[CoroutineName]} " )
}
}
try {
job.join()
} catch (e: Exception) {
e.printStackTrace()
}
Log.d("${Thread.currentThread().name} " , "end" )
}
}
D/DefaultDispatcher-worker-3 : 我要开始抛异常了
W/System.err: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine is cancelling; job=StandaloneCoroutine{Cancelling}@f6b7807
W/System.err : Caused by : java.lang.NullPointerException : 空指针异常
W/System.err : at com.carman.kotlin.coroutine.MainActivity$testException$1 $job$1 .invokeSuspend(MainActivity.kt :26 //省略...
D/DefaultDispatcher-worker-6 : end
D/exceptionHandler : CoroutineName(父协程) 处理异常 :java.lang.NullPointerException : 空指针异常
我们看到子协程 job 的异常被父协程处理了,无论我下面开启多少个子协程产生异常,最终都是被父协程处理。但是有个问题是:因为异常会导致父协程被取消执行,同时导致后续的所有子协程都没有执行完成 (可能偶尔有个别会执行完) 。那可能就会是有人问了,这种做法的意义和应用场景是什么呢?
如果有一个页面,它最终展示的数据,是通过请求多个服务器接口的数据拼接而成的,而其中某一个接口出问题都将不进行数据展示,而是提示加载失败。那么你就可以使用上面的方案去做,都不用管它们是谁报的错,反正都是统一处理,一劳永逸。类似这样的例子我们在开发中应该经常遇到。
但是另外一个问题就来了。例如我们 APP 的首页,首页上展示的数据五花八门。如:广告,弹窗,未读状态,列表数据等等都在首页存在,但是他们相互之间互不干扰又不关联,即使其中某一个失败了也不影响其他数据展示。那通过上面的方案,我们就没办法处理。
这个时候我们就可以通过 主从 (监督) 作用域 的方式去实现,与 协同作用域 一致,区别在于该作用域下的协程取消操作的单向传播性,子协程的异常不会导致其它子协程取消。我再盗个官方图:
我们在讲解 主从 (监督) 作用域 的时候提到过,要实现 主从 (监督) 作用域 需要使用 supervisorScope 或者 SupervisorJob。这里我们需要补充一下,我们在使用 supervisorScope 其实用的就是 SupervisorJob。这也是为什么使用 supervisorScope 与使用 SupervisorJob 协程处理是一样的效果。
public suspend fun <R> supervisorScope (block: suspend CoroutineScope .() -> R ) : R {
}
这段是摘自官方文档的,其他的我把它们省略了,只留了一句:"SupervisorJob 会覆盖上下文中的 Job"。这也就说明我们在使用 supervisorScope 的就是使用的 SupervisorJob。我们先用 supervisorScope 实现以下我们上面提到的案例:
private fun testException () {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler" , "${coroutineContext[CoroutineName].toString()} 处理异常 :$throwable " )
}
GlobalScope.launch(exceptionHandler) {
supervisorScope {
launch(CoroutineName("异常子协程" )) {
Log.d("${Thread.currentThread().name} " , "我要开始抛异常了" )
throw NullPointerException("空指针异常" )
}
for (index in 0. .10 ) {
launch(CoroutineName("子协程$index " )) {
Log.d("${Thread.currentThread().name} 正常执行" , "$index " )
if (index %3 == 0 ){
throw NullPointerException("子协程${index} 空指针异常" )
}
}
}
}
}
}
D/DefaultDispatcher-worker-1: 我要开始抛异常了
D/exceptionHandler: CoroutineName(异常子协程) 处理异常 :java.lang.NullPointerException: 空指针异常
D/DefaultDispatcher-worker-1 正常执行:1
D/DefaultDispatcher-worker-1 正常执行:2
D/DefaultDispatcher-worker-3 正常执行:0
D/DefaultDispatcher-worker-1 正常执行:3
D/exceptionHandler: CoroutineName(子协程 0) 处理异常 :java.lang.NullPointerException: 子协程 0 空指针异常
D/exceptionHandler: CoroutineName(子协程 3) 处理异常 :java.lang.NullPointerException: 子协程 3 空指针异常
D/DefaultDispatcher-worker-4 正常执行:4
D/DefaultDispatcher-worker-4 正常执行:5
D/DefaultDispatcher-worker-5 正常执行:7
D/DefaultDispatcher-worker-3 正常执行:6
D/DefaultDispatcher-worker-5 正常执行:8
D/DefaultDispatcher-worker-5 正常执行:9
D/exceptionHandler: CoroutineName(子协程 9) 处理异常 :java.lang.NullPointerException: 子协程 9 空指针异常
D/exceptionHandler: CoroutineName(子协程 6) 处理异常 :java.lang.NullPointerException: 子协程 6 空指针异常
D/DefaultDispatcher-worker-2 正常执行:10
可以看到即使当中有多个协程都出现问题,我们还是能够让所有的子协程执行完成。这个时候我们用这样方案是不是就可以解决,我们首页多种数据互不干扰的刷新问题了,同也能够在出现异常的时候统一处理。
那我们在用 SupervisorJob 实现一遍,看看是不是和 supervisorScope 一样的,代码奉上:
private fun testException () {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler" , "${coroutineContext[CoroutineName].toString()} 处理异常 :$throwable " )
}
val supervisorScope = CoroutineScope(SupervisorJob() + exceptionHandler)
with(supervisorScope) {
launch(CoroutineName("异常子协程" )) {
Log.d("${Thread.currentThread().name} " , "我要开始抛异常了" )
throw NullPointerException("空指针异常" )
}
for (index in 0. .10 ) {
launch(CoroutineName("子协程$index " )) {
Log.d("${Thread.currentThread().name} 正常执行" , "$index " )
if (index % 3 == 0 ) {
throw NullPointerException("子协程${index} 空指针异常" )
}
}
}
}
可以看到我们通过 CoroutineScope 创建一个 SupervisorJob 的 supervisorScope,然后再通过 with(supervisorScope) 是不是就变得跟直接使用 supervisorScope 一样了。
D/DefaultDispatcher-worker-1: 我要开始抛异常了
D/DefaultDispatcher-worker-2 正常执行:0
D/exceptionHandler: CoroutineName(子协程 0) 处理异常 :java.lang.NullPointerException: 子协程 0 空指针异常
D/exceptionHandler: CoroutineName(异常子协程) 处理异常 :java.lang.NullPointerException: 空指针异常
D/DefaultDispatcher-worker-2 正常执行:1
D/DefaultDispatcher-worker-2 正常执行:2
D/DefaultDispatcher-worker-4 正常执行:3
D/exceptionHandler: CoroutineName(子协程 3) 处理异常 :java.lang.NullPointerException: 子协程 3 空指针异常
D/DefaultDispatcher-worker-1 正常执行:4
D/DefaultDispatcher-worker-4 正常执行:5
D/DefaultDispatcher-worker-4 正常执行:6
D/exceptionHandler: CoroutineName(子协程 6) 处理异常 :java.lang.NullPointerException: 子协程 6 空指针异常
D/DefaultDispatcher-worker-4 正常执行:8
D/DefaultDispatcher-worker-3 正常执行:7
D/DefaultDispatcher-worker-2 正常执行:9
D/exceptionHandler: CoroutineName(子协程 9) 处理异常 :java.lang.NullPointerException: 子协程 9 空指针异常
D/DefaultDispatcher-worker-3 正常执行:10
当然,我们在使用协程的时候,可能某个协程需要自己处理自己的异常,这个时候只需要在这个协程的上下文中添加 CoroutineExceptionHandler 即可。毕竟按需使用,谁也不知道产品又会有什么奇怪的想法。
好了,到现在我们也基本的知道协程中的异常产生流程,和按需处理协程中的异常问题。如果您还有什么不清楚的地方,可以自己动手实验一下。