协程的异常处理

invokeOnCompletion 监听协程状态

本文先只看 GlobalScope.launch 创建的根 StandaloneCoroutine:正常完成、异常完成时,invokeOnCompletionCoroutineExceptionHandler 分别在什么时候触发。父子协程之间的异常传播先不展开,后面单独看。

在看异常处理之前,先从 invokeOnCompletion 入手。这个例子里没有父 Job,重点是看完成回调如何挂到当前 Job 上:

1
2
3
4
5
6
7
8
private fun demo() {
    val job = GlobalScope.launch {
        Log.d(TAG, "demo: launch代码")
    }
    job.invokeOnCompletion {
        Log.d(TAG, "demo: invokeOnCompletion")
    }
}

执行结果:

1
2
CoroutineJobActivity    com.example.coroutinescopedemo       D  demo: launch代码
CoroutineJobActivity    com.example.coroutinescopedemo       D  demo: invokeOnCompletion

invokeOnCompletion 是 Job 完成时的回调。正常完成时 cause=null,异常或取消完成时 cause 会带上对应原因。

通过 launch 创建协程时,内部会构造一个 StandaloneCoroutine。它本身就是一个 Joblaunch 返回的也是这个 Job: launch执行代码.png StandaloneCoroutine 继承自 AbstractCoroutineAbstractCoroutine构造器执行代码.png

initParentJob.png GlobalScope.launch 没有父 Job,所以 initParentJob 里拿不到 parent,parentHandle 会被设置成 NonDisposableHandle

launch 返回后,接着调用 job.invokeOnCompletion。这个方法最终在 JobSupport 中实现: JobSupport下的invokeOnCompletion方法.png 三个参数的invokeOnCompletion方法.png

makeNode 的实现如下: makeNode的实现.png 从调用链看,onCancelling=false,当前 handler 还不是 JobNode,所以会创建 InvokeOnCompletion,并把外部传进来的 CompletionHandler 包进去:

1
2
3
4
5
private class InvokeOnCompletion(
    private val handler: CompletionHandler
) : JobNode()  {
    override fun invoke(cause: Throwable?) = handler.invoke(cause)
}

InvokeOnCompletion 是一个 JobNode

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
internal abstract class JobNode : CompletionHandlerBase(), DisposableHandle, Incomplete {
    /**
     * Initialized by [JobSupport.makeNode].
     */
    lateinit var job: JobSupport
    override val isActive: Boolean get() = true
    override val list: NodeList? get() = null
    override fun dispose() = job.removeNode(this)
    override fun toString() = "$classSimpleName@$hexAddress[job@${job.hexAddress}]"
}

JobNode 继承了 CompletionHandlerBase

1
2
3
internal actual abstract class CompletionHandlerBase actual constructor() : LockFreeLinkedListNode(), CompletionHandler {
    actual abstract override fun invoke(cause: Throwable?)
}

CompletionHandlerBase 实现了 CompletionHandler。所以 InvokeOnCompletion.invoke(cause) 被调用时,会转调外部传进来的 CompletionHandler.invoke(cause)。这里可以理解成给业务回调包了一层 Job 节点。

结论: makeNode 返回的是一个 InvokeOnCompletion 对象,它既是 JobNode,也保存着业务侧的 CompletionHandler

回到 invokeOnCompletion 的实现。创建 InvokeOnCompletion 后,会进入 loopOnState 遍历当前 state。这个例子的初始 state 是 Empty,并且 state.isActive=true,所以最终会把 InvokeOnCompletion CAS 到当前 Job 的 state 上,并返回这个节点。

结论: 调用 job.invokeOnCompletion 后,业务侧的 CompletionHandler 会先被包成 InvokeOnCompletion,再挂到 Job 的 state 上。Job 完成时触发这个节点,最后回到业务侧的回调。

协程体执行完后的 Job 流程: 前面分析过,协程体会被编译成 SuspendLambda。它的 invokeSuspend 执行完后,会继续调用上层 continuation 的 resumeWith。在这个例子里,上层 continuation 就是 StandaloneCoroutine,也是 launch 返回的 Job:

1
2
3
4
5
public final override fun resumeWith(result: Result<T>) {
    val state = makeCompletingOnce(result.toState())
    if (state === COMPLETING_WAITING_CHILDREN) return
    afterResume(state)
}

协程体的结果会传给 makeCompletingOnce

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
internal fun makeCompletingOnce(proposedUpdate: Any?): Any? {
    loopOnState { state ->
        val finalState = tryMakeCompleting(state, proposedUpdate)
        when {
            finalState === COMPLETING_ALREADY ->
                throw IllegalStateException(
                    "Job $this is already complete or completing, " +
                        "but is being completed with $proposedUpdate", proposedUpdate.exceptionOrNull
                )
            finalState === COMPLETING_RETRY -> return@loopOnState
            else -> return finalState // COMPLETING_WAITING_CHILDREN or final state
        }
    }
}

这里会轮询读取 state,并调用 tryMakeCompletingtryMakeCompleting实现.png 前面已经把 InvokeOnCompletion 挂到了 state 上,所以这里的 state 是一个 JobNode。当前例子没有子 Job,并且 proposedUpdate 不是 CompletedExceptionally,所以会走 tryFinalizeSimpleStatetryFinalizeSimpleState实现.png 内部调用 completeStateFinalization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private fun completeStateFinalization(state: Incomplete, update: Any?) {
    /*
     * Now the job in THE FINAL state. We need to properly handle the resulting state.
     * Order of various invocations here is important.
     *
     * 1) Unregister from parent job.
     */
    parentHandle?.let {
        it.dispose() // volatile read parentHandle _after_ state was updated
        parentHandle = NonDisposableHandle // release it just in case, to aid GC
    }
    val cause = (update as? CompletedExceptionally)?.cause
    /*
     * 2) Invoke completion handlers: .join(), callbacks etc.
     *    It's important to invoke them only AFTER exception handling and everything else, see #208
     */
    if (state is JobNode) { // SINGLE/SINGLE+ state -- one completion handler (common case)
        try {
            state.invoke(cause)
        } catch (ex: Throwable) {
            handleOnCompletionException(CompletionHandlerException("Exception in completion handler $state for $this", ex))
        }
    } else {
        state.list?.notifyCompletion(cause)
    }
}

