跳至主要內容

CoroutineExceptionHandler

guodongAndroid大约 5 分钟

CoroutineExceptionHandler

在最后一个示例中,你可以看到 IndexOutOfBoundsException 被打印到控制台。你可以通过创建自定义 CoroutineExceptionHandler 来自定义该行为,该处理器作为根协程及其子协程的通用 catch 块。它类似于Thread.uncaughtExceptionHandler,这意味着你无法通过使用它从异常中恢复。

需要注意的是,CoroutineExceptionHandler 仅捕获未以任何其他方式处理的异常,即:未捕获的 异常。

通常,未捕获的异常只能由使用 launch 协程构建器创建的协程产生。使用 async 创建的协程捕获其所有异常,并在生成的 Deferred 对象中表示它们。

提示

注意: 在某些情况下,async 会传播异常,你将无法在 catch 块中捕获它。

稍后我们再讨论它。

在你的 ExceptionHandling 入门项目中打开 GlobalExceptionHandler.kt 文件并将下面代码替换其中空的 main 函数:

@OptIn(DelicateCoroutinesApi::class)
fun main() {
  runBlocking {
	// 1
    val exceptionHandler = CoroutineExceptionHandler { _, exception ->
      println("Caught $exception")
    }
	// 2
    val job = GlobalScope.launch(exceptionHandler) {
      throw AssertionError("My Custom Assertion Error!")
    }
	// 3
    val deferred = GlobalScope.async(exceptionHandler) {
      // Nothing will be printed,
      // relying on user to call deferred.await()
      throw ArithmeticException()
	}
	// 4
    // This suspends current coroutine until all given jobs are complete.
    joinAll(job, deferred)
  }
}

输出:

Caught java.lang.AssertionError: My Custom Assertion Error!

以下是对代码块的解析:

  1. 实现一个全局异常处理器,比如:CoroutineExceptionHandler。这就是你定义如何处理未捕获异常的地方。
  2. 使用 launch 协程构建器创建了一个简单的协程,然后在其中抛出一个自定义信息的 AssertionError
  3. 使用 async 协程构建器创建了一个简单的协程,然后在其中抛出一个 ArithmeticException
  4. joinAll 用于挂起当前协程,直到给定的任务全部完成。

当你想在协程之间共享全局异常处理器时,CoroutineExceptionHandler 非常有用,但如果你想以不同的方式处理特定协程的异常,则需要提供特定的实现。让我们看看你如何做到这一点。

提示

注意: CoroutineExceptionHandler 仅在用户期望不会处理的异常上调用,因此在 async 协程构建器中注入它看起来没有效果。

Try-Catch 救场

当涉及到处理特定协程的异常时,你可以使用 try/catch 块来捕获异常,并像在使用 Kotlin 进行正常同步编程时一样处理它们。

不过有一个陷阱。如果不小心,使用 async 协程构建器创建的协程通常会吞下异常。如果一个异常在 async 块执行期间抛出,则这个异常不会立即抛出。相反,它会在你调用返回的 Deferred 对象的 await() 函数时抛出。如果不考虑此行为,可能会导致无法跟踪异常的情况。另一方面,根据实际情况,将异常处理推迟到稍后的时间点也可能是理想的行为。

下面是一个例子来演示。在你的项目中打开 TryCatch.kt 文件并将以下代码替换到其空的 main 函数中:

@OptIn(DelicateCoroutinesApi::class)
fun main() {
  runBlocking {
    // Set this to 'true' to call await on the deferred variable
    val callAwaitOnDeferred = false
    val deferred = GlobalScope.async {
      // This statement will be printed with or without a call to await()
      println("Throwing exception from async")
      throw ArithmeticException("Something Crashed")
      // Nothing is printed, relying on a call to await()
	}
    if (callAwaitOnDeferred) {
      try {
        deferred.await()
      } catch (e: ArithmeticException) {
        println("Caught ArithmeticException")
      }
	}
  }
}

callAwaitOnDeferred 设置为 false 情况下的输出——即没有调用 await

Throwing exception from async

callAwaitOnDeferred 设置为 true 情况下的输出——即调用 await

Throwing exception from async
Caught ArithmeticException

处理多个子协程异常

只有一个协程是一个理想的情况。在实践中,你可能有多个协程,其他子协程在它们下面运行。如果这些子协程抛出异常,会发生什么?一切都将可能变得棘手。在这种情况下,一般是 第一个异常胜出。如果你注入了一个 CoroutineExceptionHandler,它将只处理第一个异常,而抑制所有其他的异常。

为了证明这点,在本章的入门项目中打开 ExceptionHandlingForChild.kt 文件并将以下代码替换到其空的 main 函数中:

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
  // Global Exception Handler
  val handler = CoroutineExceptionHandler { _, exception ->
    println("Caught $exception with suppressed ${exception.suppressed?.contentToString()}")
  }
  // Parent Job
  val parentJob = GlobalScope.launch(handler) {
    // Child Job 1
    launch {
      try {
        delay(Long.MAX_VALUE)
      } catch (e: Exception) {
        println("${e.javaClass.simpleName} in Child Job 1")
	  } finally {
        throw ArithmeticException()
      }
	}
	// Child Job 2
    launch {
      delay(100)
      throw IllegalStateException()
	}
    // Delaying the parentJob
    delay(Long.MAX_VALUE)
  }
  // Wait until parentJob completes
  parentJob.join()
}

输出:

JobCancellationException in Child Job 1
Caught java.lang.IllegalStateException with suppressed [java.lang.ArithmeticException]

在上面的示例中:

  • 你定义了一个 CoroutineExceptionHandler 来打印捕获的第一个异常的名称,以及从它的 suppressed 属性中获取的被抑制的异常。
  • 然后,你将这个异常处理器作为 launch 协程构建器的参数启动了一个父协程。这个父协程包含几个子协程,你再次使用 launch 函数启动这些子协程。其中第一个子协程包含一个 try-catch-finally 块。
  • try 块中,你调用非常大参数值的 delay 函数,以便等待很长时间。
  • catch 块中,你打印了捕获异常的信息。
  • finally 块中,你抛出了一个 ArithmeticException
  • 在第二个子协程中,你延迟了几毫秒然后抛出了一个 IllegalStateException
  • 然后完成父协程,在另一段长时间内调用 delay 函数。
  • main 函数的最后是允许程序等待 parentJob 的完成。

当你运行此代码时,父协程启动,其子协程也会启动。第一个子协程等待时,第二个子协程抛出一个 IllegalStateException,这是异常处理器管理的第一个异常,正如你在输出中看到的那样。因此,系统强制取消第一个子协程中的 delay,这就是 JobCancellationException 信息的原因。同时这也导致父协程的失败,因此,将调用异常处理器并显示其输出。

需要注意的是,CoroutineExceptionHandler 是父协程的一部分,因此它管理与之相关的异常。