目录
- 1.CoroutineContext
- 2.Element的作用
- 3.CoroutineContext相关的操作符原理解析
- 3.1.什么类型的集合
- 3.2.如何管理
- Element
- combinedContext
- 4.验证以及总结
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(".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(".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{<!--{C}%C!%2D%2D%20%2D%2D%3E--> _, _ -> } + 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(".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类型的话,
- element[key]不为空说明当前节点就是要找的节点,直接返回该节点的left节点(代表着把当前节点跳过,也就是移除该节点)。
- element[key]为空那么说明当前节点不是要找的节点,需要向链表的左端left去寻找目标,深度递归遍历left.minusKey(key),返回的newLeft有三种情况:
- newLeft === left,在左边找不到目标Key(根据Element.minusKey函数发现,返回的是this的话就是key没有匹配到),从该节点到左端节点都可以返回。
- newLeft === EmptyCoroutineContext ,在左边找到了目标key(根据Element.minusKey函数发现,返回的是EmptyCoroutineContext 的话key匹配到了Element),找到了目标那么需要将目标跳过,那么从本节点开始返回,左边节点需要跳过移除,该节点就成了链表的最左端节点Element。
- 不是上述的情况,那么就是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("name") + Dispatchers.IO + CoroutineExceptionHandler{ c,e -> }
Log.i(TAG, "coroutineContext $coroutineContext")
val newContext = coroutineContext + SupervisorJob()
Log.i(TAG, "newContext $newContext")
val newContext = newContext + (Job() + CoroutineName("name2"))
Log.i(TAG, "newContext $newContext2")
Log.i(TAG, "newContext[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父子关系的原理。