Kotlin协程异常处理

协程的上下文
  • Job:控制协程的生命周期
  • CoroutineDispatcher:向合适的线程分发任务
  • CoroutineName: 协程的名称,调试的时候很有用
  • CoroutineExceptionHandler:处理未捕捉的异常
组合上下文中的元素
  • 有时我们需要协程上下文中定义多个元素。我们可以使用+操作符来实现。比如说,我们可以显式指定一个调度器来启动协程并同时显式指定一个名称
fun testCoroutineContext()= runBlocking {
    launch(Dispatchers.Default + CoroutineName("test")){
        println("I'm working in thread ${Thread.currentThread().name}")
    }
}
协程上下文的继承
  • 对新创建的协程,它的CoroutineContext会包含一个全新的Job实例,它会帮助我们控制协程的生命周期。而剩下的元素会从CoroutineContext的父类继承,该父类可能是另外一个协程或者创建该协程的CoroutineScop
fun testCoroutineContextExtend()= runBlocking {
    val scope = CoroutineScope(Job() + Dispatchers.IO + CoroutineName("test"))
    val job = scope.launch {
        println("${coroutineContext[Job]} ${Thread.currentThread().name}")
        async {
            println("${coroutineContext[Job]} ${Thread.currentThread().name}")
        }
    }
    job.join()
}
  • 协程上下文 = 默认值 + 继承的CoroutineContext + 参数
  1. 一些元素包含默认值:Dispatchers.Default是默认的CoroutineDispatcher,以及”coroutine“作为默认的CoroutineName
  2. 继承CoroutineContext是CoroutineScope或者其父协程的CoroutineContext
  3. 传入协程构建器的参数的优先级高于继承的上下文参数,因此会覆盖对应的参数值
fun testCoroutineContextExtend2()= runBlocking {
    val coroutineHandler = CoroutineExceptionHandler{_,exception->
        println("Caught $exception")
    }
    val scope = CoroutineScope(Job() + Dispatchers.Main + coroutineHandler)
    val job = scope.launch(Dispatchers.IO) {//新协程
        println("${coroutineContext[Job]} ${Thread.currentThread().name}")
        async {
            println("${coroutineContext[Job]} ${Thread.currentThread().name}")
        }
    }
    job.join()
}
异常处理的必要性
  • 当应用出现一些异常情况时,给用户提供合适的体验非常重要,一方面,目睹应用崩溃是个很糟糕的体验,另一方面,在用户操作失败时,也必须要给会出正确的提示信息。
异常的传播
  • 协程构建器有两种形式:自动传播异常(launch与actor),向用户暴露异常(async和produce)当这些构建器用于创建一个根协程时(该协程不是另一个协程的子协程),前者这类构造器,异常会在它发生的第一时间被抛出,而后者则依赖用户来最终消费,例如通过await和receive
fun testExceptionPropagation() = runBlocking { 
    val job = GlobalScope.launch {  // 根协程
        try {
            throw IndexOutOfBoundsException()
        } catch(e: Exception) {
            println("Caught IndexOutOfBoundsException")
        }
    }
    job.join()
   val deferred = GlobalScope.async { 
        throw ArithmeticException()
    }
    try {
        deferred.await()
    } catch(e: Exception) {
        println("Caught ArithmeticException")
    }
}

根据以上代码可以看出launch在throw的时候直接会报出异常,而 async 没有被消费(没有执行await的时候)是不会有异常的

非根协程的异常
  • 其他协程所创建的协程中,产生的异常总是会被传播
fun testExceptionPropagation2() = runBlocking {
    val scop = CoroutineScope(Job())
    val job = scop.launch {
        async { // 非根协程,async如果有异常 launch就会立即抛出异常,不会调用await()
            throw IllegalArgumentException()
        }
    }
    job.join()
}
异常的传播特性
  • 当一个协程由于一个异常而运行失败时,它会传播这个异常并传递给它的父级,接下来父级会进行以下几步操作:
  1. 取消它的子协程
  2. 取消它自己
  3. 将异常传播并传递给它的父级
SupervisorJob
  • 使用SupervisorJob时,一个子协程的运行失败不会影响到它的子协程。SupervisorJob不会传播异常给它的父级,他会让子协程自己处理异常
  • 这种需求常见于在作用域内定义作业的UI组件,如果任何一个UI的子作业执行失败了,它并不是有必要取消整个UI组件,但是UI组件被销毁了,由于它的结果不再被需要了,它就有必要使所有的子作业执行失败
fun testSupervisorJob() = runBlocking {
    val scope = CoroutineScope(SupervisorJob())
    val job1 = scope.launch {
        delay(100)
        println("child 1")
        throw  IllegalArgumentException()
    }
    val job2 = scope.launch {
        try {
            delay(2000)
        } finally {
            println("child 2 finally")
        }
    }
   joinAll(job1,job2)
}
supervisorScope
  • 当作业自身执行失败的时候,所有子作业将会被全部取消