completeStateFinalization 会判断 state 是否是 JobNode。当前 state 是 InvokeOnCompletion,所以直接调用 state.invoke(cause),再回调到外部传入的 CompletionHandler。正常完成时 cause=null,所以日志里能看到 invokeOnCompletion 被打印出来。

invokeOnCompletion 监听 Job 异常

还是看一个根 launch 例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private fun demo3() {
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.d(TAG, "demo3: CoroutineExceptionHandler--异常信息:${throwable?.message}")
    }
    val job = GlobalScope.launch(exceptionHandler) {
        Log.d(TAG, "demo3: launch代码")
        throw RuntimeException("协程体里面发生异常了")
    }
    job.invokeOnCompletion {
        Log.d(TAG, "demo3: invokeOnCompletion--异常信息:${it?.message}")
    }
}

这里给 launch 的 context 放了一个 CoroutineExceptionHandler,先看日志:

1
2
3
com.example.coroutinescopedemo       D  demo3: launch代码
com.example.coroutinescopedemo       D  demo3: CoroutineExceptionHandler--异常信息:协程体里面发生异常了
com.example.coroutinescopedemo       D  demo3: invokeOnCompletion--异常信息:协程体里面发生异常了

invokeOnCompletion 也能拿到异常。区别在于,异常完成不会走 tryFinalizeSimpleState 这条快路径,而是进入 tryMakeCompletingSlowPathresumeWith之后异常的流程.png 非异常完成会走 tryFinalizeSimpleState;异常完成会走 tryMakeCompletingSlowPath,里面会进入 finalizeFinishingStatefinalizeFinishingState主要逻辑.png 这个例子里当前协程是根 StandaloneCoroutine,没有普通父 Job 可以处理异常,所以 cancelParent 返回 false,接着会调用自己的 handleJobExceptionStandaloneCoroutine 重写了这个方法: StandaloneCoroutine重写handleJobException方法.png

然后进入 handleCoroutineExceptionCoroutineExceptionHandler的handleCoroutineException方法.png

对根 launch 来说,如果 context 里有 CoroutineExceptionHandler,异常会先交给它处理;如果没有,就会走默认未捕获异常处理,Android 上通常表现为崩溃。

再回到 invokeOnCompletion 的触发时机。异常会先在 finalizeFinishingState 里处理,之后仍会调用 completeStateFinalization,最终触发 invokeOnCompletion 注册进来的 CompletionHandler。这也是日志里 CoroutineExceptionHandlerinvokeOnCompletion 都能拿到异常的原因。

协程异常被 try-catch 接住:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
private fun demo4() {
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.d(TAG, "demo4: CoroutineExceptionHandler--异常信息:${throwable?.message}")
    }
    val job = GlobalScope.launch(exceptionHandler) {
        try {
            Log.d(TAG, "demo4: launch代码")
            throw RuntimeException("协程体里面发生异常了")
        } catch (e: Exception) {
            Log.d(TAG, "demo4: catch中代码")
        }
    }
    job.invokeOnCompletion {
        Log.d(TAG, "demo4: invokeOnCompletion--异常信息:${it?.message}")
    }
}

日志如下:

1
2
3
com.example.coroutinescopedemo       D  demo4: launch代码
com.example.coroutinescopedemo       D  demo4: catch中代码
com.example.coroutinescopedemo       D  demo4: invokeOnCompletion--异常信息:null

从日志看,这次 CoroutineExceptionHandler 没有回调,invokeOnCompletion 里拿到的异常也是 null。 我们通过字节码可以看到原因: 协程中try后的字节码.png 异常已经在协程体内被 catch 住,传给 JobSupport 的结果就不是 CompletedExceptionally,所以后续和正常完成一样处理。

总结:
注册阶段:launch 创建根 StandaloneCoroutine,没有父 Job 时 parentHandle=NonDisposableHandle。调用 invokeOnCompletion 时,外部的 CompletionHandler 会被包成 InvokeOnCompletion,再作为 JobNode 挂到 Job 的 state 上。
正常完成:SuspendLambda.invokeSuspend 执行结束后,会调用上层 continuation,也就是 StandaloneCoroutine.resumeWith。流程进入 makeCompletingOnce -> tryMakeCompleting -> tryFinalizeSimpleState -> completeStateFinalization,最后触发 InvokeOnCompletion。这时 cause=null
异常完成:如果协程体抛出的异常没有在内部 catch 住,resumeWith(Result.failure) 会让 Job 进入 tryMakeCompletingSlowPath -> finalizeFinishingState。根 StandaloneCoroutine 没有父 Job 接手时,会调用自己的 handleJobException,再从 context 里找 CoroutineExceptionHandler。异常处理之后,completeStateFinalization 仍会触发 InvokeOnCompletion,所以回调里可以拿到同一个异常。
try-catch:如果异常已经在协程体内部 catch 住,传给 Job 的就是正常结果,CoroutineExceptionHandler 不会回调,invokeOnCompletion 收到的 cause 也是 null

InvokeOnCompletion 类图

InvokeOnCompletion类图

异常处理时序图

异常处理流程图

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计