Kotlin 协程的异常处理

发布于:2024-12-06 ⋅ 阅读:(132) ⋅ 点赞:(0)

1. 一个协程突然失败了!我该怎么办?

在协程中,如果某个协程发生异常,它会传播到其父级协程,导致父协程和所有子协程的取消。这种异常传播机制在有些场景下非常有用,但也可能带来一些问题。

协程异常传播的基本流程:

  1. 子协程发生异常,异常会传播到父级协程。
  2. 父协程取消其他子协程。
  3. 父协程自己也会被取消,并且异常继续向上传播。

问题:如果某个子协程失败,整个父协程及其他兄弟协程都会被取消,这会影响其他正常工作的协程。

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 是谁?

协程的父协程是在启动协程时通过 CoroutineScopesupervisorScope 设置的。当你使用 SupervisorJob 创建协程时,这个 SupervisorJob 的行为仅在它是父协程时才有效。如果将它作为构造器参数传递给其他协程,它就不会发挥作用。

示例:

val scope = CoroutineScope(Job())

scope.launch(SupervisorJob()) {
    // Parent is Job, not SupervisorJob
    launch {
        // Child 1
    }

    launch {
        // Child 2
    }
}

注意:SupervisorJob 只有在它作为 CoroutineScope 的一部分时才会生效。

5. 底层原理

如果你对 JobSupervisorJob 的工作原理感兴趣,可以查看 JobSupport.kt 文件中的 childCancellednotifyCancelling 函数的实现。在 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博客,收获更多技术干货! 🌟


网站公告

今日签到

点亮在社区的每一天
去签到