​1. 前言​

如果你对CoroutineContext不了解,本文值得你细细品读,如果一遍看不懂,不妨多读几遍。写作该文的过程也是我对CoroutineContext理解加深的过程。CoroutineContext是协程的基础,值得投入学习

Android开发者对Context都不陌生。在Android系统中,Context可谓神通广大,它可以获取应用资源,可以获取系统资源,可以启动Activity。Context有几个大名鼎鼎的子类,Activity、Application、Service,它们都是应用中非常重要的组件。

协程中也有个类似的概念,CoroutineContext。它是协程中的上下文,通过它我们可以控制协程在哪个线程中执行,可以设置协程的名字,可以用它来捕获协程抛出的异常等。

我们知道,通过CoroutineScope.launch方法可以启动一个协程。该方法第一个参数的类型就是CoroutineContext。默认值是EmptyCoroutineContext单例对象。用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_链表

在开始讲解CoroutineContext之前我们来看一段协程中经常会遇到的代码用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_链表_02

刚开始学协程的时候,我们经常会和Dispatchers.Main、Job、CoroutineName、CoroutineExceptionHandler打交道,它们都是CoroutineContext的子类。我们也很容易单独理解它们,Dispatchers.Main指把协程分发到主线程执行,Job可以管理协程的生命周期,CoroutineName可以设置协程的名字,CoroutineExceptionHandler可以捕获协程的异常。但是​​+​​​操作符对大部分的Java开发者甚至Kotlin开发者而言会感觉到新鲜又难懂,在协程中CoroutineContext​​+​​到底是什么意思?

其实+操作符就是把两个CoroutineContext合并成一个链表,后文会详细讲解

​2. CoroutineContext类图一览​

用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_头插法_03

根据类图结构我们可以把它分成四个层级:

  1. CoroutineContext 协程中所有上下文相关类的父接口。
  2. CombinedContext、Element、EmptyCoroutineContext。它们是CoroutineContext的直接子类。
  3. AbstractCoroutineContextElement、Job。这两个是Element的直接子类。
  4. CoroutineName、CoroutineExceptionHandler、CoroutineDispatcher​​(包含Dispatchers.Main和Dispatchers.Default)​​。它们是AbstractCoroutineContextElement的直接子类。

图中红框处,CombinedContext定义了size()和contains()方法,这与集合操作很像,CombinedContext是CoroutineContext对象的集合,而Element和EmptyCoroutineContext却没有定义这些方法,真正实现了集合操作的协程上下文只有CombinedContext,后文会详细讲解

​3. CoroutineContext接口​

​CoroutineContext源码如下:​​​用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_头插法_04​​​首先​​我们看下官方注释,我将它的作用归纳为:

Persistent context for the coroutine. It is an indexed set of [Element] instances. An indexed set is a mix between a set and a map. Every element in this set has a unique [Key].

  1. CoroutineContext是协程的上下文。
  2. CoroutineContext是element的set集合,没有重复类型的element对象。
  3. 集合中的每个element都有唯一的Key,Key可以用来检索元素。

​相信大多数的人看到这样的解释时,都会心生疑惑,既然是set类型为啥不直接用HashSet来保存Element。CoroutineContext的实现原理又是什么呢?原因是考虑到协程嵌套,用链表实现更好。​

​接着​​​我们来看下该接口定义的几个方法用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_头插法_05

​4. Key接口​

用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_子类_06

Key是一个接口定义在CoroutineContext中的一个接口,作为接口它没有声明任何的方法,那么其实它没有任何真正有用的意义,它只是用来检索。我们先来看下,协程库中是如何使用Key接口的。用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_子类_07通过观察协程官方库中的例子,我们发现Element的子类都必须重写Key这个属性,而且Key的泛型类型必须和类名相同。以CoroutineName为例,Key是一个伴生对象,同时Key的泛型类型也是CoroutineName。

​为了方便理解,​​我仿照写了MyElement类,如下:

用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_链表_08

​通过对比kt类和反编译的java类我们看到​​ Key就是一个静态变量,而且它的实现类,其实啥也没干。它的作用与HashMap中的Key类似:

  1. 实现key-value功能,为插入和删除提供检索功能
  2. Key是static静态变量,全局唯一,为Element提供唯一性保障

​Kotlin语法糖​

coroutineContext.get(CoroutineName.Key)

coroutineContext.get(CoroutineName)

coroutineContext[CoroutineName]

coroutineContext[CoroutineName.Key]

