kotlin coroutine源码解析之coroutineContext篇

5 篇文章 0 订阅
5 篇文章 0 订阅

1.CoroutineContext

表示一个元素或者是元素集合的接口。它有一个Key(索引)的Element实例集合,每一个Element的实例也是一个CoroutineContext,即集合中每个元素也是集合。
如下图所示,CoroutineContext的常见官方实现有以下几种(少见的或者自定义的实现就不列举,以后再聊):

  • Job:协程实例,控制协程生命周期(new、acruve、completing、conpleted、cancelling、cancelled)。
  • CoroutineDIspatcher:协程调度器,给指定线程分发协程任务(IO、Default、Main、Unconfined)。
  • CoroutineName:协程名称,用于定义协程的名称,调试打印信息使用。
  • CoroutineExceptionHandler:协程异常处理器,用于处理未捕获的异常。
    在这里插入图片描述

2.Element的作用

Element类也是继承自CoroutineContext接口的,该类的作用是给子类保留一个Key成员变量,用于在集合查询的时候可以快速查找到目标coroutineContext,Key成员变量是一个泛型变量,每个继承自Element的子类都会去覆盖实现Key成员变量(一般是使用子类自己去覆盖Key),就比如拿最简单的CoroutineName类来举例子:

public data class CoroutineName( 
    /**
     * User-defined coroutine name.
     */
    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
    /**
     * Key for [CoroutineName] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<CoroutineName>

    /**
     * Returns a string representation of the object.
     */
    override fun toString(): String = "CoroutineName($name)"
}

@SinceKotlin("1.3")
public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element

/**
 * Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
 */
public interface Key<E : Element>

/**
 * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
 */
public interface Element : CoroutineContext {
    /**
     * A key of this coroutine context element.
     */
    public val key: Key<*>

    public override operator fun <E : Element> get(key: Key<E>): E? =
        @Suppress("UNCHECKED_CAST")
        if (this.key == key) this as E else null

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

    public override fun minusKey(key: Key<*>): CoroutineContext =
        if (this.key == key) EmptyCoroutineContext else this
}

上面的CoroutineName构造函数定义为

public data class CoroutineName( 
    /**
     * User-defined coroutine name.
     */
    val name: String
) : AbstractCoroutineContextElement(CoroutineName)

父类构造函数中传递的参数是CoroutineName,但是我们发现CoroutineName也不是Key接口public interface Key<E : Element>的实现,为啥可以这样直接传递呢?但是我们仔细看发现CoroutineName类定义了伴生对象: public companion object Key : CoroutineContext.Key<CoroutineName>,在kotlin中伴生对象是可以直接省略 类.companion.调用方式的,CoroutineName类也就代表着伴生对象,所以可以直接作为CoroutineName父类构造函数的参数,神奇的kotlin语法搞得我一愣一愣的。

类似的还有Job,CoroutineDIspatcher,CoroutineExceptionHandler的成员变量Key的覆盖实现:

//Job
public interface Job : CoroutineContext.Element {
    /**
     * Key for [Job] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<Job> { //省略 }
     //省略 
}

//CoroutineExceptionHandler
public interface CoroutineExceptionHandler : CoroutineContext.Element {
    /**
     * Key for [CoroutineExceptionHandler] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
 	//省略 
}

// CoroutineDIspatcher
@SinceKotlin("1.3")
public interface ContinuationInterceptor : CoroutineContext.Element {
    /**
     * The key that defines *the* context interceptor.
     */
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>
}

3.CoroutineContext相关的操作符原理解析

CoroutineContext的操作符??有点莫名其妙的感觉,仅仅凭借我的直觉的话很难理解,但是平常使用协程的过程中,我们经常会使用这些相关的操作符,比如 +,[]等等符号,下面代码示例:

val comb = Job() + CoroutineName("")
val cName = comb[CoroutineName]

