MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin协程异常处理

2021-08-242.0k 阅读

Kotlin 协程异常处理基础

在 Kotlin 协程中,异常处理是确保程序健壮性和稳定性的重要环节。当协程执行过程中出现异常时,我们需要妥善处理这些异常,以避免程序崩溃或出现未预期的行为。

首先,让我们看一个简单的协程示例,其中会抛出异常:

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        throw RuntimeException("这是一个测试异常")
    }
}

在上述代码中,我们通过 launch 创建了一个协程,并在其中直接抛出了一个 RuntimeException。运行这段代码,你会发现控制台会打印出异常堆栈信息,并且 runBlocking 会终止执行。

try - catch 捕获协程异常

在 Kotlin 协程中,我们可以像在普通代码中一样使用 try - catch 块来捕获异常。示例如下:

import kotlinx.coroutines.*

fun main() = runBlocking {
    try {
        launch {
            throw RuntimeException("这是一个测试异常")
        }
    } catch (e: Exception) {
        println("捕获到异常: $e")
    }
}

然而,上述代码并不能捕获到协程内部抛出的异常。这是因为 launch 创建的协程是异步执行的,try - catch 块在协程启动后就会继续执行,而不会等待协程完成。如果协程抛出异常,它会直接传播出去,而不会被外层的 try - catch 捕获。

要正确捕获 launch 协程中的异常,我们可以使用 join 方法等待协程执行完毕,示例如下:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        throw RuntimeException("这是一个测试异常")
    }
    try {
        job.join()
    } catch (e: Exception) {
        println("捕获到异常: $e")
    }
}

在这个例子中,job.join() 会阻塞当前协程,直到 launch 创建的协程执行完毕。这样,当协程抛出异常时,try - catch 块就能捕获到异常了。

使用 CoroutineExceptionHandler 处理异常

CoroutineExceptionHandler 是 Kotlin 协程提供的一种更灵活的异常处理方式。它允许我们为一个或多个协程设置统一的异常处理器。

import kotlinx.coroutines.*

val exceptionHandler = CoroutineExceptionHandler { _, exception ->
    println("捕获到异常: $exception")
}

fun main() = runBlocking {
    launch(exceptionHandler) {
        throw RuntimeException("这是一个测试异常")
    }
}

在上述代码中,我们创建了一个 CoroutineExceptionHandler,并将其传递给 launch。当协程抛出异常时,CoroutineExceptionHandler 中的代码块会被执行,从而处理异常。

父子协程的异常处理

在 Kotlin 协程中,父子协程之间的异常传播和处理有其特定的规则。

父协程捕获子协程异常

当一个父协程创建了多个子协程时,父协程可以捕获子协程抛出的异常。示例如下:

import kotlinx.coroutines.*

fun main() = runBlocking {
    try {
        coroutineScope {
            launch {
                throw RuntimeException("子协程异常")
            }
        }
    } catch (e: Exception) {
        println("父协程捕获到异常: $e")
    }
}

在上述代码中,coroutineScope 创建了一个协程作用域,在这个作用域内创建的 launch 协程就是子协程。当子协程抛出异常时,父协程(coroutineScope 所在的协程)可以通过 try - catch 块捕获到异常。

子协程异常对父协程的影响

子协程抛出异常时,默认情况下会导致父协程取消。例如:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = coroutineScope {
        launch {
            throw RuntimeException("子协程异常")
        }
        launch {
            delay(1000)
            println("这个协程会执行吗?")
        }
    }
    try {
        job.join()
    } catch (e: Exception) {
        println("捕获到异常: $e")
    }
}

在上述代码中,第一个 launch 子协程抛出异常后,父协程会被取消,第二个 launch 子协程也会随之取消,因此 "这个协程会执行吗?" 不会被打印。

自定义子协程异常处理策略

我们可以通过 SupervisorJob 来改变子协程异常对父协程的影响。SupervisorJob 创建的子协程异常不会导致父协程取消。示例如下:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val supervisor = SupervisorJob()
    val scope = CoroutineScope(supervisor + Dispatchers.Default)
    scope.launch {
        throw RuntimeException("子协程异常")
    }
    scope.launch {
        delay(1000)
        println("这个协程会执行")
    }
    delay(2000)
}

在上述代码中,通过 SupervisorJob 创建的子协程,其中一个子协程抛出异常不会影响其他子协程的执行。"这个协程会执行" 会被打印出来。

全局协程的异常处理

全局协程是通过 GlobalScope 创建的协程,它的生命周期与应用程序相同。全局协程的异常处理与普通协程有所不同。

全局协程异常默认处理

当全局协程抛出异常时,默认情况下,异常会被打印到标准错误输出,并且不会被捕获。示例如下:

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch {
        throw RuntimeException("全局协程异常")
    }
    Thread.sleep(1000)
}

