Kotlin协程重新认知 CoroutineContext
轉載請標明出處:http://blog.csdn.net/zhaoyanjun6/article/details/121972791
本文出自【趙彥軍的博客】
文章目錄
- 前言
- 2. CoroutineContext類圖一覽
- 3. CoroutineContext接口
- 4. Key接口
- 4. CoroutineContext.get方法
- 5. CoroutineContext.plus方法
- 6. CombinedContext分析
- 7. 五種plus場景
- 8. CoroutineContext的minusKey方法
- 9. 總結
前言
如果你對CoroutineContext不了解,本文值得你細細品讀,如果一遍看不懂,不妨多讀幾遍。寫作該文的過程也是我對CoroutineContext理解加深的過程。CoroutineContext是協程的基礎,值得投入學習
Android開發者對Context都不陌生。在Android系統中,Context可謂神通廣大,它可以獲取應用資源,可以獲取系統資源,可以啟動Activity。Context有幾個大名鼎鼎的子類,Activity、Application、Service,它們都是應用中非常重要的組件。
協程中也有個類似的概念,CoroutineContext。它是協程中的上下文,通過它我們可以控制協程在哪個線程中執行,可以設置協程的名字,可以用它來捕獲協程拋出的異常等。
我們知道,通過CoroutineScope.launch方法可以啟動一個協程。該方法第一個參數的類型就是CoroutineContext。默認值是EmptyCoroutineContext單例對象。
在開始講解CoroutineContext之前我們來看一段協程中經常會遇到的代碼
剛開始學協程的時候,我們經常會和Dispatchers.Main、Job、CoroutineName、CoroutineExceptionHandler打交道,它們都是CoroutineContext的子類。我們也很容易單獨理解它們,Dispatchers.Main指把協程分發到主線程執行,Job可以管理協程的生命周期,CoroutineName可以設置協程的名字,CoroutineExceptionHandler可以捕獲協程的異常。但是+操作符對大部分的Java開發者甚至Kotlin開發者而言會感覺到新鮮又難懂,在協程中CoroutineContext+到底是什么意思?
其實+操作符就是把兩個CoroutineContext合并成一個鏈表,后文會詳細講解
2. CoroutineContext類圖一覽
根據類圖結構我們可以把它分成四個層級:
- CoroutineContext 協程中所有上下文相關類的父接口。
- CombinedContext、Element、EmptyCoroutineContext。它們是CoroutineContext的直接子類。
- AbstractCoroutineContextElement、Job。這兩個是Element的直接子類。
- CoroutineName、CoroutineExceptionHandler、CoroutineDispatcher(包含Dispatchers.Main和Dispatchers.Default)。它們是AbstractCoroutineContextElement的直接子類。
圖中紅框處,CombinedContext定義了size()和contains()方法,這與集合操作很像,CombinedContext是CoroutineContext對象的集合,而Element和EmptyCoroutineContext卻沒有定義這些方法,真正實現了集合操作的協程上下文只有CombinedContext,后文會詳細講解
3. CoroutineContext接口
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是協程的上下文。
- CoroutineContext是element的set集合,沒有重復類型的element對象。
- 集合中的每個element都有唯一的Key,Key可以用來檢索元素。
相信大多數的人看到這樣的解釋時,都會心生疑惑,既然是set類型為啥不直接用HashSet來保存Element。CoroutineContext的實現原理又是什么呢?原因是考慮到協程嵌套,用鏈表實現更好。
接著我們來看下該接口定義的幾個方法
4. Key接口
Key是一個接口定義在CoroutineContext中的一個接口,作為接口它沒有聲明任何的方法,那么其實它沒有任何真正有用的意義,它只是用來檢索。我們先來看下,協程庫中是如何使用Key接口的。
通過觀察協程官方庫中的例子,我們發現Element的子類都必須重寫Key這個屬性,而且Key的泛型類型必須和類名相同。以CoroutineName為例,Key是一個伴生對象,同時Key的泛型類型也是CoroutineName。
為了方便理解,我仿照寫了MyElement類,如下:
通過對比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方法
源碼(整理在一起,下同)
使用方式
講解
通過Key檢索Element。返回值只能是Element或者null,鏈表節點中的元素值。
- 1、Element get方法:只要Key與當前Element的Key匹配上了,返回該Element否則返回null。
- 2、CombinedContext get方法:遍歷鏈表,查詢與Key相等的Element,如果沒找到返回null。
5. CoroutineContext.plus方法
源碼
使用方式
講解
將兩個CoroutineContext組合成一個CoroutineContext,如果是兩個類型相同的Element會返回一個新的Element。如果是兩個不同類型的Element會返回一個CombinedContext。如果是多個不同類型的Element會返回一條CombinedContext鏈表。
我將上述算法總結成了5種場景,不過在介紹這5種場景前,我們先講解CombinedContext的數據結構。
6. CombinedContext分析
因為CombinedContext是CoroutineContext的子類,left也是CoroutineContext類型的,所以它的數據結構是鏈表。我們經常用next來表示鏈表的下一個節點。那么為什么這里取名叫left呢?我甚至懷疑寫這段代碼的是個左撇子。真正的原因是,協程可以啟動子協程,子協程又可以啟動孫協程。父協程在左邊,子協程在右邊。
嵌套啟動協程
越是外層的協程的Context越在左邊,大概示意圖如下 (真實并非如此,比這更復雜)
鏈表的兩個知識點在此都有體現。CoroutineContext.plus方法中使用的是頭插法。CombinedContext的toString方法采用的是鏈表倒序打印法。
7. 五種plus場景
根據plus源碼,我總結出會覆蓋到五種場景。
- 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方法
源碼
講解
- 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那簡直如盲人摸象。
總結
以上是生活随笔為你收集整理的Kotlin协程重新认知 CoroutineContext的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: kotlin 协程异常处理机制颠覆三观
- 下一篇: Kotlin reduce、fold