Kotlin 协程的异常处理
1. 一个协程突然失败了!我该怎么办?
在协程中,如果某个协程发生异常,它会传播到其父级协程,导致父协程和所有子协程的取消。这种异常传播机制在有些场景下非常有用,但也可能带来一些问题。
协程异常传播的基本流程:
- 子协程发生异常,异常会传播到父级协程。
- 父协程取消其他子协程。
- 父协程自己也会被取消,并且异常继续向上传播。
问题:如果某个子协程失败,整个父协程及其他兄弟协程都会被取消,这会影响其他正常工作的协程。
2. SupervisorJob 来拯救你
为了解决协程异常传播带来的问题,我们可以使用 SupervisorJob
。它允许我们在一个协程发生异常时,其他兄弟协程不受影响,父协程也不会被取消。
示例代码:
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
scope.launch {
val deferred1 = async {
log("hello")
delay(300)
throw IllegalStateException("hello")
}
val deferred2 = async {
log("world")
delay(10000)
log("卧槽")
}
deferred1.await() // Will throw
deferred2.await()
log("哈哈")
}
输出:
hello
world
后续的日志没出现,应用崩溃
使用 SupervisorJob
后,子协程失败不会影响其他子协程的执行。
3. Job or SupervisorJob?
- Job:使用
Job
时,异常会传播到父协程,导致父协程和所有子协程被取消。 - SupervisorJob:使用
SupervisorJob
时,父协程和兄弟协程不会受到影响,异常不会传播。
何时使用:
- 使用
Job
:当你希望父协程在子协程失败时被取消时。 - 使用
SupervisorJob
:当你希望一个子协程失败不会影响其他子协程时。
示例:
val scope = CoroutineScope(SupervisorJob())
scope.launch {
// Child 1
}
scope.launch {
// Child 2
}
在这种情况下,即使 Child 1
失败,Child 2
也不会被取消。
4. 协程的 parent 是谁?
协程的父协程是在启动协程时通过 CoroutineScope
或 supervisorScope
设置的。当你使用 SupervisorJob
创建协程时,这个 SupervisorJob
的行为仅在它是父协程时才有效。如果将它作为构造器参数传递给其他协程,它就不会发挥作用。
示例:
val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
// Parent is Job, not SupervisorJob
launch {
// Child 1
}
launch {
// Child 2
}
}
注意:SupervisorJob
只有在它作为 CoroutineScope
的一部分时才会生效。
5. 底层原理
如果你对 Job
和 SupervisorJob
的工作原理感兴趣,可以查看 JobSupport.kt
文件中的 childCancelled
和 notifyCancelling
函数的实现。在 SupervisorJob
中,childCancelled
返回 false
,表示它不会传播取消,但不会处理异常。
6. 处理异常
Kotlin 协程中有几种方法可以捕获和处理异常:
launch
使用 launch
启动协程时,异常会立即抛出。如果你不想让异常终止协程,可以用 try-catch
捕获异常:
scope.launch {
try {
codeThatCanThrowExceptions()
} catch (e: Exception) {
// Handle exception
}
}
async
使用 async
时,异常不会立刻抛出,只有在调用 await()
时,异常才会被抛出。处理 async
的异常,通常需要在 await()
调用时加上 try-catch
:
supervisorScope {
val deferred = async {
codeThatCanThrowExceptions()
}
try {
deferred.await()
} catch (e: Exception) {
// Handle exception thrown in async
}
}
CoroutineExceptionHandler
CoroutineExceptionHandler
是一个可选的 CoroutineContext
参数,可以帮助你处理未捕获的异常。它的作用类似于 Thread.UncaughtExceptionHandler
。
val handler = CoroutineExceptionHandler { context, exception ->
println("Caught $exception")
}
val scope = CoroutineScope(Job())
scope.launch(handler) {
launch {
throw Exception("Failed coroutine")
}
}
当 launch
的子协程发生异常时,CoroutineExceptionHandler
会捕获并处理该异常。
7. 总结
- 异常处理在协程中至关重要,正确地捕获和处理异常能避免应用崩溃,并提高用户体验。
- 使用
SupervisorJob
可以避免异常传播到父协程,确保兄弟协程不受影响。 CoroutineExceptionHandler
是处理未捕获异常的有力工具,可以捕获launch
类型的协程中的异常,但不能捕获async
类型协程的异常,除非在await
调用时捕获。
理解并正确使用这些工具将帮助你更好地管理协程中的异常,提供一个更加稳健和友好的应用程序。
🌟 关注我的CSDN博客,收获更多技术干货! 🌟