Kotlin | 关于协程异常处理,你想知道的都在这里(下)

简介: 关于协程的异常处理,一直以来都不是一个简单问题。因为涉及到了很多方面,包括 异常的传递 ,结构化并发下的异常处理 ,异常的传播方式 ,不同的Job 等,所以常常让很多(特别是刚使用协程的,也不乏老手)同学摸不着头脑。

答案是: 不会生效

Tips: 如果你不是很理解 async 的 CoroutineContext 里此时为什么要加 SupervisorJob ,请看下面,会再做解释。

你可能会想,这还不简单吗,上面不是已经提过了,如果根协程或者scope中没有设置 CoroutineExceptionHandler,异常会被直接抛出,所以这里肯定异常了啊。


如果你这样想了,恭喜回答正确~ 👏

那该怎么改一下上述示例呢?

scope 初始化时 或者 根协程里 加上 CoroutineExceptionHandler,或者直接 async 里面 try catch 都可以。那还有没有其他方式呢?

此处停留10s 思考,loading…

如果你还记得我们最开始说过的异常的 传播形式 ,就会知道,对于 async 这种,在其异常时,其会主动向用户暴漏,而不是优先向上传递。

也就是说,我们直接可以在 await() 时 try Catch 。代码如下:

scope.launch {
    val asyncA = async(SupervisorJob()){}
    val asyncB = async(SupervisorJob()){}
    val resultA = kotlin.runCatching { asyncA.await() }
    val resultB = kotlin.runCatching { asyncB.await() }
}

runCatching 是 kotlin 中对于 tryCatch 的一种包装,其会将结果使用 Result 类进行包装,从而让我们能更直观的处理结果,从而更加符合 kotlin 的语法习惯。

Tips

为什么上述 async 里要添加 SupervisorJob() ,这里再做一个解释。

val scope = CoroutineScope(Job())
scope.launch {
    val asyncA = async(SupervisorJob()) { throw RuntimeException()}
    val asyncB = async xxx
}

因为 async 时内部也是新的作用域,如果 async 对应的是根协程,那么我们可以在 await() 时直接捕获异常。怎么理解呢?

如下示例:

val scope = CoroutineScope(Job())
// async 作为根协程
val asyncA = scope.async { throw NullPointerException() }
val asyncB = scope.async { }
scope.launch {
    // 此时可以直接tryCatch
    kotlin.runCatching {
        asyncA.await()
        asyncB.await()
    }
}

但如果 async 其对应的不是根协程(即不是 scope直接.async ),则会先将异常传递给父协程,从而导致异常没有在调用处暴漏,我们的tryCatch 自然也就无法拦截。如果此时我们为其增加 SupervisorJob() ,则标志着其不会主动传递异常,而是由该协程自行处理。所以我们可以在调用处(await()) 捕获。


相关扩展

supervisorScope

官方解释如下:使用 SupervisorJob 创建一个 CoroutineScope 并使用此范围调用指定的挂起块。提供的作用域从外部作用域继承其coroutineContext ,但用 SupervisorJob 覆盖上下文的 Job 。一旦给定块及其所有子协程完成,此函数就会返回。

通俗点就是,我们帮你创建了一个 CoroutineScope ,初始化作用域时,使用 SupervisorJob 替代默认的Job,然后将其的作用域扩展至外部调用。如下代码所示:

val scope = CoroutineScope(CoroutineExceptionHandler { _, _ -> })
scope.launch() {
    supervisorScope {
        // launch A ❎
        launch(CoroutineName("A")) {
            delay(10)
            throw RuntimeException()
        }
        // launch B 👍
        launch(CoroutineName("B")) {
            delay(100)
            Log.e("petterp", "正常执行,我不会收到影响")
        }
    }
}

当 supervisorScope 里的所有子协程执行完成时,其就会正常退出作用域。

需要注意的是,supervisorScope 内部的 Job 为 SupervisorJob ,所以当作用域中子协程异常时,异常不会主动层层向上传递,而是由子协程自行处理,所以意味着我们也可以为子协程增加 CoroutineExceptionHandler 。如下所示:

当子协程异常时,因为我们使用了 supervisorScope ,所以异常此时不会主动传递给外部,而是由子类自行处理。

当我们在内部 launch 子协程时,其实也就是类似 scope.launch ,所以此时子协程A相也就是根协程,所以我们使用 CoroutineExceptionHandler 也可以正常拦截异常。但如果我们子协程不增加 CoroutineExceptionHandler ,则此时异常会被supervisorScope 抛出,然后被外部的 CoroutineExceptionHandler 拦截(也就是初始化scope作用域时使用的 ExceptionHandler)。

相应的,与 supervisorScope 相似的,还有一个 coroutineScope ,下面我们也来说一下这个。


coroutineScope

其主要用于并行分解协程子任务时而使用,当其范围内任何子协程失败时,其所有的子协程也都将被取消,一旦内部所有的子协程完成,其也会正常返回。

如下示例:

当子协程A 异常未被捕获时,此时 子协程B 和整个 协程作用域 都将被异常取消,此时异常将传递到顶级 CoroutineExceptionHandler

场景推荐

严格意义上来说,所有异常都可以用 tryCatch 去处理,只要我们的处理位置得当。但这并不是所有方式的最优解,特别是如果你想更优雅的处理异常时,此时就可以考虑 CoroutineExceptionHandler 。下面我们通过实际需求来举例,从而体会异常处理的的一些实践。

