Kotlin coroutineContext源碼層深入分析

1.CoroutineContext

表示一個元素或者是元素集合的接口。它有一個Key(索引)的Element實例集合,每一個Element的實例也是一個CoroutineContext,即集合中每個元素也是集合。

如下圖所示,CoroutineContext的常見官方實現有以下幾種(少見的或者自定義的實現就不列舉,以後再聊):

  • Job:協程實例,控制協程生命周期(new、acruve、completing、conpleted、cancelling、cancelled)。
  • CoroutineDIspatcher:協程調度器,給指定線程分發協程任務(IO、Default、Main、Unconfined)。
  • CoroutineName:協程名稱,用於定義協程的名稱,調試打印信息使用。
  • CoroutineExceptionHandler:協程異常處理器,用於處理未捕獲的異常。

2.Element的作用

Element類也是繼承自CoroutineContext接口的,該類的作用是給子類保留一個Key成員變量,用於在集合查詢的時候可以快速查找到目標coroutineContext,Key成員變量是一個泛型變量,每個繼承自Element的子類都會去覆蓋實現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)"
}
@SinceKotlin("1.3")
public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element
/**
 * Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
 */
public interface Key<E : Element>
/**
 * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
 */
public interface Element : CoroutineContext {
    /**
     * A key of this coroutine context element.
     */
    public val key: Key<*>
    public override operator fun <E : Element> get(key: Key<E>): E? =
        @Suppress("UNCHECKED_CAST")
        if (this.key == key) this as E else null
    public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
        operation(initial, this)
    public override fun minusKey(key: Key<*>): CoroutineContext =
        if (this.key == key) EmptyCoroutineContext else this
}

上面的CoroutineName構造函數定義為

public data class CoroutineName( 
    /**
     * User-defined coroutine name.
     */
    val name: String
) : AbstractCoroutineContextElement(CoroutineName)

父類構造函數中傳遞的參數是CoroutineName,但是我們發現CoroutineName也不是Key接口public interface Key<E : Element>的實現,為啥可以這樣直接傳遞呢?但是我們仔細看發現CoroutineName類定義瞭伴生對象: public companion object Key : CoroutineContext.Key<CoroutineName>,在kotlin中伴生對象是可以直接省略 類.companion.調用方式的,CoroutineName類也就代表著伴生對象,所以可以直接作為CoroutineName父類構造函數的參數,神奇的kotlin語法搞得我一愣一愣的。

類似的還有Job,CoroutineDIspatcher,CoroutineExceptionHandler的成員變量Key的覆蓋實現:

//Job
public interface Job : CoroutineContext.Element {
    /**
     * Key for [Job] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<Job> { //省略 }
     //省略 
}
//CoroutineExceptionHandler
public interface CoroutineExceptionHandler : CoroutineContext.Element {
    /**
     * Key for [CoroutineExceptionHandler] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
 	//省略 
}
// CoroutineDIspatcher
@SinceKotlin("1.3")
public interface ContinuationInterceptor : CoroutineContext.Element {
    /**
     * The key that defines *the* context interceptor.
     */
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>
}

3.CoroutineContext相關的操作符原理解析

CoroutineContext的操作符??有點莫名其妙的感覺,僅僅憑借我的直覺的話很難理解,但是平常使用協程的過程中,我們經常會使用這些相關的操作符,比如 +,[]等等符號,下面代碼示例:

val comb = Job() + CoroutineName("")
val cName = comb[CoroutineName]

上面的+代表兩個coroutineContext合並到集合中,這裡的集合實際上是一個鏈表,後面會講到。

上面的[]代表著從集合中索引出CoroutineName類型的CoroutineContext,這裡也可以看出來僅僅通過key就查找出元素和map很相似,那麼可以知道value是唯一的。key都是coroutineContext子類作為泛型類型的,具有唯一性,那也可以間接推斷出上面+操作其實也會覆蓋擁有相同key的value的值。

還有其他操作函數:fold展開操作, minusKey刪除集合中存在的元素。

還有一個問題就是,這個集合到底是什麼類型的集合,已經如何管理的,我們來一一解答:

3.1.什麼類型的集合

