携手创造,共同生长!这是我参加「日新计划 8 月更文应战」的第1天,点击检查活动详情

引言

关于协程的反常处理,一直以来都不是一个简略问题。由于触及到了许多方面,包含 反常的传递结构化并发下的反常处理 ,反常的传达办法 ,不同的Job 等,所以常常让许多(特别是刚运用协程的,也不乏内行)同学摸不着头脑。

常见有如下两种处理办法:

  • try catch
  • CoroutineExceptionHandler

但这两种办法(特别是第二种)究竟该什么时候用,用在哪里,却是一个问题?

比如尽管知道 CoroutineExceptionHandler ,但为什么添加了却还是溃散?究竟应该加在哪里? 尝试半响发现无解,终究又只能直接 try catch ,粗暴并有用,终究遇到此类问题,直接下意识 try 住。

try catch 尽管直接,必定程度上也帮咱们规避了许多运用方面的问题,但一起也埋下了许多坑,也便是说,并不是一切协程的反常都能够 try 住(取决于运用方位),其也不是任何场景的最优解。

鉴于此,本篇将从头到尾,协助你理清以下问题:

  • 什么是结构化并发?
  • 协程的反常传达流程与形式
  • 协程的反常处理办法
  • 为什么有些反常处理了却还是崩了
  • SupervisorJob 的运用场景
  • supervisorScopecoroutineScope
  • 反常处理办法的场景推荐

本文尽或许会用大白话与你分享了解,如有遗失或了解不当,也欢迎谈论区反应。

好了,让咱们开端吧!


结构化并发

在最开端前,咱们先搞清楚什么是 结构化并发,这对咱们了解协程反常的传递将非常有协助。

让咱们先将思路转为日常业务开发中,比如在某某业务中,或许存在好几个需求一起处理的逻辑,比如一起恳求两个网络接口,一起操作两个子使命等。咱们暂时称上述学术化概念为 多个并发操作

而每个并发操作其实都是在处理一个独自的使命,这个 使命 中,或许还存在 子使命 ; 相同对于这个子使命来说,它又是其父使命的子单元。每个使命都有自己的生命周期,子使命的生命周期会承继父使命的生命周期,比如假如父使命封闭,子使命也会被撤销。而假如满足这样特性,咱们就称其便是 结构化并发

在协程中,咱们常用的 CoroutineScope,正是根据这样的特性,即其也有自己的效果域与层级概念。

比如当咱们每次调用其扩展办法 launch() 时,这个内部又是一个新的协程效果域,新的效果域又会与父协程保持着层级联系,当咱们 撤销 CoroutineScope 时,其一切子协程也都会被封闭。

如下代码片段:

val scope = CoroutineScope(Job())
val jobA = scope.launch(CoroutineName("A")) {
   val jobChildA = launch(CoroutineName("child-A")) {
        delay(1000)
        println("xxx")
   }
  	// jobChildA.cancel()
}
val jobB = scope.launch(CoroutineName("B")) {
  			 delay(500)
        println("xxx")
}
// scope.cancel()

咱们界说了一个名为 scope 的效果域, 其中有两个子协程 jobA,B,一起 jobA 又有一个子协程 jobChildA。

假如咱们要撤销jobB,并不会影响jobA,其仍然会继续履行;

但假如咱们要撤销整个效果域时 scope.cancel(),jobA,jobB 都会被撤销,相应 jobA 被撤销时, 由于其也有自己的效果域,所以 jobChildA 也会被撤销,以此类推。而这便是协程的 结构化并发特性


反常传达流程

默许情况下,恣意一个协程产生反常时都会影响到整个协程树,而反常的传递通常是双向的,也即协程会向子协程与父协程共同传递,如下方所示:

Kotlin | 关于协程异常处理,你想知道的都在这里

全体流程如下:

  • 先 cancel 子协程
  • 撤销自己
  • 将反常传递给父协程
  • (重复上述进程,直到根协程封闭)

举个比如,比如下面这段代码:

Kotlin | 关于协程异常处理,你想知道的都在这里

在上图中,咱们创建了 两个子协程A,B,并在 A中 抛出反常,检查结果如右图所示, 当子协程A反常被停止时,咱们的子协程B与父协程都受到影响被停止。

当然假如不想在协程反常时,同级别子协程或许父协程受到影响,此刻就能够运用 SupervisorJob ,这个咱们放在下面再谈。


反常传达形式

在协程中,反常的传达形式有两种,一种是主动传达( launchactor),一种是向用户暴漏该反常( asyncproduce ),这两种的差异在于,前者的反常传递进程是层层向上传递(假如反常没有被捕获),而后者将不会向上传递,会在调用途直接暴漏。

