Kotlin 协程上下文和异常处理

发布于:2024-05-06 ⋅ 阅读:(18) ⋅ 点赞:(0)
上下文是什么

CoroutineContext是一组用于定义协程行为的元素,包括以下几部分:

  • Job:控制协程的生命周期
  • CoroutineDispatcher:向合适的线程分发任务
  • CoroutineName:协程的名称,调试的时候很有用
  • CoroutineExceptionHandler:处理未被捕获的异常
  • 这几个部分可以通过"+"来组合
@Test
fun `test coroutine context`() = runBlocking {
    launch(Dispatchers.IO + CoroutineName("test")) {
        println("thread: ${Thread.currentThread().name}")
    }
}
协程上下文的继承
  • 对于新创建的协程,它的CoroutineContext会包含一个全新的Job实例,它会帮助我们控制协程的生命周期。
  • 剩下的元素会从CoroutineContext的父类继承,该父类可能是另外一个协程或者创建该协程的CoroutineScope

协程的上下文 = 默认值 + 继承的CoroutineContext + 参数

  • 一些元素包含默认值:Dispatchers.Default是默认的CoroutineDispatcher,以及“coroutine”作为默认的CoroutineName
  • 继承的CoroutineContext是CoroutineScope或是其父协程的CoroutineContext
  • 传入协程构建器的参数的优先级高于继承的上下文参数,因此会覆盖对应的参数值
@Test
fun `test coroutine context extend`() = runBlocking {
    val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable -> 
        println("handle exception: $throwable")
    }
    //scope有了新的CoroutineContext,和runBlocking不一样
    val scope = CoroutineScope(Job() + Dispatchers.Main + coroutineExceptionHandler)
    //job的CoroutineContext继承自scope,但是Job会是新的,每个协程都会有新的Job
    val job = scope.launch(Dispatchers.IO) { 
        //新协程
    }
}

由于传入协程构建器的参数优先级更高,所以job的调度器被覆盖,是Dispatchers.IO而不是父类的Dispatchers.Main

异常

异常的传播

协程构建器有2种传播形式:

  • 自动传播异常(launch和actor)、向用户暴露异常(async和produce)
  • 当这些构建器用于创建一个根协程时(该协程不是另一个协程的子协程),前者这类构建器异常发生时会第一时间被抛出,而后者则依赖用户来最终消费异常,例如通过调用await或receive
  • 非根协程产生的异常总是被传播
异常传播的特性

当一个协程由于一个异常而运行失败时,它会传播这个异常并传递给它的父级。接下来父级会进行下面几步操作:

  • 取消它自己的子级协程
  • 取消它自己
  • 将异常传播并传递给它的父级
SupervisorJob和SupervisorScope
  • 使用SupervisorJob时,一个子协程的运行失败不会影响其他的子协程,SupervisorJob不会传播异常给它的父级,它会让子协程自己处理异常
  • 或者SupervisorScope中的子协程,一个失败,其他的子协程也不会受影响,但如果是协程作用域里面有异常失败,则所有子协程都会失败退出

异常的捕获

  • 使用CoroutineExceptionHandler对协程的异常进行捕获
  • 时机:异常是被自动抛出异常的协程抛出的(使用launch,而不是async时)
  • 位置:在CoroutineScope的CoroutineContext中或在一个根协程中(CoroutineScope或者supervisorScope的直接子协程)中
  • handler要安装在外部协程中,不能在内部协程中,否则捕获不到异常
@Test
fun `test exception handler`() = runBlocking {
    val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
        println("handle exception: $throwable")
    }
    val scope = CoroutineScope(Job())
    //能捕获到异常
    val job1 = scope.launch(coroutineExceptionHandler) {
        launch { 
            throw IllegalArgumentException()
        }
    }
    val job2 = scope.launch() {
        //不能捕获到异常
        launch(coroutineExceptionHandler) { 
            throw IllegalArgumentException()
        }
    }
}
Android中全局异常处理
  • 全局异常处理器可以获取到所有协程未处理的未捕获异常,不过它不能对异常进行捕获。虽然不能阻止程序奔溃,全局异常处理器在程序调试和异常上报等场景中仍然有非常大的用处
  • 我们需要在classpath下面创建META-INF/services目录,并在其中创建一个名为kontlinx.coroutines.CoroutineExceptionHandler的文件,文件内容就是我们的全局异常处理器的全类名
class GlobeCoroutineExceptionHandler : CoroutineExceptionHandler {
    override val key = CoroutineExceptionHandler
    override fun handleException(context: CoroutineContext, exception: Throwable) {
        Log.d("xxx", "unHandle exception: $exception")
    }
}

然后再main目录下,新建resources/META-INF/services目录,然后新建kontlinx.coroutines.CoroutineExceptionHandler文件,内容为:

com.example.kotlincoroutine.GlobeCoroutineExceptionHandler
取消与异常处理
  • 取消与异常紧密相关,协程内部使用CancellationException来取消异常,但这个异常会被忽略
  • 当子协程被取消时,不会取消它的父协程
  • 如果一个协程遇到了CancellationException以外的异常,它将使用该异常取消它的父协程。当父协程的所有子协程都结束后,异常才会被父协程处理
//取消与异常
/*
* 打印顺序为:
* section 3
* section 1
* section 2
* handle exception:ArithmeticException
* 
* */
@Test
fun `test exception handler2`() = runBlocking {
    val handler = CoroutineExceptionHandler { _, throwable ->
        println("handle exception: $throwable")
    }
    val job = GlobalScope.launch(handler) {
        launch {
            try {
                delay(Long.MAX_VALUE)
            }finally {
                withContext(NonCancellable){
                    println("section 1")
                    delay(100)
                    println("section 2")
                }
            }
        }
        launch {
            delay(10)
            println("section 3")
            throw ArithmeticException()
        }
    }
    job.join()
}
异常的聚合
  • 当协程的多个子协程因为异常而失败时,一般情况下取第一个异常进行处理。在第一个异常之后发生的所有其他异常,都将被绑定到第一个异常之上。
  • 其他异常信息可以通过exception.suppressed.contentToString来打印出来

欢迎关注我的公众号查看更多精彩文章!

AntDream