Kotlin协程详解——协程上下文

目录

一、上下文结构

get()获取元素

minusKey()删除元素

fold()元素遍历

plus()添加元素

CombinedContext

Key

二、协程名称CoroutineName

三、上下文组合

四、协程作用域CoroutineScope

五、典型用例


协程的上下文,它包含用户定义的一些数据集合,这些数据与协程密切相关。它类似于map集合,可以通过key来获取不同类型的数据。同时CoroutineContext的灵活性很强,如果其需要改变只需使用当前的CoroutineContext来创建一个新的CoroutineContext即可。

在协程启动部分提到,启动协程需要三个部分,其中一个部分就是上下文,其接口类型是CoroutineContext,通常所见的上下文类型是CombinedContext或者EmptyCoroutineContext,一个表示上下文组合,另一个表示空。

协程上下文是Kotlin协程的基本结构单元,主要承载着资源获取,配置管理等工作,是执行环境的通用数据资源的统一管理者。除此之外,也包括携带参数,拦截协程执行等,是实现正确的线程行为、生命周期、异常以及调试的关键。

协程使用以下几种元素集定义协程行为,他们均继承自CoroutineContext:

  1. 【Job】:协程的句柄,对协程的控制和管理生命周期。
  2. 【CoroutineName】:协程的名称,用于调试
  3. 【CoroutineDispatcher】:调度器,确定协程在指定的线程执行
  4. 【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,是这个元素的索引。

JobDispatchersCoroutineName都实现了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提供唯一性保障。

前述内容总结如下:

  1. 协程上下文是一个元素的集合,单个元素本身也是一个上下文,其定义是递归的,自己包含若干个自己。
  2. 协程上下文这个集合有点像 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对应于leftDispatchers.IO对应element。如果再拼接一层CoroutineName(aa)就是这样的

((Job, Dispatchers.IO),CoroutineName)

功能类似与链表,但不同的是你能够拿到上一个与你相连的整体内容。与之对应的就是minusKey方法,从集合中移除对应KeyCoroutineContext实例。

有了这个基础,我们再看它的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方法,所以我们再来看下Elementget方法

public override operator fun <E : Element> get(key: Key<E>): E? =
    @Suppress("UNCHECKED_CAST")
    if (this.key == key) this as E else null

这里使用到了Kotlinoperator操作符重载的特性。那么下面的代码就是等效的。

context.get(CoroutineName)
context[CoroutineName]

所以我们就可以直接通过类似于Map的方式来获取整个协程中CoroutineContext集合中对应KeyCoroutineContext实例。

二、协程名称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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闲暇部落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值