写法是等价的

​4. CoroutineContext.get方法​

​源码(整理在一起,下同)​​​用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_子类_09

​使用方式​​​用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_链表_10

​讲解​

通过Key检索Element。返回值只能是Element或者null,链表节点中的元素值。

  1. Element get方法:只要Key与当前Element的Key匹配上了,返回该Element否则返回null。
  2. CombinedContext get方法:遍历链表,查询与Key相等的Element,如果没找到返回null。

​5. CoroutineContext.plus方法​

​源码​​​用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_链表_11

​使用方式​​​用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_链表_12

​讲解​

将两个CoroutineContext组合成一个CoroutineContext,如果是两个类型相同的Element会返回一个新的Element。如果是两个不同类型的Element会返回一个CombinedContext。如果是多个不同类型的Element会返回一条CombinedContext链表。

我将上述算法总结成了5种场景,不过在介绍这5种场景前,我们先讲解CombinedContext的数据结构。

​6. CombinedContext分析​

用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_头插法_13

因为CombinedContext是CoroutineContext的子类,left也是CoroutineContext类型的,所以它的数据结构是链表。我们经常用next来表示链表的下一个节点。那么为什么这里取名叫left呢?我甚至怀疑写这段代码的是个左撇子。真正的原因是,协程可以启动子协程,子协程又可以启动孙协程。父协程在左边,子协程在右边

用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_头插法_14

嵌套启动协程用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_链表_15越是外层的协程的Context越在左边,大概示意图如下 (真实并非如此,比这更复杂)用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_头插法_16

链表的两个知识点在此都有体现。CoroutineContext.plus方法中使用的是头插法。CombinedContext的toString方法采用的是链表倒序打印法。

​7. 五种plus场景​

根据plus源码,我总结出会覆盖到五种场景。用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_子类_17

  1. plus EmptyCoroutineContext
  2. plus 相同类型的Element
  3. plus方法的调用方没有Dispatcher相关的Element
  4. plus方法的调用方只有Dispatcher相关的Element
  5. plus方法的调用方是包含Dispatcher相关Element的链表

结果如下:

  1. ​Dispatchers.Main + EmptyCoroutineContext​​​ 结果:​​Dispatchers.Main​​。
  2. ​CoroutineName("c1") + CoroutineName("c2")​​​结果: ​​CoroutineName("c2")​​。相同类型的直接替换掉。
  3. ​CoroutineName("c1") + Job()​​​结果:​​CoroutineName("c1") <- Job​​。头插法被plus的(​​Job​​)放在链表头部
  4. ​Dispatchers.Main + Job()​​​结果:​​Job <- Dispatchers.Main​​。虽然是头插法,但是ContinuationInterceptor必须在链表头部。
  5. ​Dispatchers.Main + Job() + CoroutineName("c5")​​​结果:​​Job <- CoroutineName("c5") <- Dispatchers.Main​​。Dispatchers.Main在链表头部,其它的采用头插法。

如果不考虑Dispatchers.Main的情况。我们可以把​​+​​​用​​<-​​​代替。​​CoroutineName("c1") + Job()​​​等价于​​CoroutineName("c1") <- Job​

​8. CoroutineContext的minusKey方法​

​源码​​​用了20多张图终于把协程上下文CoroutineContext彻底搞懂了_子类_18

​讲解​

  1. Element minusKey方法:如果Key与当前element的Key相等,返回EmptyCoroutineContext,否则相当于没减成功,返回当前element
  2. CombinedContext minusKey方法:删除链表中符合条件的节点,分三种情况。

三种情况以下面链表为例

Job <- CoroutineName("c5") <-Dispatchers.Main

  1. 没找到节点:minusKey(MyElement)。在Job节点处走newLeft === left分支,依此类推,在CoroutineName处走同样的分支,在Dispatchers.Main处走同样的分支。
  2. 节点在尾部:minusKey(Job)。在CoroutineName("c5")节点走newLeft === EmptyCoroutineContext分支,依此往头部递归
  3. 节点不在尾部:minusKey(CoroutineName)。在Dispatchers.Main节点处走else分支

​9.  总结​

学习CoroutineContext首先要搞清楚各类之间的继承关系,其次,CombinedContext各具体Element的集合,它的数据结构是链表,如果读者对链表增删改查操作熟悉的话,那么很容易就能搞懂CoroutineContext原理,否则想要搞懂CoroutineContext那简直如盲人摸象。