协程上下文

16 阅读4分钟

在讲协程的如何切换线程之前,有必要先了解下协程的上下文是什么?它的结构是什么样的?以及我们如何使用它?今天带着该问题来认识它。

CoroutineContext

协程上下文都是继承自CoroutineContext,它是一个接口,内部方法以及内部类如下:

它的实现子类有如下:

image.png

比如我们常见的EmptyCoroutineContext,它的内部实现如下:

image.png 可以看到它的get、fold、plus、minusKey几个方法都是默认实现,你可以理解它就是个空壳子的context。

Element

在讲CoroutineContext内部结构之前,先来认识下Element,它也实现了CoroutineContext接口:

image.png

Element中有一个key的属性,这里可以理解key就是当前Element的唯一标识。实现一个context的时候需要指明它的key是啥,此处就是用该key来标识
get:如果传进来的key和自己的key相等,则返回自己,否则返回null
fold:将初始值和当前element返回给lambda,让lambda自己去处理
minusKey:如果传进来的key和自己相同,则返回EmptyCoroutineContext,否则返回自己,其实是删除对应key的context.

写了3个context,然后用"+"拼接:

image.png

自定义context的时候,需要继承自AbstractCoroutineContextElement,它是继承自Element,因为它强制要求需要一个key作为context的标识,一般key的element标识是当前context,看上面的One这个context,它的key拥有的element是One。

输出日志如下:

image.png

One()+Two()+Three()得到的是一个CombinedContext,get方法通过One这个key取到了One这个取对应的Context

image.png

日志如下: image.png

可以看到我给One的context拼接了一个EmptyCoroutineContext时候,得到的是它自己,"+"是重载了context的plus方法,看下plus方法的实现:

public operator fun plus(context: CoroutineContext): CoroutineContext =
    //①
    if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
        //②
        context.fold(this) { acc, element ->
            //③
            val removed = acc.minusKey(element.key)
            //③.1
            if (removed === EmptyCoroutineContext) element else {
                // make sure interceptor is always last in the context (and thus is fast to get when present)
                //④
                val interceptor = removed[ContinuationInterceptor]
                //⑤
                if (interceptor == null) CombinedContext(removed, element) else {
                    //⑥
                    val left = removed.minusKey(ContinuationInterceptor)
                    //⑦
                    if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                        //⑧
                        CombinedContext(CombinedContext(left, element), interceptor)
                }
            }
        }

1.如果传进来的context是EmptyCoroutineContext,则返回自己,所以上面的One()+EmptyCoroutineContext,得到的是One这个context
2.context.fold,会把初始值和context传给闭包,所以acc是当前context,element是传进来的context
3.acc.minuskey(element.key),如果传进来的context的key和当前context的key相等,则返回传进来的context,所以新的context会把旧的context给覆盖掉了
4.如果传进来的context的key和当前context的key不相等,removed则是当前context,查看当前context中是否有ContinuationInterceptor类型的context,我们的dispatcher都是属于该类型,需要单独处理
5.如果context中没有ContinuationInterceptor类型的context,则初始化出一个CombinedContext的context,所以上面的One()+Two()+Three()是一个CombinedContext的context
6.如果当前context中存在ContinuationInterceptor类型的context,则继续判断当前context是不是ContinuationInterceptor类型的context
7.如果是ContinuationInterceptor类型的context,则把传进来的context和当前的context组合成CombinedContext的context
8.如果当前的context不是一个ContinuationInterceptor类型的context,则把当前当前的context和传进来的context新组合成一个CombinedContext的context,再和前面的ContinuationInterceptor组合成一个新的CombinedContext的context

CombinedContext

internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {

    override fun <E : Element> get(key: Key<E>): E? {
        var cur = this
        while (true) {
            cur.element[key]?.let { return it }
            val next = cur.left
            if (next is CombinedContext) {
                cur = next
            } else {
                return next[key]
            }
        }
    }

    public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
        operation(left.fold(initial, operation), element)

    public override fun minusKey(key: Key<*>): CoroutineContext {
        //①
        element[key]?.let { return left }
        //②
        val newLeft = left.minusKey(key)
        return when {
            //③
            newLeft === left -> this
            //④
            newLeft === EmptyCoroutineContext -> element
            //⑤
            else -> CombinedContext(newLeft, element)
        }
    }

    override fun toString(): String =
        "[" + fold("") { acc, element ->
            if (acc.isEmpty()) element.toString() else "$acc, $element"
        } + "]"
}

它是直接继承自CoroutineContext,有两个比较重要的属性:

left:CoroutineContext,它是左边的节点

element:Element,当前节点

其实和链表的结构有点类似,left相当于next节点。

get:递归节点,直到left节点不是CombinedContext类型的

fold:先把left和初始值组成一个初始值,然后再把这个初始值和当前节点传给闭包

minusKey:

1.如果当前节点中找到了该key,则返回left节点

2.如果找不到,则继续在left节点中找

3.如果找不到返回this

4.如果找到了则返回当前节点

5.否则继续往左边再找

整个分析来看,协程中的context如果是多个context拼接的时候如果传进来的是EmptyCoroutineContext,则只保存自己。如果传进来的context的key和当前context的key一样,则会覆盖掉原来的context。如果都不满足,则采用链表的形式插入到原来的context头节点上,如果传进来的是ContinuationInterceptor类型的,则会把该类型放到头节点。

类图

再来一张本次讲解的context类图:

协程上下文类图.png