记住上述思路对咱们处理协程的反常将会很有协助。

反常处理办法

tryCatch

一般而言, tryCath 是咱们最常见的处理反常办法,如下所示:

fun main() = runBlocking {
    launch {
        try {
            throw NullPointerException()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
    println("嘿害哈")
}

当反常产生时,咱们底部的输出仍然能正常打印,这也不难了解,就像咱们在 Android 或许 Java 中的运用相同。但有些时候这种办法并不必定能有用,咱们在下面中会专门提到。但大多数情况下,tryCatch 仍然如万金油一般,安稳且牢靠。


CoroutineExceptionHandler

其是用于在协程中全局捕获反常行为的最终一种机制,你能够了解为,相似 Thread.uncaughtExceptionHandler 相同。

但需求留意的是,CoroutineExceptionHandler 仅在未捕获的反常上调用,也即这个反常没有任何办法处理时(比如在源头tryCatch了),由于协程是结构化的,当子协程产生反常时,它会优先将反常委托给父协程区处理,以此类推 直到根协程效果域或许尖端协程 。因此其永久不会运用咱们子协程 CoroutineContext 传递的 CoroutineExceptionHandler(SupervisorJob 除外),对于 async 这种,而是直接向用户直接暴漏该反常,所以咱们在详细调用途直接处理就行。

如下示例所示:

 val scope = CoroutineScope(Job())
 scope.launch() {
     launch(CoroutineExceptionHandler { _, _ -> }) {
         delay(10)
         throw RuntimeException()
     }
 }

Kotlin | 关于协程异常处理,你想知道的都在这里

不难发现反常了,原因便是咱们的 CoroutineExceptionHandler 方位不是根协程或许 CoroutineScope 初始化时。

假如咱们改成下述办法,就能够正常处理该反常:

// 1. 初始化scope时
val scope = CoroutineScope(Job() + CoroutineExceptionHandler { _, _ -> })
// 2. 根协程
scope.launch(CoroutineExceptionHandler { _, _ -> }) { }

SupervisorJob

supervisorJob 是一个特殊的Job,其会改动反常的传递办法,当运用它时,咱们子协程的失利不会影响到其他子协程与父协程,浅显点了解便是:子协程会自己处理反常,并不会影响其兄弟协程或许父协程,如下图所示:

Kotlin | 关于协程异常处理,你想知道的都在这里

举个简略的比如:

val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, _ -> })
scope.launch(CoroutineName("A")) {
    delay(10)
    throw RuntimeException()
}
scope.launch(CoroutineName("B")) {
    delay(100)
    Log.e("petterp", "正常履行,我不会收到影响")
}

当协程A失利时,协程B仍然能够正常打印。

假如咱们将上述的示例改一下,会产生什么情况?如下所示:

val scope = CoroutineScope(CoroutineExceptionHandler { _, _ -> })
scope.launch(SupervisorJob()) {
    launch(CoroutineName("A")) {
        delay(10)
        throw RuntimeException()
    }
    launch(CoroutineName("B")) {
        delay(100)
        Log.e("petterp", "正常履行,我不会收到影响")
    }
}

猜一猜B协程内部的log能否正常打印?

结果是不能

为什么? 我不是现已运用了 SupervisorJob() 吗?咱们用一张图来看一下:

Kotlin | 关于协程异常处理,你想知道的都在这里

如上图所示,咱们在 scope.launch 时传递了 SupervisorJob ,看着似乎没什么问题,咱们希望的是 SupervisorJob 也会传递到子协程。但实则不会,由于子协程在 launch 时会创建新的协程效果域,其会运用默许新的 Job 代替咱们传递 SupervisorJob ,所以导致咱们传递的 SupervisorJob 被覆盖。所以假如咱们想让子协程不影响父协程或许其他子协程,此刻就必须再显现添加 SupervisorJob

正确的打开办法如下所示:

  scope.launch {
     launch(CoroutineName("A") + SupervisorJob()) {
         delay(10)
         throw RuntimeException()
     }
     launch(CoroutineName("B")) {
         delay(200)
         Log.e("petterp", "猜猜我还能不能打印")
     }
   }

总结如下:

SupervisorJob 能够用来改动咱们的协程反常传递办法,然后让子协程自行处理反常。但需求留意的是,由于协程具有结构化的特点,SupervisorJob 仅只能用于同一级别的子协程。假如咱们在初始化 scope 时添加了 SupervisorJob ,那么整个scope对应的一切 根协程 都将默许带着 SupervisorJob ,不然就必须在 CoroutineContext 显现带着 SupervisorJob


小测试

try不住的反常

如下的代码,能够 try 住吗?

 val scope = CoroutineScope(Job())
 try {
   	 //A
     scope.launch {
         throw NullPointerException()
     }
 } catch (e: Exception) {
     e.printStackTrace()
 }

答案是不会

为什么呢?我不是现已在 A 外面try了吗?

默许情况下,假如 反常没有被处理,并且尖端协程 CoroutineContext 中没有带着 CoroutineExceptionHandler ,则反常会传递给默许线程的 ExceptionHandler 。在 Android 中,假如没有设置 Thread.setDefaultUncaughtExceptionHandler , 这个反常将立即被抛出,然后导致引发App溃散。

咱们在 launch 时,由于启动了一个新的协程效果域,而新的效果域内部现已是新的线程(能够了解为),由于内部产生反常时由于没有被直接捕获 , 再加上其Job不是 SupervisorJob ,所以反常将向上开端传递,由于其本身现已是根协程,此刻根协程的 CoroutineContext 也没有带着 CoroutineExceptionHandler, 然后导致了直接反常。


CoroutinexxHandler 不收效?

下列代码中,添加的 CoroutineExceptionHandler 会收效吗?

val scope = CoroutineScope(Job())
scope.launch {
    val asyncA = async(SupervisorJob()+CoroutineExceptionHandler { _, _ -> }) {
        throw RuntimeException()
    }
    val asyncB = async(SupervisorJob()+CoroutineExceptionHandler { _, _ -> }) {
        throw RuntimeException()
    }
    asyncA.await()
    asyncB.await()
}

答案是: 不会收效

Tips: 假如你不是很了解 asyncCoroutineContext 里此刻为什么要加 SupervisorJob ,请看下面,会再做解说。

你或许会想,这还不简略吗,上面不是现已提过了,假如根协程或许scope中没有设置 CoroutineExceptionHandler,反常会被直接抛出,所以这儿肯定反常了啊。

假如你这样想了,恭喜答复正确~

那该怎么改一下上述示例呢?

scope 初始化时 或许 根协程里 加上 CoroutineExceptionHandler,或许直接 async 里边 try catch 都能够。那还有没有其他办法呢?

此处逗留10s 思考,loading…..

假如你还记住咱们最开端说过的反常的 传达形式 ,就会知道,对于 async 这种,在其反常时,其会主动向用户暴漏,而不是优先向上传递。

也便是说,咱们直接能够在 await()try Catch 。代码如下:

scope.launch {
    val asyncA = async(SupervisorJob()){}
    val asyncB = async(SupervisorJob()){}
   	val resultA = kotlin.runCatching { asyncA.await() }
   	val resultB = kotlin.runCatching { asyncB.await() }
}

runCatching 是 kotlin 中对于 tryCatch 的一种包装,其会将结果运用 Result 类进行包装,然后让咱们能更直观的处理结果,然后更加契合 kotlin 的语法习气。


Tips

为什么上述 async 里要添加 SupervisorJob() ,这儿再做一个解说。

val scope = CoroutineScope(Job())
scope.launch {
    val asyncA = async(SupervisorJob()) { throw RuntimeException()}
    val asyncB = async xxx
}

由于 async 时内部也是新的效果域,假如 async 对应的是根协程,那么咱们能够在 await() 时直接捕获反常。怎么了解呢?

如下示例:

val scope = CoroutineScope(Job())
// async 作为根协程
val asyncA = scope.async { throw NullPointerException() }
val asyncB = scope.async { }
scope.launch {
  	// 此刻能够直接tryCatch
    kotlin.runCatching {
        asyncA.await()
        asyncB.await()
    }
}

但假如 async 其对应的不是根协程(即不是 scope直接.async ),则会先将反常传递给父协程,然后导致反常没有在调用途暴漏,咱们的tryCatch 天然也就无法阻拦。假如此刻咱们为其添加 SupervisorJob() ,则标志着其不会主动传递反常,而是由该协程自行处理。所以咱们能够在调用途(await()) 捕获。


相关扩展

supervisorScope

官方解说如下:

运用 SupervisorJob 创建一个 CoroutineScope 并运用此范围调用指定的挂起块。供给的效果域从外部效果域承继其coroutineContext ,但用 SupervisorJob 覆盖上下文Job 。一旦给定块及其一切子协程完结,此函数就会回来。

浅显点便是,咱们帮你创建了一个 CoroutineScope ,初始化效果域时,运用 SupervisorJob 代替默许的Job,然后将其的效果域扩展至外部调用。如下代码所示:

