协程异常处理(二)

总结

协程中发生的异常都会先沿着 job 体系往上传播,不过不同的 job 有不同的处理逻辑,一般有三种:

  1. 继续往上传递:launch 与 async 属于此咱类型,因此在 launch/async 外面使用 try-catch 无法捕获任何异常。原因是协程内部会 try-catch 住代码中的异常,然后将异常当作参数沿 job 往上传递

    • async 的异常处理方式,跟它们是否是顶层协程有关,具体见示例
  2. 自己处理:supervisorScope 属于此种类型,其收到异常后交由子协程处理,不会往上传递给自己的父协程。可以简单理解为supervisorScope 下的 job 是一个独立的子体系,出异常时完全内部消化

  3. 重新抛出:coroutineScope 属于此种类型,所以可以在 coroutineScope 外面使用 try-catch 捕获其内部所有协程抛出的异常

coroutineScope

其后代协程抛出的异常,都可以在外层使用 try-catch 捕获。如下

kotlin 复制代码
fun test() {
    viewModelScope.launch {
        launch {
            e("launch1")
            try {
                coroutineScope {
                    e("scope1")
                    launch {
                        e("launch2")
                        launch {
                            e("launch3")
                            throw IllegalStateException()
                        }
                    }
                }
            } catch (e:Exception){
                // 可以捕获 launch3 抛出的异常
                e("catch $e")
            }
        }
    }
}
// output
launch1
scope1
launch2
launch3
catch java.lang.IllegalStateException

supervisorScope

阻止异常沿着 Job 体系往上传播,将异常信息交由子协程处理。如下,supervisorScope 后代协程抛出异常会一直传递到 supervisorScope,它会直接交由自己的子协程 launch2 处理,launch2 配置有 handler,所以最终由 handler2 处理。

要注意:supervisorScope 只保存其直接子协程不相互影响,但不能保存所有后代子协程在有异常发生时都不会被取消。如下 launch2 的所有子协程都被取消,但 launch2 的兄弟协程 launch21 并没有。

scss 复制代码
fun test() {
    viewModelScope.launch {
        launch {
            e("launch1")
            try {
                supervisorScope {
                    e("scope1")
                    launch(handler) {
                        e("launch2")
                        launch {
                            e("launch3")
                            launch {
                                e("launch4")
                                throw IllegalStateException()
                            }
                        }
                    }
                    launch {
                        delay(100)
                        e("launch21")
                    }
                }
            } catch (e: Exception) {
                e("catch $e")
            }
        }
    }
}
// output
launch1
scope1
launch2
launch3
launch4
handle java.lang.IllegalStateException
launch21

launch

launch 对异常的处理并没有任何特殊之处,就是往上传播

async

async 对异常的处理分两种情况:

  1. async 启动的是最顶层协程,则会在 await() 中重新抛出,可使用 try-catch 进行捕获
  2. 非顶层协程,会在 await 中抛出(可使用 try-catch 捕获),同时异常会继续往上抛,如果没有对异常进行处理,也会发生崩溃。如下所示,会 catch 住异常,但应用一样会崩溃
kotlin 复制代码
viewModelScope.launch {
    val job = async {
        launch {
            e("launch1")
            throw IllegalStateException()
        }
    }
    try {
        job.await()
    } catch (e: Exception) {
        // 此处会有异常输出,但应用还是会发生 crash,并不能阻止应用崩溃
        e(e.toString())
    }
}

SupervisorJob 与 CoroutineExceptionHandler

子协程会从父协程继承除 job 外的所有 element,包括 excepthonHandler。因此在任何地方添加 handler,其子协程都会拥有 handler,这是理解 exceptionHandler 发生作用的关键。

另外,如果异常传播到最顶层,且最顶层有 exceptionHandler,则异常会交由 handler 处理。

SupervisorJob 的唯一作用就是阻断异常向上传播,让子协程自己处理异常。如果子协程有 exceptionHander,异常就会被交由 exceptionHandler 处理。