运行上述代码,你会在控制台看到异常堆栈信息,但由于没有捕获异常,可能会导致应用程序出现不稳定的情况。

为全局协程设置异常处理器

我们可以为全局协程设置 CoroutineExceptionHandler 来处理异常。示例如下:

import kotlinx.coroutines.*

val globalExceptionHandler = CoroutineExceptionHandler { _, exception ->
    println("全局协程捕获到异常: $exception")
}

fun main() {
    GlobalScope.launch(globalExceptionHandler) {
        throw RuntimeException("全局协程异常")
    }
    Thread.sleep(1000)
}

在上述代码中,我们为 GlobalScope.launch 设置了 CoroutineExceptionHandler,这样当全局协程抛出异常时,就能被捕获并处理。

异步流(Flow)中的异常处理

Kotlin 中的 Flow 是一种异步数据流,它也涉及到异常处理。

Flow 中异常的传播

Flow 发射数据过程中抛出异常时,异常会沿着流传播。示例如下:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    flow {
        emit(1)
        throw RuntimeException("流中的异常")
        emit(2)
    }.collect { value ->
        println("收到值: $value")
    }
}

在上述代码中,flow 在发射 1 之后抛出异常,collect 块在收到 1 之后会捕获到异常并终止。

使用 catch 操作符处理 Flow 异常

Flow 提供了 catch 操作符来处理流中的异常。示例如下:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    flow {
        emit(1)
        throw RuntimeException("流中的异常")
        emit(2)
    }.catch { exception ->
        println("捕获到流中的异常: $exception")
    }.collect { value ->
        println("收到值: $value")
    }
}

在这个例子中,catch 操作符捕获了 flow 中抛出的异常,collect 块不会因为异常而终止,并且异常被处理。

Flow 与协程作用域的异常处理结合

Flow 在协程作用域中使用时,我们可以结合协程作用域的异常处理机制。示例如下:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    try {
        coroutineScope {
            flow {
                emit(1)
                throw RuntimeException("流中的异常")
                emit(2)
            }.collect { value ->
                println("收到值: $value")
            }
        }
    } catch (e: Exception) {
        println("协程作用域捕获到异常: $e")
    }
}

在上述代码中,coroutineScope 捕获了 Flow 中抛出的异常,实现了更全面的异常处理。

异常处理的最佳实践

在实际应用中,为了确保 Kotlin 协程的健壮性和稳定性,以下是一些异常处理的最佳实践。

明确异常处理范围

在编写协程代码时,要明确哪些部分可能会抛出异常,并在合适的层次进行处理。例如,对于业务逻辑相关的异常,应该在业务逻辑所在的协程或其上层协程进行处理;对于底层技术相关的异常,如网络请求失败,可以在专门处理网络的协程模块中处理。

避免过度捕获异常

虽然捕获异常可以防止程序崩溃,但过度捕获异常可能会隐藏真正的问题。只捕获你能够处理的异常,对于无法处理的异常,让其继续传播,以便在更高层次进行统一处理或记录。

记录异常信息

在处理异常时,要记录详细的异常信息,包括异常类型、堆栈跟踪等。这有助于调试和排查问题。可以使用日志框架,如 Log4jSLF4J 来记录异常信息。

进行适当的恢复操作

在捕获到异常后,根据业务需求进行适当的恢复操作。例如,在网络请求失败时,可以尝试重新请求;在数据库操作失败时,可以进行数据回滚等。

通过遵循这些最佳实践,可以使 Kotlin 协程的异常处理更加有效和可靠,提高整个应用程序的质量。

总结 Kotlin 协程异常处理要点

  1. 基本异常捕获:普通的 try - catch 块无法直接捕获 launch 协程异步抛出的异常,需要结合 join 方法等待协程完成才能捕获。
  2. CoroutineExceptionHandler:是一种灵活的异常处理方式,可以为一个或多个协程设置统一的异常处理器。
  3. 父子协程:子协程异常默认会导致父协程取消,可通过 SupervisorJob 改变这种行为。
  4. 全局协程:默认情况下,全局协程异常打印到标准错误输出,可通过设置 CoroutineExceptionHandler 进行处理。
  5. FlowFlow 中的异常会沿着流传播,可使用 catch 操作符处理,也可结合协程作用域的异常处理机制。
  6. 最佳实践:明确异常处理范围,避免过度捕获,记录异常信息并进行适当恢复操作。

掌握这些 Kotlin 协程异常处理的知识和技巧,能够帮助开发者编写更加健壮、稳定的异步代码,提升应用程序的质量和可靠性。在实际开发中,应根据具体的业务场景和需求,合理选择和运用这些异常处理方法。