目录
一、上下文结构
get()获取元素
minusKey()删除元素
fold()元素遍历
plus()添加元素
CombinedContext
Key
二、协程名称CoroutineName
三、上下文组合
四、协程作用域CoroutineScope
五、典型用例
协程的上下文,它包含用户定义的一些数据集合,这些数据与协程密切相关。它类似于map
集合,可以通过key
来获取不同类型的数据。同时CoroutineContext
的灵活性很强,如果其需要改变只需使用当前的CoroutineContext
来创建一个新的CoroutineContext
即可。
在协程启动部分提到,启动协程需要三个部分,其中一个部分就是上下文,其接口类型是CoroutineContext,通常所见的上下文类型是CombinedContext或者EmptyCoroutineContext,一个表示上下文组合,另一个表示空。
协程上下文是Kotlin协程的基本结构单元,主要承载着资源获取,配置管理等工作,是执行环境的通用数据资源的统一管理者。除此之外,也包括携带参数,拦截协程执行等,是实现正确的线程行为、生命周期、异常以及调试的关键。
协程使用以下几种元素集定义协程行为,他们均继承自CoroutineContext:
- 【Job】:协程的句柄,对协程的控制和管理生命周期。
- 【CoroutineName】:协程的名称,用于调试
- 【CoroutineDispatcher】:调度器,确定协程在指定的线程执行
- 【CoroutineExceptionHandler】:协程异常处理器,处理未捕获的异常
简而言之,协程上下文是协程必备组成部分,管理了协程的线程绑定、生命周期、异常处理和调试。
一、上下文结构
看一下CoroutineContext的接口定义:
每一个CoroutineContext
都有它唯一的一个Key
其中的类型是Element
,我们可以通过对应的Key
来获取对应的具体对象。说的有点抽象我们直接通过例子来了解。
var context = Job() + Dispatchers.IO + CoroutineName("aa")
LogUtils.d("$context, ${context[CoroutineName]}")
context = context.minusKey(Job)
LogUtils.d("$context")
// 输出
[JobImpl{Active}@158b42c, CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]], CoroutineName(aa)
[CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]]
Element:协程上下文的一个元素,本身就是一个单例上下文,里面有一个key,是这个元素的索引。
Job
、Dispatchers
与CoroutineName
都实现了Element
接口。如果需要结合不同的CoroutineContext
可以直接通过+
拼接,本质就是使用了plus
方法。
可知,Element本身也实现了CoroutineContext接口。
这里我们再看一下官方解释:
/**
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].*/
从官方解释可知,CoroutineContext是一个Element的集合,这种集合被称为indexed set,介于set 和 map 之间的一种结构。set 意味着其中的元素有唯一性,map 意味着每个元素都对应一个键。
如果将协程上下文内部的一系列上下文称为子上下文,上下文为每个子上下文分配了一个Key,它是一个带有类型信息的接口。
这个接口通常被实现为companion object。
源码中定义的子上下文,都会在内部声明一个静态的Key,类内部的静态变量意味着被所有类实例共享,即全局唯一的 Key 实例可以对应多个子上下文实例。
在一个类似 map 的结构中,每个键必须是唯一的,因为对相同的键 put 两次值,新值会代替旧值。通过上述方式,通过键的唯一性保证了上下文中的所有子上下文实例都是唯一的。
我们按照这个格式仿写一下然后反编译。
对比kt和Java文件,可以看到Key就是一个静态变量,且其实现类未做处理,作用与HashMap中的Key类似。
Key是静态变量,全局唯一,为Element提供唯一性保障。
前述内容总结如下:
- 协程上下文是一个元素的集合,单个元素本身也是一个上下文,其定义是递归的,自己包含若干个自己。
- 协程上下文这个集合有点像 set 结构,其中的元素都是唯一的,不重复的。其通过给每一个元素配有一个静态的键实例,构成一组键值对的方式实现。这使其类似 map 结构。这种介于 set 和 map 之间的结构称为indexed set。
get()获取元素
关于CoroutineContext,我们先看一下其是如何取元素的。
这里看一下Element、CombinedContext、EmptyCoroutineContext的内部实现,其中CombinedContext就是CoroutineContext集合结构的实现,EmptyCoroutineContext就表示一个空的CoroutineContext,它里面是空实现。
通过Key检索Element,返回值只能是Element或null,链表节点中的元素值,其中CombinedContext利用while循环实现了类似递归的效果,其中较早被遍历到的元素自然具有较高的优先级。
minusKey()删除元素
同理看一下Element、CombinedContext、EmptyCoroutineContext的内部实现。
internal class CombinedContext(
//左上下文
private val left: CoroutineContext,
//右元素
private val element: Element
) : CoroutineContext, Serializable {
public override fun minusKey(key: Key<*>): CoroutineContext {
//如果element就是要删除的元素,返回left,否则说明要删除的元素在left中,继续从left中删除对应的元素
element[key]?.let { return left }
//在左上下文中去掉对应元素
val newLeft = left.minusKey(key)
return when {
//如果left中不存在要删除的元素,那么当前CombinedContext就不存在要删除的元素,直接返回当前CombinedContext实例
newLeft === left -> this
//如果left中存在要删除的元素,删除了这个元素后,left变为了空,那么直接返回当前CombinedContext的element就行
newLeft === EmptyCoroutineContext -> element
//如果left中存在要删除的元素,删除了这个元素后,left不为空,那么组合一个新的CombinedContext返回
else -> CombinedContext(newLeft, element)
}
}
......
}
public object EmptyCoroutineContext : CoroutineContext, Serializable {
public override fun minusKey(key: Key<*>): CoroutineContext = this
......
}
public interface Element : CoroutineContext {
//如果key和自己的key匹配,那么自己就是要删除的Element,返回EmptyCoroutineContext(表示删除了自己),否则说明自己不需要被删除,返回自己
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
......
}
如果把CombinedContext和Element结合来看,那么CombinedContext的整体结构如下:
其结构类似链表,left就是指向下一个结点的指针,get、minusKey操作大体逻辑都是先访问当前element,不满足,再访问left的element,顺序都是从right到left。
fold()元素遍历
fold也是递归的形式操作,fold的操作大体逻辑是:先访问left,直到递归到最后的element,然后再从left到right的返回,从而访问了所有的element。
plus()添加元素
关于CoroutineContext的元素添加方法,直接看其plus()实现,也是唯一没有被重写的方法。
public operator fun plus(context: CoroutineContext): CoroutineContext =
//如果要相加的CoroutineContext为空,那么不做任何处理,直接返回
if (context === EmptyCoroutineContext) this else
//如果要相加的CoroutineContext不为空,那么对它进行fold操作,可以把acc理解成+号左边的CoroutineContext,element理解成+号右边的CoroutineContext的某一个element
context.fold(this) { acc, element ->
//首先从左边CoroutineContext中删除右边的这个element
val removed = acc.minusKey(element.key)
//如果removed为空,说明左边CoroutineContext删除了和element相同的元素后为空,那么返回右边的element即可
if (removed === EmptyCoroutineContext) element else {
//如果removed不为空,说明左边CoroutineContext删除了和element相同的元素后还有其他元素,那么构造一个新的CombinedContext返回
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)
}
}
}
plus方法大部分情况下返回一个CombinedContext,即我们把两个CoroutineContext相加后,返回一个CombinedContext,在组合成CombinedContext时,+号右边的CoroutineContext中的元素会覆盖+号左边的CoroutineContext中的含有相同key的元素。plus
的实现逻辑是将两个拼接的CoroutineContext
封装到CombinedContext
中组成一个拼接链,同时每次都将ContinuationInterceptor
添加到拼接链的最尾部.
这个覆盖操作就在fold方法的参数operation代码块中完成,通过minusKey方法删除掉重复元素。
plus方法中可以看到里面有个对ContinuationInterceptor的处理,目的是让ContinuationInterceptor在每次相加后都能变成CoroutineContext中的最后一个元素。
ContinuationInterceptor继承自Element,称为协程上下文拦截器,作用是在协程执行前拦截它,从而在协程执行前做出一些其他的操作。通过把ContinuationInterceptor放在最后面,协程在查找上下文的element时,总能最快找到拦截器,避免了递归查找,从而让拦截行为前置执行。
CombinedContext
internal class CombinedContext(
private val left: CoroutineContext,
private val element: Element
) : CoroutineContext, Serializable {
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]
}
}
}
...
}
注意看它的两个参数,我们直接拿上面的例子来分析
Job() + Dispatchers.IO
(Job, Dispatchers.IO)
Job
对应于left
,Dispatchers.IO
对应element
。如果再拼接一层CoroutineName(aa)
就是这样的
((Job, Dispatchers.IO),CoroutineName)
功能类似与链表,但不同的是你能够拿到上一个与你相连的整体内容。与之对应的就是minusKey
方法,从集合中移除对应Key
的CoroutineContext
实例。
有了这个基础,我们再看它的get
方法就很清晰了。先从element
中去取,没有再从之前的left
中取。
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)"
}
很简单它的Key
就是CoroutineContext.Key<CoroutineName>
,当然这样还不够,需要继续结合对于的operator get
方法,所以我们再来看下Element
的get
方法
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
这里使用到了Kotlin
的operator
操作符重载的特性。那么下面的代码就是等效的。
context.get(CoroutineName)
context[CoroutineName]
所以我们就可以直接通过类似于Map
的方式来获取整个协程中CoroutineContext
集合中对应Key
的CoroutineContext
实例。
二、协程名称CoroutineName
CoroutineName是用户用来指定的协程名称的,用于方便调试和定位问题。
协程内部可以通过coroutineContext这个全局属性直接获取当前协程的上下文。
三、上下文组合
如果要传递多个上下文元素,CoroutineContext可以使用"+"运算符进行合并。由于CoroutineContext是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的CoroutineContext。
如果有重复的元素(key一致)则右边的会代替左边的元素,相关原理参看协程上下文结构章节。
四、协程作用域CoroutineScope
CoroutineScope实际上是一个CoroutineContext的封装,当我们需要启动一个协程时,会在CoroutineScope的实例上调用构建函数,如async和launch。
在构建函数中,一共出现了3个CoroutineContext。
查看协程构建函数async和launch的源码,其第一行都是如下代码:
进一步查看:
构建器内部进行了一个CoroutineContext拼接操作,plus左值是CoroutineScope内部的CoroutineContext,右值是作为构建函数参数的CoroutineContext。
抽象类AbstractCoroutineScope实现了CoroutineScope和Job接口。大部分CoroutineScope的实现都继承自AbstractCoroutineScope,意味着他们同时也是一个Job。
从上述分析可知:coroutine context = parent context + coroutine job
五、典型用例
全限定Context
launch( Dispatchers.Main + Job() + CoroutineName("HelloCoroutine") + CoroutineExceptionHandler { _, _ -> /* ... */ }) {
/* ... */
}
全限定Context,即全部显式指定具体值的Elements。不论你用哪一个CoroutineScope构建该协程,它都具有一致的表现,不会受到CoroutineScope任何影响。
CoroutineScope Context
基于Activity生命周期实现一个CoroutineScope
Dispatcher:使用Dispatcher.Main,以在UI线程进行绘制
Job:在onCreate时构建,在onDestroy时销毁,所有基于该CoroutineContext创建的协程,都会在Activity销毁时取消,从而避免Activity泄露的问题
临时指定参数
CoroutineContext的参数主要有两个来源:从scope中继承+参数指定。我们可以用withContext便捷地指定某个参数启动子协程,例如我们想要在协程内部执行一个无法被取消的子协程:
读取协程上下文参数
通过顶级挂起只读属性coroutineContext获取协程上下文参数,它位于 kotlin-stdlib / kotlin.coroutines / coroutineContext
Nested Context内嵌上下文
内嵌上下文切换:在协程A内部构建协程B时,B会自动继承A的Dispatcher。
可以在调用async时加入Dispatcher参数,切换到工作线程
推荐文章
https://zhuanlan.zhihu.com/p/552225674
Kotlin协程实现原理:Suspend&CoroutineContext