上面的+代表两个coroutineContext合并到集合中,这里的集合实际上是一个链表,后面会讲到。
上面的[]代表着从集合中索引出CoroutineName类型的CoroutineContext,这里也可以看出来仅仅通过key就查找出元素和map很相似,那么可以知道value是唯一的。key都是coroutineContext子类作为泛型类型的,具有唯一性,那也可以间接推断出上面+操作其实也会覆盖拥有相同key的value的值。

还有其他操作函数:fold展开操作, minusKey删除集合中存在的元素。
还有一个问题就是,这个集合到底是什么类型的集合,已经如何管理的,我们来一一解答:

3.1.什么类型的集合

CoroutineConetxt集合是链表结构的集合,是一个从本节点开始,向左遍历parent节点的一个链表,节点的都是CoroutineContext的子类,分为Element,CombinedContext,EmptyCoroutineContext三种。
有以下代码作为举例:

val scope = CoroutineScope(CoroutineName("") + Job() + CoroutineExceptionHandler{ _, _ -> } + Dispatchers.Default)

假如CoroutineScope自己的coroutineContext变量集合中是包含CoroutineName,Job,CoroutineExceptionHanlder,CoroutineDIspatcher四种上下文的,那么他们组成的集合结构可能就会是下图所示的链表结构,
在这里插入图片描述
使用scope查找对应的Job的话直接调用scope[Job]方法,替代Job的话调用 scope + Job(),看源码就是使用scope的上下文集合替换Job

public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =
    ContextScope(coroutineContext + context)

为啥是链表结构的集合呢,接下来直接看源码就知道了。

3.2.如何管理

我们集合的链表结构,每个节点都是CombinedContext类型,里面包含了element,left两个成员变量,left指向链表的左边,element表示当前节点的上下文元素(一般是job,name,handler,dispatcher四种),链表的最左端节点一定是Element元素

Element

主要实现在combinedContext,Element元素的方法实现比较简单,不单独列举。

combinedContext

  • 构造函数
@SinceKotlin("1.3")
internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {
  • get函数:
	//Element
    public override operator fun <E : Element> get(key: Key<E>): E? =
          @Suppress("UNCHECKED_CAST")
          if (this.key == key) this as E else null
	
	//CombinedContext
    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]
            }
        }
    }

在代码中一般不会使用get方法,而是使用context[key]来代替,类似于map集合的查询。上下文是Element类型,key是对应类型那么返回当前Element,不是当前类型,返回null;上下文是CombinedContext类型,指针cur指向当前节点,while循环开始,当前的element元素的key查找到了,那么就返回当前combinedContext,如果没找到,那么将指针指向left节点,如果left节点是combinedContext类型,那么重复上述操作,如果是Element类型直接判断是否可以查找到key值。那么从这里看出链表的最左端元素一定是Element节点。

  • contain函数
    private fun contains(element: Element): Boolean =
        get(element.key) == element

    private fun containsAll(context: CombinedContext): Boolean {
        var cur = context
        while (true) {
            if (!contains(cur.element)) return false
            val next = cur.left
            if (next is CombinedContext) {
                cur = next
            } else {
                return contains(next as Element)
            }
        }
    }

类似于get操作,contains函数直接调用get方法来判断元素是不是和传入参数相等。
containAll函数就是遍历参数的链表节点是不是都包含在当前链表中。

  • fold函数
   	//coroutineContext
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R

从表面意思就是展开操作,第一个入参 CoroutineContext,第二个入参 lambda表达式 用表达式的两个参数CoroutineContext, Element 返回一个新的 CoroutineContext:

	operation :(R , Element) -> R
	Job.fold(CoroutineName("测试"),{ coroutineContext , element ->
        TODO("return new CoroutineContext")
    }) //example
    //Element
	public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
    	operation(initial, this)

作为receiver的上下文是Element,调用fold的话,是让ELement和入参的CoroutineContext作为lambda表达式 的两个参数调用该lambda表达式返回结果。

    MainScope().coroutineContext.fold(Job(),{ coroutineContext , element ->
        TODO("return new CoroutineContext")
    }) //example
	//CombinedContext
	public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
        operation(left.fold(initial, operation), element)