CoroutineConetxt集合是鏈表結構的集合,是一個從本節點開始,向左遍歷parent節點的一個鏈表,節點的都是CoroutineContext的子類,分為Element,CombinedContext,EmptyCoroutineContext三種。

有以下代碼作為舉例:

val scope = CoroutineScope(CoroutineName("") + Job() + CoroutineExceptionHandler{<!--{C}%3C!%2D%2D%20%2D%2D%3E--> _, _ -> } + Dispatchers.Default)

假如CoroutineScope自己的coroutineContext變量集合中是包含CoroutineName,Job,CoroutineExceptionHanlder,CoroutineDIspatcher四種上下文的,那麼他們組成的集合結構可能就會是下圖所示的鏈表結構,

使用scope查找對應的Job的話直接調用scope[Job]方法,替代Job的話調用 scope + Job(),看源碼就是使用scope的上下文集合替換Job

public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =
    ContextScope(coroutineContext + context)

為啥是鏈表結構的集合呢,接下來直接看源碼就知道瞭。

3.2.如何管理

我們集合的鏈表結構,每個節點都是CombinedContext類型,裡面包含瞭element,left兩個成員變量,left指向鏈表的左邊,element表示當前節點的上下文元素(一般是job,name,handler,dispatcher四種),鏈表的最左端節點一定是Element元素

Element

主要實現在combinedContext,Element元素的方法實現比較簡單,不單獨列舉。

combinedContext

構造函數

@SinceKotlin("1.3")
internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {

get函數:

	//Element
    public override operator fun <E : Element> get(key: Key<E>): E? =
          @Suppress("UNCHECKED_CAST")
          if (this.key == key) this as E else null
	//CombinedContext
    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]
            }
        }
    }

在代碼中一般不會使用get方法,而是使用context[key]來代替,類似於map集合的查詢。上下文是Element類型,key是對應類型那麼返回當前Element,不是當前類型,返回null;上下文是CombinedContext類型,指針cur指向當前節點,while循環開始,當前的element元素的key查找到瞭,那麼就返回當前combinedContext,如果沒找到,那麼將指針指向left節點,如果left節點是combinedContext類型,那麼重復上述操作,如果是Element類型直接判斷是否可以查找到key值。那麼從這裡看出鏈表的最左端元素一定是Element節點。

contain函數

    private fun contains(element: Element): Boolean =
        get(element.key) == element
    private fun containsAll(context: CombinedContext): Boolean {
        var cur = context
        while (true) {
            if (!contains(cur.element)) return false
            val next = cur.left
            if (next is CombinedContext) {
                cur = next
            } else {
                return contains(next as Element)
            }
        }
    }

類似於get操作,contains函數直接調用get方法來判斷元素是不是和傳入參數相等。

containAll函數就是遍歷參數的鏈表節點是不是都包含在當前鏈表中。

fold函數

   	//coroutineContext
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R

從表面意思就是展開操作,第一個入參 CoroutineContext,第二個入參 lambda表達式 用表達式的兩個參數CoroutineContext, Element 返回一個新的 CoroutineContext:

    operation :(R , Element) -> R

	Job.fold(CoroutineName("測試"),{ coroutineContext , element ->
        TODO("return new CoroutineContext")
    }) //example
    //Element
	public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
    	operation(initial, this)

作為receiver的上下文是Element,調用fold的話,是讓ELement和入參的CoroutineContext作為lambda表達式 的兩個參數調用該lambda表達式返回結果。

    MainScope().coroutineContext.fold(Job(),{ coroutineContext , element ->
        TODO("return new CoroutineContext")
    }) //example
	//CombinedContext
	public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
        operation(left.fold(initial, operation), element)

