Kotlin协程异常处理机制的前提 : 构建Job树

332 阅读4分钟

Kotlin协程的异常处理机器蛮复杂的,不过,我似乎找到了主线,

在捋异常处理机制之前,必须先把Job树给捋清楚。

协程的代码太过复杂,我只是边看边猜,只能保证主体框架没什么大问题,不敢保证一定正确。

A 别管那么多, 我们要相信CoroutineScope一定会有一个Job

通常用scope.launch {} 的方式启动一个协程

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

class StandaloneCoroutine 
    : AbstractCoroutine 
    : JobSupport(active), Job, Continuation<T>, CoroutineScope (...) {
    init {
        initParentJob(parentContext[Job])
    }
}
    

首先关注一下 context 的流程 newCoroutineContext(context):

  1. scope有自己的context, 在代码中对应newCoroutineContext(context)方法中用到的coroutineContext

  2. 使用者可以传入自己的context, 在代码中对应newCoroutineContext(context)

  3. 新创建的协程会通过newCoroutineContext(context)产生一个新的context

  4. 新创建的协程作为StandaloneCoroutine(parentContext=newContext)的参数,创建出一个Job/JobSupport对象.

  5. 结合 initParentJob(parentContext[Job]) 可知 : 新创建的Job, 是参数newContext中的Job对象的子Job, 父子关系由此产生

那么问题来了,

A1 parentContext[Job]是个啥?

这部分代码,我是看了一些代码以后,凭现状猜测的:

MainScope 参数传进来一个Job
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

lifecycleScope 参数传进来一个Job
LifecycleCoroutineScopeImpl(
    lifecycle,
    SupervisorJob() + Dispatchers.Main.immediate
)

viewModelScope 参数传进来一个Job
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)

重点:
CoroutineScope()方法创建一个Scope, 如无Job就new一个添加进来
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

我就是凭这个, 猜测 CoroutineScope一定会有一个Job

A2 scope.launch(Job()) {} 可以传参一个新Job

根据launch()的第一步, 它会创建一个新的context, 此新context会把scope.context中的Job替换掉, 然后创建出新的StandaloneCoroutine->Job (暂且称之为任务Job), 并且将参数Job作为任务Job的父。

这一点会影响到scope.cancel(), 可以参考 字节小站的# Kotlin协程之Job初体验(juejin.cn/post/704254…),

scope.cancel()时,它取出自己的Job(暂且称之为顶层Job),然后沿着Job树一直取消下去。

可是因为子协程中直接把顶层Job替换成了参数Job,它们并没有建立父子关系,所以顶层Job根据父子关系往下找,就找不到参数Job,也就没办法取消掉它。

想要参数Job,还想正常取消,那就正确建立父子关系呗:

scope.launch(Job(parent = scope.coroutineContext[Job])) {}

B 建立父子关系的流程: initParentJob(parentContext[Job])

此处就不求准确,只求易懂啦。

scope.launch {
    launch {
        launch {
        }
    }
    launch {
    }
    launch {
    }
}

一个协程代码块中,可以继续launch多个协程,可以一直launch下去,每个协程块都是一个StandaloneCoroutine : Job对象, 它会被多次包装成其他类型,按照层级建立树状结构。

首先,把Job树想像成一棵View树,ViewGroup 里有一个 List<View>。

StandaloneCoroutine : JobSupport里有一个_state: NodeList 字段,在此方法流程中会把 Job对象封装成 JobNode 对象添加到此List中, 添加过程使用CAS方式,保证线程安全。

protected fun initParentJob(parent: Job?) {
    ...
    parent.start() // make sure the parent is started
    // 在这里构建父子关系 -> 主要是让父Job知道有自己这么个子Job
    val handle = parent.attachChild(this)
    // 在这里构建父子关系 -> 主要是让自己这个子Job知道自己的父Job
    parentHandle = handle
    ...
}

此方法过后,父子互相知道对方。

C 每个Job都有操作父Job和子Job的能力 parent.attachChild(this)

最终的封装形式大概是这样的:

在子Job中执行 : parent.attachChild(this)
其结果就是在父Job中执行 this.attachChild(child)
面向对象编程的树状结构中尤其要注意, 不能只关心类, 一定要思考当前是哪个具体的对象

JobNode {
    ChildHandleNode 
        : ChildHandle { fun childCancelled(ex) 我是子Job, 我出现异常后, 向父Job上报 },
        : CompletionHandle { invoke() 我是父Job, 我要结束子Job } {
            Job child
        }
}

JobNode node = makeNode() 
其中会执行 node.job = this, 转换成对象名就是 child.job = parent

ChildHandleNode extends JobNode {
    override val parent: Job get() = job
}
这里也明确了, child.parent = job对象 = parent在makeNode()的this

总结

协程的代码挺难懂的,反正只要知道:

  1. 子协程最终会变成Job树

  2. 每个Job都有 向父Job上报 和 向子Job下发 的功能

  3. scope.cancel()要想成功, 一定要正确构建Job树, 要么别传参数Job, 要么参数Job手动设置它的父Job.

  4. scope.canel()主要是从上向下, 而异常传播机制是从下向上, 这是另外一个故事.