作为receiver的上下文是CombinedContext,调用fold的话,是让left深度递归调用fold函数,一直到链表的最左端节点,我们知道链表的最左端节点一定是Element,那么根据上面的代码,Element的fold函数内调用operation返回一个CoroutineContext后,递归回溯到上一层,继续调用operation返回一个CoroutineContext,继续回溯,一直回溯到开始调用MainScope().coroutineContext.fold(Job的地方。如下图所示:
在这里插入图片描述

  • minusKey函数

该函数的意思是从上下文集合中删除key对应的上下文。

  	//Element
    public override fun minusKey(key: Key<*>): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this

接收者receiver是Element类型的话,如果入参key和receiver是相等的话,那么返回EmptyCoroutineContext空上下文,否则返回receiver本身。(可见找到得到key的话会返回空上下文,找不到的话返回本身)

	//CombinedContext
	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)
	    }
	}

接收者receiver是CombinedContext类型的话,

  1. element[key]不为空说明当前节点就是要找的节点,直接返回该节点的left节点(代表着把当前节点跳过,也就是移除该节点)。
  2. element[key]为空那么说明当前节点不是要找的节点,需要向链表的左端left去寻找目标,深度递归遍历left.minusKey(key),返回的newLeft有三种情况:
  3. newLeft === left,在左边找不到目标Key(根据Element.minusKey函数发现,返回的是this的话就是key没有匹配到),从该节点到左端节点都可以返回。
  4. newLeft === EmptyCoroutineContext ,在左边找到了目标key(根据Element.minusKey函数发现,返回的是EmptyCoroutineContext 的话key匹配到了Element),找到了目标那么需要将目标跳过,那么从本节点开始返回,左边节点需要跳过移除,该节点就成了链表的最左端节点Element。
  5. 不是上述的情况,那么就是newLeft是触发了1.或者4.情况,返回的是left的element元素,或者是本节点的left节点跳过了,返回的是left.left节点,这样将newLeft和本节点的element构造出新的CombinedContext节点。

上述操作,都只是在链表上跳过节点,然后将跳过的节点左节点left和右节点创建新的CombinedContext,产生一个新的链表出来。

操作例子:

删除最左端节点
在这里插入图片描述
删除中间节点:
在这里插入图片描述
结论:minusKey的操作只是将原始链表集合中排除某一个节点,然后复制一个链表返回,所以并不会影响原始集合

  • plus函数
    该函数重写+操作符,函数定义operator fun plus(context: CoroutineContext): CoroutineContext ,作用是对上下文集合进行添加(相同会覆盖)指定上下文操作。这个函数只有CoroutineContext实现了,代码如下:
    /**
     * Returns a context containing elements from this context and elements from  other [context].
     * The elements from this context with the same key as in the other one are dropped.
     */
    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)
                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)
                    }
                }
            }

入参如果是EmptyContext,那么直接返回;不是空的话,对入参进行fold操作,上面讲了fold操作是将context链表展开,从链表最左端开始向context回溯调用fold函数的入参lambda表达式。那么我们就知道了![A (CombineContext) + B(CombineContext)](https://img-blog.csdnimg.cn/40fb2861842a4c8f8c918752e3f89fd0.png) 是如何操作的了,首先B作为plus的入参,那么B先展开到B链表结构的最左端,然后执行lambda操作{ acc, element -> ... }, 这个lambda里面

第一步

context.fold(this) { acc, element ->
	acc.minusKey(Element.Key)
	// ...
}

//CombineContext的实现
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
    operation(left.fold(initial, operation), element)

根据CombineContext的实现,知道lambda的acc参数是A (CombineContext),element参数是B(CombineContext)的fold递归的当前位置的element的元素,acc.minusKey(Element.Key)所做的事情就是移除A (CombineContext)链表中的B(CombineContext)的element元素。

第二步

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]
	// ...
}

