Kotlin系列文章- Kotlin专栏:

  • Kotlin协程之根底运用
  • Kotlin协程之深化了解协程作业原理
  • Kotlin协程之协程取消与反常处理
  • Kotlin协程之再次读懂协程作业原理-推荐
  • Kotlin协程之Flow作业原理
  • Kotlin协程之一文看懂StateFlow和SharedFlow
  • Kotlin协程之一文看懂Channel管道
  • Kotlin系列|一文看懂Lazy机制

概述

协程 CoroutineContext 是运用协程时不可避免会接触到的概念,能够解释为协程上下文,这比较笼统,所以测验去看看它的源码:

深入Kotlin协程系列|图解上下文

好家伙,更笼统了。能看到有个叫 Element 的东西贯穿了 CoroutineContext 的一直,能够猜测 CoroutineContext 相似一个容器,里边存着履行协程需求用到的 Element 元素,那么 CoroutineContext 里边存的是啥呢?又有啥效果?

CoroutineContext

协程中有个常用的写法,相似下面:

scope.launch(Dispatchers.IO + SupervisorJob()) {}

看到这个 + 号,能够猜测是重载了操作符,点进去看看源码:

深入Kotlin协程系列|图解上下文

环顾四周,还有个 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 的状况看起:

深入Kotlin协程系列|图解上下文

  1. Element.fold() 的源码能够知道,fold 办法便是直接履行后边的 operation 代码块,那么参数 acc = Element1element = Element2
  2. Element1.minusKey(Element2.key) 有两条分支:
    1. Element1.key == Element2.key: 则 removed = EmptyCoroutineContext,终究回来 Element2Element1 直接被疏忽掉了。
    2. 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)
        }
    }
  1. 传入的 context = Element3,依据其 fold 办法可知会直接履行后边的 operation 代码块,参数 acc = CombinedContext(left = Element1, element = Element2),element = Element3

  2. 再看看 CombinedContext.minusKey(Element3.key) 办法,也有几条分支:

    深入Kotlin协程系列|图解上下文

    1. Element2.key == Element3.key: 则 removed = Element1,Element2 被除掉了,终究 plus 回来 CombinedContext(left = Element1, element = Element3)

    2. Element2.key != Element3.key: 则接着履行 Element1.minusKey(key),又要依据 Element1.keyElement3.key 是否持平,分两个分支:

      1. 持平,则回来的 newLeft = EmptyCoroutineContext,Element1 被除掉,所以 removed = Element2,终究 plus 回来 CombinedContext(left = Element2, element = Element3)
      2. 不持平,则回来的 newLeft = Element1 == left,所以 removed = CombinedContext(left = Element1, element = Element2),终究 plus 回来 CombinedContext(left = CombinedContext(left = Element1, element = Element2), element = Element3)

划要点:

  1. 关于 Element1 + Element2 + Element3 的状况,假如三者的 key 都不一样,则成果是 CombinedContext(left = CombinedContext(left = Element1, element = Element2), element = Element3)
  2. 假如有 key 持平的状况,则一直会运用 + 号后边的 Element 目标,前面相同 key 的元素都被除掉。比如说假定 Element1 和 Element2 的 key 一样,不等于 Element3 的 key,则成果是 CombinedContext(left = Element2, element = Element3)

这个 plus 操作能够用下图表明,比较明晰的左偏结构:

深入Kotlin协程系列|图解上下文

Get 操作

接下来看看 get 办法。Element 的 get 办法如下:

深入Kotlin协程系列|图解上下文

CombinedContext 的 get 办法如下:

深入Kotlin协程系列|图解上下文

其实便是迭代取,先从 element 开始,假如它的 key 等于要取的 key 就直接回来,不然再从 left 里取,如此循环,直到最后的 left 不再是 CombinedContext 后,直接从它里边取,取不到就回来空的。

划要点:

能够看出 get 的迭代取元素是有优先级的,依据上面画的左偏结构图,越靠右边的元素取出的优先级越高。CombinedContext 是有层级的,它和 set 和 map 这种平面结构不同,遍历时从右到左进行,更右的元素有更高的优先级。

这里再补充一下最开始 plus 办法里疏忽的代码:

深入Kotlin协程系列|图解上下文

能够看到永久会把 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)

用一张图总结这个行为:

深入Kotlin协程系列|图解上下文

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:

深入Kotlin协程系列|图解上下文

总结

这篇文章介绍了 CoroutineContext 的数据结构:

  1. 它是一个 left-biased list (左偏)结构,是一系列元素的调集,单个元素本身也是一个 CoroutineContext 上下文。介于 set 和 map 之间的结构,所以每个元素都有一个 Key,且对应 Key 的元素是唯一的。它是有层级的,和 set 和 map 这种平面结构不同,遍历时从右到左进行,更右的元素有更高的优先级,会被优先遍历到。
  2. 两个 CoroutineContext 相加的成果会合并成一个新的左偏结构。
    1. + 号右边是 Element 类型,则该元素会把本身作为被追加的元素,成果总会出现在左偏结构的右层;
    2. + 号右边是 CombinedContext 类型,则它的右层还是在本来的方位,左层 left 会和 + 号左边的元素融组成新的左偏结构。

深入Kotlin协程系列|图解上下文

通过这种方法,CoroutineContext 能够去拼装上下文,并在父子协程或 withContext 的时候进行传递,每个 coroutine 中都能够具有自己共同的上下文,用来决定这些协程该怎样运转(运转线程,反常处理等)。

后边进一步剖析 Job, Dispatcher, CoroutineExceptionHandler 等上下文 Element 是咋作业的。

文中内容如有错误欢迎指出,共同进步!更新不易,觉得不错的留个再走哈~


Android视图体系:Android 视图体系相关的底层原了解析,看完定有收成。

Kotlin专栏:Kotlin 学习相关的博客,包含协程, Flow 等。

Android架构学习之路:架构不是一蹴即至的,希望咱们有一天的时候,能够从自己写的代码中找到架构的成就感,而不是干几票就跑路。作业太忙,更新比较慢,大家有爱好的话能够一同学习。

Android实战系列:记录实践开发中遇到和解决的一些问题。

Android优化系列:记录Android优化相关的文章,继续更新。