fun testSupervisorScope() = runBlocking {
    supervisorScope {
        launch {
            try {
                println("child 1")
                delay(10000)
            } finally {
                println("child 1 finally")
            }
//            throw  IllegalArgumentException()
        }
        yield()
        try {
            delay(2000)
        } finally {
            println("child 2 finally")
        }
        throw  IllegalArgumentException()
    }
}

以上代码中,在supervisorScope作用域里面抛出异常,子作业将会全部取消,而如果只在launch里面抛出异常,不会影响其他子协程

异常捕获
  • 使用CoroutineExceptionHanlder对协程的异常进行捕获
  • 以下条件满足时,异常就会捕获
  1. 时机:异常时被自动抛出异常的协程抛出时(使用launch,而不是async时)
  2. 位置:在CoroutineScope的CoroutineContext中或在一个根协程(CoroutineScope或者supervisorScope的字节子协程)中
fun testCoroutineExceptionHandler() = runBlocking {
    val coroutineHandler = CoroutineExceptionHandler { _, exception ->
        println("Caught $exception")
    }
    val job = GlobalScope.launch(coroutineHandler){
        throw AssertionError()
    }
}
fun testCoroutineExceptionHandler2() = runBlocking {
    val coroutineHandler = CoroutineExceptionHandler { _, exception ->
        println("Caught $exception")
    }
    val scope = CoroutineScope(Job())
    val job = scope.launch(coroutineHandler){
       async {
           throw  IllegalArgumentException()
       }
    }
    job.join()
}
Android中全局异常处理
  • 全局异常处理器可以获取到所有协程未处理的未捕获的异常,不过它并不能够对异常进行捕获,虽然不能阻止程序崩溃,全局异常处理器在程序调试和异常上报等场景中仍然有非常大的用处
  • 我们需要在classpath下面创建META-INF/server目录,并在其中创建一个名为kotlinx.coroutines.CoroutineExptionHandler的文件,文件内容就是我们全局异常处理器的全类名。


    0211221-141019.png

    另 创建一个类实现CoroutineExceptionHandler接口如下

class GlobalCoroutineExceptionHandler : CoroutineExceptionHandler {
    override val key: CoroutineContext.Key<*>
        get() = CoroutineExceptionHandler

    override fun handleException(context: CoroutineContext, exception: Throwable) {
         Log.e("lyb====","Unhandled coroutine Exception $exception")
    }
}

在新创建的kotlinx.coroutines.CoroutineExptionHandler的文件中间中写上GlobalCoroutineExceptionHandler类的全路径
这样如果有异常的话 就会打印 异常信息了 如下

findViewById

可见以上代码明显会出现异常,该全局异常处理器不会阻止程序崩溃,但是可以打印出异常信息,异常信息结果为

Unhandled Coroutine Exception:java.lang.StringIndexOutOfBoundsException:length=3;index=10
取消与异常
  • 取消与异常紧密相关,协程内部使用CancellationExption来进行取消,这个异常会被忽略
  • 当子协程取消时,不会取消它的父协程
fun testCancelAndException()= runBlocking {
  val job = launch {
       val child = launch {
           try {
               delay(Long.MAX_VALUE)
           } finally {
               println("Child is cancelled.")
           }
       }
       yield()
       println("cancelling child")
       child.cancelAndJoin()
       yield()
       println("parent is not_cancelled")
   }
   job.join()
}
  • 如果一个协程遇到CancellationExption以外的异常,它将使用该异常取消它的父协程。当父协程的所有子协程都结束后,异常才会被父协程处理
fun testCancelAndException2()= runBlocking {
   val coroutineHandler = CoroutineExceptionHandler { _, exception ->
       println("Caught $exception")
   }
   GlobalScope.launch(coroutineHandler){
       try {
           delay(Long.MAX_VALUE)
       } finally {
           withContext(NonCancellable){
               println("NonCancellable 1")
               delay(100)
               println("NonCancellable 2")
           }
       }
       launch { 
           delay(10)
           println("second child throws an exception")
           throw ArithmeticException()
       }
   }
}
异常聚合
  • 当协程的多个子协程因为异常而失败时,一般情况下取第一个异常进行处理。在第一个异常之后发生的其他异常,都会绑定在第一个异常之上
fun testExceptionAggregation()= runBlocking {
   val coroutineHandler = CoroutineExceptionHandler { _, exception ->
       println("Caught $exception----${exception.suppressed.contentToString()}")
   }
   val job = GlobalScope.launch(coroutineHandler){
       launch {
           try {
               delay(Long.MAX_VALUE)
           } finally {
               throw ArithmeticException()
           }
       }
       launch {
           try {
               delay(Long.MAX_VALUE)
           } finally {
               throw IllegalArgumentException()
           }
       }
       launch {
           delay(100)
           throw IOException()
       }
   }
   job.join()
}

你可能感兴趣的:(Kotlin协程异常处理)