什么时候该用 SupervisorJob ,什么时候该用 Job?

引用官方的一句话就是:想要避免取消操作在异常发生时被传播,记得使用 SupervisorJob ;反之则使用 Job。

对于一个普通的协程,如何处理我的异常?

对于一个普通的协程,你可以在其协程作用域内使用 tryCatch(runCatching) ,如果其是根协程,你也可以使用 CoroutineExceptionHandler 作为最后的拦截手段 ,如下所示:

val scope = CoroutineScope(Job())
scope.launch {
    runCatching { }
}
scope.launch(CoroutineExceptionHandler { _, throwable -> }) {  
}

在某个子协程中,想使用 SupervisorJob 的特性去作为某个作用域去执行?

val scope = CoroutineScope(Job())
scope.launch(CoroutineExceptionHandler { _, _ -> }) {
    supervisorScope {
        launch(CoroutineName("A")) {
            throw NullPointerException()
        }
        launch(CoroutineName("B")) {
            delay(1000)
            Log.e("petterp", "依然会正常执行")
        }
    }
}

SupervisorJob+tryCatch

我们有两个接口 A,B 需要同时请求,当接口A异常时,需要不影响B接口的正常展示,当接口B异常时,此时界面展示异常信息。伪代码如下:

val scope = CoroutineScope(Job())
scope.launch {
    val jobA = async(SupervisorJob()) {
        throw NullPointerException()
    }
    val jobB = async(SupervisorJob()) {
        delay(100)
        1
    }
    val resultA = kotlin.runCatching { jobA.await() }
    val resultB = kotlin.runCatching { jobB.await() }
}

CoroutineExceptionHandler+SupervisorJob

如果你有一个顶级协程,并且需要自动捕获所有的异常,则此时可以选用上述方式,如下所示:

val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
    Log.e("petterp", "自动捕获所有异常")
}
val ktxScope = CoroutineScope(SupervisorJob() + exceptionHandler)
目录
相关文章
|
30天前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
19 1
|
3月前
|
调度 开发者 UED
Kotlin 中的协程是什么?
【8月更文挑战第31天】
205 0
|
5月前
|
Java Serverless Kotlin
Kotlin中的异常处理
Kotlin中的异常处理
206 1
|
5月前
|
存储 Java 调度
Android面试题之Kotlin 协程的挂起、执行和恢复过程
了解Kotlin协程的挂起、执行和恢复机制。挂起时,状态和上下文(局部变量、调用栈、调度器等)被保存;挂起点通过`Continuation`对象处理,释放线程控制权。当恢复条件满足,调度器重新分配线程,调用`resumeWith`恢复执行。关注公众号“AntDream”获取更多并发知识。
124 2
|
6月前
|
移动开发 Android开发 开发者
构建高效Android应用:Kotlin与协程的完美融合
【5月更文挑战第25天】 在移动开发的世界中,性能和响应性是衡量应用质量的关键指标。随着Kotlin的流行和协程的引入,Android开发者现在有了更强大的工具来提升应用的性能和用户体验。本文深入探讨了Kotlin语言如何与协程相结合,为Android应用开发带来异步处理能力的同时,保持代码的简洁性和可读性。我们将通过实际案例分析,展示如何在Android项目中实现协程,以及它们如何帮助开发者更有效地管理后台任务和用户界面的流畅交互。
|
6月前
|
移动开发 数据库 Android开发
构建高效Android应用:探究Kotlin的协程优势
【5月更文挑战第22天】随着移动开发技术的不断进步,Android平台的性能优化已经成为开发者关注的焦点。在众多提升应用性能的手段中,Kotlin语言提供的协程概念因其轻量级线程管理和异步编程能力而受到广泛关注。本文将深入探讨Kotlin协程在Android开发中的应用,以及它如何帮助开发者构建出更高效、响应更快的应用,同时保持代码的简洁性和可读性。
|
6月前
|
移动开发 Android开发 开发者
构建高效安卓应用:Kotlin 协程的实践指南
【5月更文挑战第18天】 随着移动开发技术的不断进步,安卓平台亟需一种高效的异步编程解决方案来应对日益复杂的应用需求。Kotlin 协程作为一种新兴的轻量级线程管理机制,以其简洁的语法和强大的功能,成为解决这一问题的关键。本文将深入探讨Kotlin协程在安卓开发中的实际应用,从基本概念到高级技巧,为开发者提供一份全面的实践指南,旨在帮助读者构建更加高效、稳定的安卓应用。
|
5月前
|
XML 存储 数据格式
Kotlin Fuel库:图像下载过程中的异常处理
Kotlin Fuel库:图像下载过程中的异常处理
|
6月前
|
移动开发 数据处理 Android开发
构建高效Android应用:Kotlin的协程与Flow的使用
【5月更文挑战第23天】 在移动开发领域,性能优化和异步编程一直是核心议题。随着Kotlin语言在Android开发中的普及,其提供的协程(coroutines)和流式编程(Flow)功能为开发者带来了革命性的工具,以更简洁、高效的方式处理异步任务和数据流。本文将深入探讨Kotlin协程和Flow在Android应用中的实际应用,以及它们如何帮助开发者编写更加响应迅速且不阻塞用户界面的应用程序。我们将通过具体案例分析这两种技术的优势,并展示如何在现有项目中实现这些功能。
|
2月前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
75 1