Kotlin协程异常传递机制
Posted 冬天的毛毛雨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin协程异常传递机制相关的知识,希望对你有一定的参考价值。
先说结论:
-
异常传播机制的前提条件是建立一棵Job树
-
当发生异常时,当前Job开始沿着树向上逐级询问父Job,它是否要处理
-
父Job B如果要处理,就没子Job A什么事了。此时,Job B它自己就成了子Job B,继续去问它的父Job C,一路问上去,直接找到一个愿意处理异常的Job。而所谓的愿意愿意处理的Job, 其实就是到达顶层Job, 它要是不处理, 那就没人能处理了, 不论怎样就它了.
-
父Job不愿意处理, 那就只好由子Job自己处理. 所谓的父Job不愿意处理, 有这几种情况:
(1) 异常类型是CancellationException, 你取消就取消了呗, 多大点事, 你自己收尾
(2) 父Job是SupervisorJob, 他会重写一个方法: childCancelled(ex) = false, 它告诉子Job, 不管是什么异常, 我都不管, 你自己处理.
-
不管异常是上报给父Job处理还是留着自己处理, 反正最后一定会有一个Job负责处理, 它会通过JobNode.CompletionHandler向下取消自己的所有子Job.
异常处理的入口
这个流程基本上是先看了一堆别人的博客, 然后自己边看边猜的
AbstractCoroutine.resumeWith(Result<T>) ->
JobSupport.makeCompletingOnce(result.toState()) ->
.tryMakeCompleting(...) ->
.tryMakeCompletingSlowPath() ->
.finalizeFinishingState() ->
// 最终的核心方法
(1) val handled = cancelParent(finalException) || handleJobException(finalException)
cancelParent 沿树向上找到一个愿意处理此异常的Job对象
handleJobException 在没有其他Job愿意处理时, 就只能自己处理
(2) completeStateFinalization() -> (state as CompletionHandler).invoke(cause)
把自己的所有子Job也都结束掉
cancelParent 沿树向上找到一个愿意处理此异常的Job对象
先假设没有SupervisorJob, 一路上全是Job, 而且也不是取消
private fun cancelParent(cause: Throwable): Boolean
...
// 假设不是取消
val isCancellation = cause is CancellationException
val parent = parentHandle
// No parent -- ignore CE, report other exceptions.
if (parent === null || parent === NonDisposableHandle)
return isCancellation = false
return parent.childCancelled(cause) || isCancellation = false
进入 parent.childCancelled(cause)
parent对象在构建Job树时有提到, 在说到SupervisorJob时需要再强调一遍, 此处沿着Job树往上找就可以了.
public open fun childCancelled(cause: Throwable): Boolean
if (cause is CancellationException) return true // 假设不是取消
return cancelImpl(cause) && handlesException
此时进入parent对象中, 需要再次强调一遍, 面向对象的树状结构, 要时刻注意当前是在哪个对象上.
cancelImpl()中的代码看不懂, 有一大堆的state, 只能凭感觉猜测 ->
makeCancelling(cause) ->
tryMakeCompleting(...) ->
tryMakeCompletingSlowPath() ->
finalizeFinishingState() ->
最终又回到了
cancelParent(finalException) || handleJobException(finalException)
然后就这样一层一层得向上找, 直到尽头:
if (parent === null || parent === NonDisposableHandle)
return isCancellation 假设不是取消, return false
这样就会进入 handleJobException(finalException), 由自己处理异常
上述过程中一直在强调, 假设它不是取消,
如果是取消, 要就有这几种情形:
-
一路向上找到顶, return isCancellation = true,
-
中间任意一个父Job说它不处理, return parent.childCancelled(cause) = false || isCancellation = true, 最终返回true
-
先提一句SupervisorJob, 它会重写 childCancelled(ex) = false 表示自己不处理, return parent.childCancelled(cause) = false || isCancellation = true, 最终返回true
然后它就不会进入 handleJobException(finalException)
SupervisorJob时需要再强调一遍的parent
scope.launch(SupervisorJob())
val newContext = 把scope.context的Job替换成参数SupervisorJob
val job = new StandaloneCoroutine(parentContext=newContext)
--> job.parentJob = parentContext[Job]=参数SupervisorJob
job.start(协程体)
return job
任务Job发生异常, 去询问父Job是否要处理, 父Job是SupervisorJob, 它不处理.
这里潜藏的父子关系比较难找出来.
handleJobException(ex) 由自己去处理异常
public fun handleCoroutineException(context: CoroutineContext, exception: Throwable)
// Invoke an exception handler from the context if present
try
context[CoroutineExceptionHandler]?.let
it.handleException(context, exception)
return
catch (t: Throwable)
handleCoroutineExceptionImpl(context, handlerException(exception, t))
return
// If a handler is not present in the context or an exception was thrown, fallback to the global handler
handleCoroutineExceptionImpl(context, exception)
fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable)
// use thread's handler
val currentThread = Thread.currentThread()
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
最终会调用到这里,哪个Job想处理这个异常,那就看看这个Job有没有设置CoroutineExceptionHandler, 如果有, 那就由它来兜底处理.
如果没有设置, 最终会执行 thread.uncaughtExceptionHandler.uncaughtException(), 而它没办法用try-catch捕获。
coroutineScope 和 supervisorScope
这一条就凭感觉硬往前面的结论上凑的, 以后如果能非常清晰得搞清楚, 再来修改.
在看字节小站的协程异常处理机制时,关于他的第7 第8点,按照前面分析代码的结论,似乎走不通,直到我尝试在第7条的try-catch里打印了错误堆栈:
2022-01-16 19:30:24.437 16178-16235/com.innocent.coroutine E/Innocent: caught : 测试
java.lang.RuntimeException: 测试
at com.innocent.coroutine.MainActivity$onCreate$1$1$1.invokeSuspend(MainActivity.kt:45)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
我恍然大悟, coroutineScope 不能把它理解为一个Scope对象, 而是应该理解为一个 subScope.launch(Job()) , 这样再按照上述结论, 就可以跑通了.
subScope.launch(Job())
发生异常
subScope内部发生异常, 最终一路上报给subScope, 此时协程异常退出, 但因为它是sub, 跟launch很类似, 最终还是要通过 resumeWith()恢复主协程, 就在恢复主协程时, 内部异常就作为 resumeWith(Result.failure(ex)) 被捕获.
subScope.launch(SupervisorJob())
发生异常
同样的, supervisorScope也可以理解为 subScope.launch(SupervisorJob()) , 内部异常因为 SupervisorJob , 交给子Job处理, 如果此子Job有innerExceptionHandler, 那就可以由innerExceptionHandler处理. 假设它没有, subScope会thread.uncaughtExceptionHandler.uncaughtException(), 因为它是sub, 所以由主协程最后兜底, 如有outterExceptionHandler则最终捕获, 如无则继续崩溃.
对这俩东西实在搞太不懂, 以后工作中我就尽量少用, 尽量用scope.launch方式
以上是关于Kotlin协程异常传递机制的主要内容,如果未能解决你的问题,请参考以下文章
kotlin协程硬核解读(5. Java异常本质&协程异常传播取消和异常处理机制)
kotlin协程硬核解读(5. Java异常本质&协程异常传播取消和异常处理机制)
kotlin协程硬核解读(5. Java异常本质&协程异常传播取消和异常处理机制)
Kotlin 协程协程异常处理 ② ( SupervisorJob 协程 | supervisorScope 协程作用域构建器函数 )
Kotlin 协程协程异常处理 ② ( SupervisorJob 协程 | supervisorScope 协程作用域构建器函数 )