Kotlin 协程异常处理

297 阅读4分钟

kotlin-for-android-text.svg

协程异常处理

1、协程之间的关系

多个并发操作:多个需要同时处理的任务

结构化并发操作:CoroutineScope中通过launch启用一个子任务,当父协程取消时,其所有子协程也都会关闭,单独取消一个子协程并不会影响其他子协程的执行

异常流程:当子协程crash时,会传递给父协程,且取消掉子协程和父协程

  • 先 cancel 子协程
  • 取消自己
  • 将异常传递给父协程
  • (重复上述过程,直到根协程关闭)

2、异常传播形式

和java 异常传播类似

  • 自动传播:层层向上传递且未被捕获( launch 或 actor)
  • 向用户暴露该异常:直接暴露由调用者处理( async 或 produce )

3、处理方式

  • try catch或runCatching 直接捕获异常,大多数情况都可以处理,除了部分特殊异常(InvocationTargetException)如
   fun main() = runBlocking {
    launch {
        try {
            throw NullPointerException()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
    println("11111")
   }
  • try catch也不能try住的如:
val scope = CoroutineScope(Job())
try {
    //新开启子协程
    scope.launch {
        throw NullPointerException()
    }
} catch (e: Exception) {
    e.printStackTrace()
}

Tip:

  1. launch默认情况下,如果 异常没有被处理,而且顶级协程 CoroutineContext 中没有携带 CoroutineExceptionHandler ,则异常会传递给默认线程的 ExceptionHandler 。在 Android 中,如果没有设置 Thread.setDefaultUncaughtExceptionHandler , 这个异常将立即被抛出,从而导致引发App崩溃
  2. 对于scope.launch/async 都是启动子协程,launch/aysnc未传入SupervisorJob(即子协程不自己处理异常),launch会向上抛异常,而aysnc则直接暴露异常。 2)CoroutineExceptionHandler 用于全局捕获异常 3)SupervisorJob 子协程会自己处理异常,并不会影响其兄弟协程或者父协程 supervisorJob 是一个特殊的Job,其会改变异常的传递方式,当使用它时,我们子协程的失败不会影响到其他子协程与父协程, 通俗点理解就是:子协程会自己处理异常,并不会影响其兄弟协程或者父协程 SupervisorJob 可以用来改变我们的协程异常传递方式,从而让子协程自行处理异常。 但需要注意的是,因为协程具有结构化的特点,SupervisorJob 仅只能用于同一级别的子协程而不会向下传递。 如果我们在初始化 scope 时添加了 SupervisorJob ,那么整个scope对应的所有 根协程 都将默认携带 SupervisorJob ,否则就必须在 CoroutineContext 显示携带 SupervisorJob。 如:
                   val scope = CoroutineScope(CoroutineExceptionHandler { _, _ -> })
                   scope.launch {
                          launch(CoroutineName("A") + SupervisorJob()) {
                               delay(10)
                               throw RuntimeException()
                           }
                           launch(CoroutineName("B")) {
                               delay(200)
                               Log.e("petterp", "猜猜我还能不能打印")
                           }
                   }

4、应用:

A:普通场景用于防止出现普通异常的情况如类型转换,json转换等等,可以直接使用try catch/runCatching 处理也可以直接在顶层协程加入CoroutineExceptionHandler处理异常

B:SupervisorJob用于单独某个协程出现异常不影响其他兄弟协程继续执行的情况如首页加载多个接口, 但是不希望因为某个接口异常导致所有的协程被取消

                /**
                  * SupervisorJob+tryCatch用于比如我们某个界面需要获取列表,需要获取用户权限,
                  * 但是我们需要列表正常获取,当用户权限接口异常的时候弹窗提示用户权限不足且要取消获                   * 取列表
                  */
                private fun testSupervisorJobAndTryCatch() {
                   val coroutineScope = CoroutineScope(Job())
                   coroutineScope.launch {
                   val jobGetList = async(SupervisorJob()) {
                       Log.i(TAG, "testSupervisorJobAndTryCatch jobA:延迟1000")
                       delay(5000)
                       Log.i(TAG, "testSupervisorJobAndTryCatch jobA:异常")
                       throw NullPointerException()
                   }
                   val jobUserPermission = async(SupervisorJob()) {
                       Log.i(TAG, "testSupervisorJobAndTryCatch jobB:开始")
                       delay(100)
                       Log.i(TAG, "testSupervisorJobAndTryCatch jobB:异常")
                       throw NullPointerException()
                       Log.i(TAG, "testSupervisorJobAndTryCatch jobB:结束")
                       200
                   }
                   // 不能先jobA.await()然后在jobB.await() 这样会导致不能取消jobA
                   // async 是并行的但是外面是串行的会先等待jobA的结果
                   val resultB = kotlin.runCatching { jobUserPermission.await() }
                   Log.i(TAG, "testSupervisorJobAndTryCatch resultB=$resultB")
                   if (resultB.isFailure) {
                       jobGetList.cancel()
                       Log.i(TAG, "testSupervisorJobAndTryCatch jobA:cancel")
                   }
                   val resultA = kotlin.runCatching { jobGetList.await() }
                   Log.i(TAG, "testSupervisorJobAndTryCatch resultA=$resultA")
                  }
                }

C:SupervisorJob+tryCatch用于比如我们某个界面需要获取列表,需要获取用户权限, 但是我们需要列表正常获取,当用户权限接口异常的时候弹窗提示用户权限不足且要取消列表

D:CoroutineExceptionHandler+SupervisorJob需要顶层协程捕获所有异常,子协程处理自己的异常则使用这种方式

val scope = CoroutineScope(CoroutineExceptionHandler { _, _ -> })