作為receiver的上下文是CombinedContext,調用fold的話,是讓left深度遞歸調用fold函數,一直到鏈表的最左端節點,我們知道鏈表的最左端節點一定是Element,那麼根據上面的代碼,Element的fold函數內調用operation返回一個CoroutineContext後,遞歸回溯到上一層,繼續調用operation返回一個CoroutineContext,繼續回溯,一直回溯到開始調用MainScope().coroutineContext.fold(Job的地方。如下圖所示:

minusKey函數

該函數的意思是從上下文集合中刪除key對應的上下文。

  	//Element
    public override fun minusKey(key: Key<*>): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this

接收者receiver是Element類型的話,如果入參key和receiver是相等的話,那麼返回EmptyCoroutineContext空上下文,否則返回receiver本身。(可見找到得到key的話會返回空上下文,找不到的話返回本身)

	//CombinedContext
	public override fun minusKey(key: Key<*>): CoroutineContext {
	    element[key]?.let { return left }
	    val newLeft = left.minusKey(key)
	    return when {
	        newLeft === left -> this
	        newLeft === EmptyCoroutineContext -> element
	        else -> CombinedContext(newLeft, element)
	    }
	}

接收者receiver是CombinedContext類型的話,

  • element[key]不為空說明當前節點就是要找的節點,直接返回該節點的left節點(代表著把當前節點跳過,也就是移除該節點)。
  • element[key]為空那麼說明當前節點不是要找的節點,需要向鏈表的左端left去尋找目標,深度遞歸遍歷left.minusKey(key),返回的newLeft有三種情況:
  • newLeft === left,在左邊找不到目標Key(根據Element.minusKey函數發現,返回的是this的話就是key沒有匹配到),從該節點到左端節點都可以返回。
  • newLeft === EmptyCoroutineContext ,在左邊找到瞭目標key(根據Element.minusKey函數發現,返回的是EmptyCoroutineContext 的話key匹配到瞭Element),找到瞭目標那麼需要將目標跳過,那麼從本節點開始返回,左邊節點需要跳過移除,該節點就成瞭鏈表的最左端節點Element。
  • 不是上述的情況,那麼就是newLeft是觸發瞭1.或者4.情況,返回的是left的element元素,或者是本節點的left節點跳過瞭,返回的是left.left節點,這樣將newLeft和本節點的element構造出新的CombinedContext節點。

上述操作,都隻是在鏈表上跳過節點,然後將跳過的節點左節點left和右節點創建新的CombinedContext,產生一個新的鏈表出來。

操作例子:

刪除最左端節點

刪除中間節點:

結論:minusKey的操作隻是將原始鏈表集合中排除某一個節點,然後復制一個鏈表返回,所以並不會影響原始集合

plus函數

該函數重寫+操作符,函數定義operator fun plus(context: CoroutineContext): CoroutineContext ,作用是對上下文集合進行添加(相同會覆蓋)指定上下文操作。這個函數隻有CoroutineContext實現瞭,代碼如下:

    /**
     * Returns a context containing elements from this context and elements from  other [context].
     * The elements from this context with the same key as in the other one are dropped.
     */
    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
                    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)
                    }
                }
            }