第一步移除掉element之后,判断剩余的removed链表是不是empty的,如果为空,返回B(CombineContext)的fold递归位置的element元素;不为空,接着从removed链表中获取ContinuationInterceptor上下文(也就是dispatcher)。

第三步

if (interceptor == null) CombinedContext(removed, element) else {
    val left = removed.minusKey(ContinuationInterceptor)
    if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
        CombinedContext(CombinedContext(left, element), interceptor)
}

获取的interceptor为空,那将element和removed链表构造出一个新的CombinedContext节点返回;如果不为空,从removed链表中移除interceptor返回一个不包含interceptor的链表left;移除后left链表为空,那么将element和interceptor构造出一个新的CombinedContext节点返回;left链表不为空,那么将left, element构造出一个新的CombinedContext节点,将新的CombinedContext节点和interceptor早构造出一个新的节点返回。

每一层递归fold操作结束后,返回一个新的context给上一层继续递归,直到结束为止。

操作例子图如下:

有如下两个集合A (CombineContext) + B(CombineContext)
在这里插入图片描述
第一次递归回溯:
在这里插入图片描述
第二次递归回溯:
在这里插入图片描述
回溯深度取决于入参B的链表长度,B有多长回溯就会发生几次,这里没有加入interceptor上下文元素,减少画图复杂度。

plus操作结论:
1.发现每次返回节点的时候,都会将interceptor移除后,放到节点的最右边的位置,可以知道interceptor一定在链表的头部;
2.lambda表达式中,一定会先移除掉相同key的上下文元素,然后用后加入的element和left链表新建一个CombinedContext节点插入到头部
3.plus操作会覆盖掉有相同key的上下文元素

4.验证以及总结

经过对上面的源码的分析,可以推断出一些上下文元素的操作符操作后,集合的元素排列状态。比如下面操作:

    private fun test() {
        val coroutineContext = Job() + CoroutineName("name1") + Dispatchers.IO + CoroutineExceptionHandler{ c,e -> }
        Log.i(TAG, "coroutineContext $coroutineContext")

        val newContext = coroutineContext + SupervisorJob()
        Log.i(TAG, "newContext $newContext")

        val newContext2 = newContext + (Job() + CoroutineName("name2"))
        Log.i(TAG, "newContext2 $newContext2")

        Log.i(TAG, "newContext2[CoroutineName] ${newContext2[CoroutineName]}")
    }

打印的日志如下:

I/MainActivity: coroutineContext [
									JobImpl{Active}@b32c44, 
									CoroutineName(name1), 
									com.meeting.kotlinapplication.MainActivity$test$$inlined$CoroutineExceptionHandler$1@45ec12d, 
									Dispatchers.IO
								 ]

I/MainActivity: newContext [
								CoroutineName(name1), 
								com.meeting.kotlinapplication.MainActivity$test$$inlined$CoroutineExceptionHandler$1@45ec12d, 
								SupervisorJobImpl{Active}@1022662, 
								Dispatchers.IO
							]
							
I/MainActivity: newContext2 [
								com.meeting.kotlinapplication.MainActivity$test$$inlined$CoroutineExceptionHandler$1@45ec12d, 
								JobImpl{Active}@76863f3, 
								CoroutineName(name2), 
								Dispatchers.IO
							]

I/MainActivity: newContext2[CoroutineName] CoroutineName(name2)

I/MainActivity: coroutineContext [
									JobImpl{Active}@b32c44, 
									CoroutineName(name1), 
									com.meeting.kotlinapplication.MainActivity$test$$inlined$CoroutineExceptionHandler$1@45ec12d, 
									Dispatchers.IO
								 ]

可以看出来:

1. Dispatchers元素一定是在链表的头部;
2. 重复key的元素会被后加入的元素覆盖,集合中不存在重复key的元素;
3. +操作后返回新的链表集合,不会影响原始集合链表结构

上面总结的这些性质,可以很好的为job协程的父子关系,子job继承父job的上下文集合这些特性服务,下一篇我将讲解 协程Job父子关系的原理

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值