如下面代码 scope 中并未定义 handler,但 launch2 中定义有,所以 launch2 的子协程(包括 luanch3) 都会具有 exceptionHandler。在 launch3 使用 SupervisorJob 时后,launch3 的子协程抛出的异常都会被 handler 处理 ------ launch3 继承了 launch2 的 handler

kotlin 复制代码
scope.launch {
    launch(h1) {
        val job = SupervisorJob()
        e("launch2")
        launch(job) {
            e("launch3")
            launch {
                throw IllegalArgumentException()
            }
        }
    }
}

Job 与 CoroutineExceptionHandler

如果在协程中直接 new Job() 并当作参数传递给协程,则该协程及其子协程就是一个完全独立的协程体系,并不受原来的协程体系管理 ------ 也就是说即使旧协程已经取消了,新协程也不会取消,只不过新协程一样会从原来的协程中继承 handler

kotlin 复制代码
val j = viewModelScope.launch {
    launch(handler) {
        val job = Job()
        e("launch2")
        // 使用新 job 构建协程,则新协程是一个完全独立的体系
        // 当 j 取消时,并没有影响到该体系
        // 只不过它依旧继承了 handler,所以发生异常时会触发 handler
        launch(job) {
            e("launch3")
            delay(1000)
            e("launch4")
            launch {
                throw IllegalArgumentException()
            }
        }
    }
    launch {
        e("launch5")
        delay(400)
        e("launch6")
    }
}
Thread.sleep(10)
j.cancel()

// output
launch2
launch5
launch3
launch4
handle java.lang.IllegalArgumentException

Job 与 SupervisorJob

从异常处理来看,Job 与 SupervisorJob 并没有太大区别:都会新成新的独立的协程体系,异常传递到它们后都会调用 handler 对异常进行处理。两者的最大区别在于:子协程是否会被取消

如下 Scope 使用 SupervisorJob 会输出 launch5,使用 Job 则不会输出,这就是 SupervisorJob 的作用:阻止直接子协程被取消。

kotlin 复制代码
scope.launch {
    launch {
        delay(10)
        e("launch2 ${System.identityHashCode(handler)}")
        throw IllegalStateException()
    }
    launch {
        e("launch3 ${System.identityHashCode(handler)}")
        delay(1000)
        e("launch4 ${System.identityHashCode(handler)}")
    }
}

scope.launch {
    delay(100)
    e("launch5 ${System.identityHashCode(handler)}")
}

下面是一个易错点:launch 中的 Job 的直接子 Job 是 lambda 对应的 Job,并不是 launch2 对应的 Job

kotlin 复制代码
// launch 中使用 Job 与 SupervisorJob 效果完全一样,launch4 都不会输出
launch(SupervisorJob()) {
    launch {
        delay(10)
        e("launch2")
        throw IllegalStateException()
    }
    launch {
        e("launch3")
        delay(1000)
        e("launch4")
    }
}

// output
launch3
launch2
相关推荐
aqi008 小时前
鸿蒙版本的电子书阅读APP开放源码啦
android·华为·ai编程·harmonyos·鸿蒙
墨天梦8 小时前
B19_SunnyWeather到阅读客户端
android·kotlin
美狐美颜sdk9 小时前
直播APP源码可以直接接入视频美颜sdk吗?技术方案详解
android·人工智能·音视频·美颜sdk·直播美颜sdk
用户693717500138411 小时前
2026,程序员的时代拐点到了
android·前端·后端
浪潮IT馆13 小时前
Android Studio 非最新版本下载安装教程
android·ide·android studio
xiaozongt198914 小时前
MasterGo + Claude Code 生成 Android 布局:完整实践指南
android
Anhty15 小时前
2026最新免费手机音频处理工具 !!
android·功能测试·ios·智能手机·音视频
2501_9159090617 小时前
iOS应用从开发到上架App Store的完整发布流程与步骤指南
android·ios·小程序·https·uni-app·webview
狗凯之家源码网18 小时前
AI 步数修改提交系统源码应用场景与落地指南
android·数据库·人工智能·运动步数
ZHOUPUYU18 小时前
PHP 9.0 前瞻:下一代 PHP 会带来哪些新功能?
android·开发语言·php