入參如果是EmptyContext,那麼直接返回;不是空的話,對入參進行fold操作,上面講瞭fold操作是將context鏈表展開,從鏈表最左端開始向context回溯調用fold函數的入參lambda表達式。那麼我們就知道瞭![A (CombineContext) + B(CombineContext)](https://img-blog.csdnimg.cn/40fb2861842a4c8f8c918752e3f89fd0.png) 是如何操作的瞭,首先B作為plus的入參,那麼B先展開到B鏈表結構的最左端,然後執行lambda操作{ acc, element -> ... }, 這個lambda裡面

第一步

context.fold(this) { acc, element ->
	acc.minusKey(Element.Key)
	// ...
}
//CombineContext的實現
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
    operation(left.fold(initial, operation), element)

根據CombineContext的實現,知道lambda的acc參數是A (CombineContext),element參數是B(CombineContext)的fold遞歸的當前位置的element的元素,acc.minusKey(Element.Key)所做的事情就是移除A (CombineContext)鏈表中的B(CombineContext)的element元素。

第二步

if (removed === EmptyCoroutineContext) element else {
	// make sure interceptor is always last in the context (and thus is fast to get when present)
	val interceptor = removed[ContinuationInterceptor]
	// ...
}

第一步移除掉element之後,判斷剩餘的removed鏈表是不是empty的,如果為空,返回B(CombineContext)的fold遞歸位置的element元素;不為空,接著從removed鏈表中獲取ContinuationInterceptor上下文(也就是dispatcher)。

第三步

if (interceptor == null) CombinedContext(removed, element) else {
    val left = removed.minusKey(ContinuationInterceptor)
    if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
        CombinedContext(CombinedContext(left, element), interceptor)
}

獲取的interceptor為空,那將element和removed鏈表構造出一個新的CombinedContext節點返回;如果不為空,從removed鏈表中移除interceptor返回一個不包含interceptor的鏈表left;移除後left鏈表為空,那麼將element和interceptor構造出一個新的CombinedContext節點返回;left鏈表不為空,那麼將left, element構造出一個新的CombinedContext節點,將新的CombinedContext節點和interceptor早構造出一個新的節點返回。

每一層遞歸fold操作結束後,返回一個新的context給上一層繼續遞歸,直到結束為止。

操作例子圖如下:

有如下兩個集合A (CombineContext) + B(CombineContext)

第一次遞歸回溯:

第二次遞歸回溯:

回溯深度取決於入參B的鏈表長度,B有多長回溯就會發生幾次,這裡沒有加入interceptor上下文元素,減少畫圖復雜度。

plus操作結論:

1.發現每次返回節點的時候,都會將interceptor移除後,放到節點的最右邊的位置,可以知道interceptor一定在鏈表的頭部;

2.lambda表達式中,一定會先移除掉相同key的上下文元素,然後用後加入的element和left鏈表新建一個CombinedContext節點插入到頭部

3.plus操作會覆蓋掉有相同key的上下文元素

4.驗證以及總結

經過對上面的源碼的分析,可以推斷出一些上下文元素的操作符操作後,集合的元素排列狀態。比如下面操作:

    private fun test() {
        val coroutineContext = Job() + CoroutineName("name1") + Dispatchers.IO + CoroutineExceptionHandler{ c,e -> }
        Log.i(TAG, "coroutineContext $coroutineContext")
        val newContext = coroutineContext + SupervisorJob()
        Log.i(TAG, "newContext $newContext")
        val newContext2 = newContext + (Job() + CoroutineName("name2"))
        Log.i(TAG, "newContext2 $newContext2")
        Log.i(TAG, "newContext2[CoroutineName] ${newContext2[CoroutineName]}")
    }

打印的日志如下:

I/MainActivity: coroutineContext [
                                    JobImpl{Active}@b32c44, 
                                    CoroutineName(name1), 
                                    com.meeting.kotlinapplication.MainActivity$test$$inlined$CoroutineExceptionHandler$1@45ec12d, 
                                    Dispatchers.IO
                                 ]

I/MainActivity: newContext [
                                CoroutineName(name1), 
                                com.meeting.kotlinapplication.MainActivity$test$$inlined$CoroutineExceptionHandler$1@45ec12d, 
                                SupervisorJobImpl{Active}@1022662, 
                                Dispatchers.IO
                            ]
                            
I/MainActivity: newContext2 [
                                com.meeting.kotlinapplication.MainActivity$test$$inlined$CoroutineExceptionHandler$1@45ec12d, 
                                JobImpl{Active}@76863f3, 
                                CoroutineName(name2), 
                                Dispatchers.IO
                            ]

I/MainActivity: newContext2[CoroutineName] CoroutineName(name2)

I/MainActivity: coroutineContext [
                                    JobImpl{Active}@b32c44, 
                                    CoroutineName(name1), 
                                    com.meeting.kotlinapplication.MainActivity$test$$inlined$CoroutineExceptionHandler$1@45ec12d, 
                                    Dispatchers.IO
                                 ]

可以看出來:

1. Dispatchers元素一定是在鏈表的頭部;

2. 重復key的元素會被後加入的元素覆蓋,集合中不存在重復key的元素;

3. +操作後返回新的鏈表集合,不會影響原始集合鏈表結構

上面總結的這些性質,可以很好的為job協程的父子關系,子job繼承父job的上下文集合這些特性,下一篇我將講解 協程Job父子關系的原理。

到此這篇關於Kotlin coroutineContext源碼層深入分析的文章就介紹到這瞭,更多相關Kotlin coroutineContext內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: