答案是: 不会生效
Tips: 如果你不是很理解 async 的 CoroutineContext 里此时为什么要加 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 内部的 Job 为 SupervisorJob ,所以当作用域中子协程异常时,异常不会主动层层向上传递,而是由子协程自行处理,所以意味着我们也可以为子协程增加 CoroutineExceptionHandler 。如下所示:
当子协程异常时,因为我们使用了 supervisorScope ,所以异常此时不会主动传递给外部,而是由子类自行处理。
当我们在内部 launch 子协程时,其实也就是类似 scope.launch ,所以此时子协程A相也就是根协程,所以我们使用 CoroutineExceptionHandler 也可以正常拦截异常。但如果我们子协程不增加 CoroutineExceptionHandler ,则此时异常会被supervisorScope 抛出,然后被外部的 CoroutineExceptionHandler 拦截(也就是初始化scope作用域时使用的 ExceptionHandler)。
相应的,与 supervisorScope 相似的,还有一个 coroutineScope ,下面我们也来说一下这个。
coroutineScope
其主要用于并行分解协程子任务时而使用,当其范围内任何子协程失败时,其所有的子协程也都将被取消,一旦内部所有的子协程完成,其也会正常返回。
如下示例:
当子协程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)