Kotlin系列文章- Kotlin专栏:
- Kotlin协程之根底运用
- Kotlin协程之深化了解协程作业原理
- Kotlin协程之协程取消与反常处理
- Kotlin协程之再次读懂协程作业原理-推荐
- Kotlin协程之Flow作业原理
- Kotlin协程之一文看懂StateFlow和SharedFlow
- Kotlin协程之一文看懂Channel管道
- Kotlin系列|一文看懂Lazy机制
概述
协程 CoroutineContext 是运用协程时不可避免会接触到的概念,能够解释为协程上下文,这比较笼统,所以测验去看看它的源码:
好家伙,更笼统了。能看到有个叫 Element 的东西贯穿了 CoroutineContext 的一直,能够猜测 CoroutineContext 相似一个容器,里边存着履行协程需求用到的 Element 元素,那么 CoroutineContext 里边存的是啥呢?又有啥效果?
CoroutineContext
协程中有个常用的写法,相似下面:
scope.launch(Dispatchers.IO + SupervisorJob()) {}
看到这个 +
号,能够猜测是重载了操作符,点进去看看源码:
环顾四周,还有个 Element 接口,它同时也继承了 CoroutineContext 接口,
public interface Element : CoroutineContext
多看几眼,这里边又是 key,又是 get 的,大致能猜出来这个 CoroutineContext 应该是个类 Map 结构,为什么说是个 类 Map
结构呢?由于看代码,这不像咱们一般了解的 Map 容器,依据官方注释,称它是一个 indexed set
,介于 set 和 map 之间的一个结构,CoroutineContext 是包含一系列 CoroutineContext 的调集。
接下来咱们就来解析一下这个结构到底是怎样作业的。
Plus 操作
再回到上面 plus 的代码,能够看到除了传入的参数是 EmptyCoroutineContext 外(即 +
号右边),其他状况都回来一个 CombinedContext 目标。看看这是个啥:
// this is a left-biased list, so that `plus` works naturally
internal class CombinedContext(
private val left: CoroutineContext,
private val element: Element
) : CoroutineContext, Serializable
看注释和属性得知,这是一个 left-biased list
。咱们把影响阅览的逻辑都去掉,留下这个:
// context 便是加号右边的目标
public operator fun plus(context: CoroutineContext): CoroutineContext =
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
CombinedContext(removed, element)
}
}
首要从 Element1 + Element2
的状况看起:
- 从
Element.fold()
的源码能够知道,fold 办法便是直接履行后边的 operation 代码块,那么参数acc = Element1
,element = Element2
。 -
Element1.minusKey(Element2.key)
有两条分支:-
Element1.key == Element2.key
: 则removed = EmptyCoroutineContext
,终究回来Element2
,Element1
直接被疏忽掉了。 -
Element1.key != Element2.key
: 则removed = Element1
,终究回来CombinedContext(Element1, Element2)
-
划要点:
关于
Element1 + Element2
的状况,假如这俩 key 一样,直接回来Element2
,假如 key 不一样,那么回来的便是CombinedContext(left = Element1, element = Element2)
,看起来很好了解,对吧?
接着,咱们再用上面的成果去 plus 另一个 Element3 目标,即 Element1 + Element2 + Element3 = CombinedContext(left = Element1, element = Element2) + Element3
,为了便利阅览,把 plus 代码再贴一下:
// context = Element3
public operator fun plus(context: CoroutineContext): CoroutineContext =
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
CombinedContext(removed, element)
}
}
-
传入的
context = Element3
,依据其 fold 办法可知会直接履行后边的 operation 代码块,参数acc = CombinedContext(left = Element1, element = Element2),element = Element3
。 -
再看看
CombinedContext.minusKey(Element3.key)
办法,也有几条分支:-
Element2.key == Element3.key
: 则removed = Element1
,Element2 被除掉了,终究 plus 回来CombinedContext(left = Element1, element = Element3)
。 -
Element2.key != Element3.key
: 则接着履行Element1.minusKey(key)
,又要依据Element1.key
和Element3.key
是否持平,分两个分支:- 持平,则回来的
newLeft = EmptyCoroutineContext
,Element1 被除掉,所以removed = Element2
,终究 plus 回来CombinedContext(left = Element2, element = Element3)
。 - 不持平,则回来的
newLeft = Element1 == left
,所以removed = CombinedContext(left = Element1, element = Element2)
,终究 plus 回来CombinedContext(left = CombinedContext(left = Element1, element = Element2), element = Element3)
。
- 持平,则回来的
-
划要点:
- 关于
Element1 + Element2 + Element3
的状况,假如三者的 key 都不一样,则成果是CombinedContext(left = CombinedContext(left = Element1, element = Element2), element = Element3)
。- 假如有 key 持平的状况,则一直会运用
+
号后边的 Element 目标,前面相同 key 的元素都被除掉。比如说假定 Element1 和 Element2 的 key 一样,不等于 Element3 的 key,则成果是CombinedContext(left = Element2, element = Element3)
。
这个 plus 操作能够用下图表明,比较明晰的左偏结构:
Get 操作
接下来看看 get 办法。Element 的 get 办法如下:
CombinedContext 的 get 办法如下:
其实便是迭代取,先从 element 开始,假如它的 key 等于要取的 key 就直接回来,不然再从 left 里取,如此循环,直到最后的 left 不再是 CombinedContext 后,直接从它里边取,取不到就回来空的。
划要点:
能够看出 get 的迭代取元素是有优先级的,依据上面画的左偏结构图,越靠右边的元素取出的优先级越高。CombinedContext 是有层级的,它和 set 和 map 这种平面结构不同,遍历时从右到左进行,更右的元素有更高的优先级。
这里再补充一下最开始 plus 办法里疏忽的代码:
能够看到永久会把 ContinuationInterceptor 这个 Key 的 Element 放到右侧,即优先级最高的方位,这个做法会使得第一次就能取出:thus is fast to get when present
,我猜应该是由于它的运用频率最高,由于每次启动协程或者是 withContext 都会先调用 intercepted 去拦截。
Key是什么
好了,弄了解 CoroutineContext 的 plus 和 get 操作后,那咱们又有个疑问,这个结构里的 Key 在咱们实践运用中是啥?
首要看下 Key 这个接口的界说:
public interface Key<E : Element>
public interface Element : CoroutineContext {
public val key: Key<*>
}
Key 的界说里有个范型,它有必要是 Element 的子类,且每个 Element 的完成类里都需求完成 Key,咱们挑几个常见的看看:
public interface Job : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<Job>
}
public interface ContinuationInterceptor : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<ContinuationInterceptor>
}
能够看出 Job.Key 和 ContinuationInterceptor.Key 都是 Key,它们是伴生目标,能够直接运用 Job 和 ContinuationInterceptor 表明。这样咱们就能了解下面经常会看到的用法了:
val job = coroutineContext[Job]
便是从 coroutineContext 里通过 get 办法取出 Job.Key 这个 Key 对应的 Element 目标了,而哪些 Element 是以 Job 为 Key 的呢?能够自己翻翻源码,咱们一般通过 SupervisorJob()
或 Job()
创建出来的 Job 实例都是用 Job 作为 Key 的,包含协程 launch 源码里能够看到的 AbstractCoroutine 实例也是用 Job 作为 Key 的,关于 JobSupport 的子类,都以 Job 为 Key:
public open class JobSupport constructor(active: Boolean) : Job, ChildJob, ParentJob {
final override val key: CoroutineContext.Key<*> get() = Job
Fold 和 MinusKey
这里再回过头来看看上面用到的 fold 和 minusKey 办法,上面只给出了这两个办法关于实践输入后的输出是啥,这个章节咱们理一下这两个办法的效果。
fold
fold 办法需求传入一个 initial 初始值和一个 operation 累加算法,首要看看 Element 的 fold 办法:
// Element
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
直接履行 operation 算法,然后将传入的 initial 作为左值,本身作为右值(被追加)传入。结合上面 plus 传入的 operation 块能够知道:假如 +
号右边是个 Element(即只包含自己一个元素),则该元素会把本身作为被追加的元素,成果总会出现在左偏结构的右层。即:
element1 + element2 -> element2.fold(element1) -> operation(element1, element2) -> CombinedContext(left = element1, element = element2)
再看看 CombinedContext 的 fold 办法:
// CombinedContext
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(left.fold(initial, operation), element)
它会先对 left 进行递归运算,然后再跟 element 做运算。假如 +
号右边是个 CombinedContext,则它的右层还是在本来的方位,左层 left 会和 +
号左边融组成新的左偏结构。即:
val context = element1 + element2 = CombinedContext(left = element1, element = element2)
element3 + context
-> context.fold(element3)
-> operation(element1.fold(element3, operation), element2)
-> operation(operation(element3, element1), element2)
-> operation(CombinedContext(left = element3, element = element1), element2)
-> CombinedContext(CombinedContext(left = element3, element = element1), element2)
用一张图总结这个行为:
minusKey
别离看看 Element 和 CombinedContext 的 minusKey 办法:
// Element
public override fun minusKey(key: Key<*>): CoroutineContext =
// 假如要去掉的是本身,则回来 EmptyCoroutineContext 空上下文,不然回来自己
if (this.key == key) EmptyCoroutineContext else this
// CombinedContext
public override fun minusKey(key: Key<*>): CoroutineContext {
// 假如最右侧便是对应的 Key 就直接回来 left 左边节点
element[key]?.let { return left }
// 从左边节点递归
val newLeft = left.minusKey(key)
return when {
// 左边节点也不包含对应 Key 的元素,则回来本身
newLeft === left -> this
// 左边节点中除了对应 Key 的元素外不包含其他元素,则回来右侧元素
newLeft === EmptyCoroutineContext -> element
// 不然把移除了对应 Key 元素的左边节点和右侧元素组组成新的上下文
else -> CombinedContext(newLeft, element)
}
}
划要点:
在 Context 结构中除掉对应 Key 的元素,然后将剩余的结构按原顺序重新组组成左偏结构。
如下图,假定 A 元素的 Key 是 a:
总结
这篇文章介绍了 CoroutineContext 的数据结构:
- 它是一个
left-biased list
(左偏)结构,是一系列元素的调集,单个元素本身也是一个 CoroutineContext 上下文。介于 set 和 map 之间的结构,所以每个元素都有一个 Key,且对应 Key 的元素是唯一的。它是有层级的,和 set 和 map 这种平面结构不同,遍历时从右到左进行,更右的元素有更高的优先级,会被优先遍历到。 - 两个 CoroutineContext 相加的成果会合并成一个新的左偏结构。
-
+
号右边是 Element 类型,则该元素会把本身作为被追加的元素,成果总会出现在左偏结构的右层; -
+
号右边是 CombinedContext 类型,则它的右层还是在本来的方位,左层 left 会和+
号左边的元素融组成新的左偏结构。
-
通过这种方法,CoroutineContext 能够去拼装上下文,并在父子协程或 withContext 的时候进行传递,每个 coroutine 中都能够具有自己共同的上下文,用来决定这些协程该怎样运转(运转线程,反常处理等)。
后边进一步剖析 Job, Dispatcher, CoroutineExceptionHandler 等上下文 Element 是咋作业的。
文中内容如有错误欢迎指出,共同进步!更新不易,觉得不错的留个赞再走哈~
Android视图体系:Android 视图体系相关的底层原了解析,看完定有收成。
Kotlin专栏:Kotlin 学习相关的博客,包含协程, Flow 等。
Android架构学习之路:架构不是一蹴即至的,希望咱们有一天的时候,能够从自己写的代码中找到架构的成就感,而不是干几票就跑路。作业太忙,更新比较慢,大家有爱好的话能够一同学习。
Android实战系列:记录实践开发中遇到和解决的一些问题。
Android优化系列:记录Android优化相关的文章,继续更新。