val scope = CoroutineScope(CoroutineExceptionHandler { _, _ -> })
scope.launch() {
    supervisorScope {
      	// launch A ❎
        launch(CoroutineName("A")) {
            delay(10)
            throw RuntimeException()
        }
				// launch B 
        launch(CoroutineName("B")) {
            delay(100)
            Log.e("petterp", "正常履行,我不会收到影响")
        }
    }
}

supervisorScope 里的一切子协程履行完结时,其就会正常退出效果域。

需求留意的是,supervisorScope 内部的 JobSupervisorJob ,所以当效果域中子协程反常时,反常不会主动层层向上传递,而是由子协程自行处理,所以意味着咱们也能够为子协程添加 CoroutineExceptionHandler 。如下所示:

Kotlin | 关于协程异常处理,你想知道的都在这里

当子协程反常时,由于咱们运用了 supervisorScope ,所以反常此刻不会主动传递给外部,而是由子类自行处理。

当咱们在内部 launch 子协程时,其实也便是相似 scope.launch ,所以此刻子协程A相也便是根协程,所以咱们运用 CoroutineExceptionHandler 也能够正常阻拦反常。但假如咱们子协程不添加 CoroutineExceptionHandler ,则此刻反常会被supervisorScope 抛出,然后被外部的 CoroutineExceptionHandler 阻拦(也便是初始化scope效果域时运用的 ExceptionHandler)。

相应的,与 supervisorScope 相似的,还有一个 coroutineScope ,下面咱们也来说一下这个。


coroutineScope

其主要用于并行分化协程子使命时而运用,当其范围内任何子协程失利时,其一切的子协程也都将被撤销,一旦内部一切的子协程完结,其也会正常回来。

如下示例:

Kotlin | 关于协程异常处理,你想知道的都在这里

当子协程A 反常未被捕获时,此刻 子协程B 和整个 协程效果域 都将被反常撤销,此刻反常将传递到尖端 CoroutineExceptionHandler

场景推荐

严厉意义上来说,一切反常都能够用 tryCatch 去处理,只要咱们的处理方位得当。但这并不是一切办法的最优解,特别是假如你想更优雅的处理反常时,此刻就能够考虑 CoroutineExceptionHandler 。下面咱们通过实践需求来举例,然后体会反常处理的的一些实践。

什么时候该用 SupervisorJob ,什么时候该用 Job?

引用官方的一句话便是:想要防止撤销操作在反常产生时被传达,记住运用 SupervisorJob ;反之则运用 Job

对于一个普通的协程,如何处理我的反常?

对于一个普通的协程,你能够在其协程效果域内运用 tryCatch(runCatching) ,假如其是根协程,你也能够运用 CoroutineExceptionHandler 作为最终的阻拦手法 ,如下所示:

val scope = CoroutineScope(Job())
scope.launch {
    runCatching { }
}
scope.launch(CoroutineExceptionHandler { _, throwable -> }) {  
}

在某个子协程中,想运用 SupervisorJob 的特性去作为某个效果域去履行?

val scope = CoroutineScope(Job())
scope.launch(CoroutineExceptionHandler { _, _ -> }) {
    supervisorScope {
        launch(CoroutineName("A")) {
            throw NullPointerException()
        }
        launch(CoroutineName("B")) {
            delay(1000)
            Log.e("petterp", "仍然会正常履行")
        }
    }
}

SupervisorJob+tryCatch

咱们有两个接口 A,B 需求一起恳求,当接口A反常时,需求不影响B接口的正常展现,当接口B反常时,此刻界面展现反常信息。伪代码如下:

val scope = CoroutineScope(Job())
scope.launch {
    val jobA = async(SupervisorJob()) {
        throw NullPointerException()
    }
    val jobB = async(SupervisorJob()) {
        delay(100)
        1
    }
    val resultA = kotlin.runCatching { jobA.await() }
    val resultB = kotlin.runCatching { jobB.await() }
}

CoroutineExceptionHandler+SupervisorJob

假如你有一个尖端协程,并且需求主动捕获一切的反常,则此刻能够选用上述办法,如下所示:

val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
    Log.e("petterp", "主动捕获一切反常")
}
val ktxScope = CoroutineScope(SupervisorJob() + exceptionHandler)

参考

  • 协程中的撤销和反常 | 反常处理详解
  • 什么是 结构化并发 ?
  • 「Kotlin篇」多方位处理协程的反常

关于我

我是Petterp,一个三流开发,假如本文对你有所协助,欢迎点赞谈论,你的支持是我继续创造的最大鼓励!