本文原作者:字節小站,原文發布於: 字節小站。
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 合併成一個鍊表,後文會詳細講解。
根據類圖結構我們可以把它分成四個層級:
CoroutineContext 協程中所有上下文相關類的父接口。CombinedContext、Element、EmptyCoroutineContext。它們是 CoroutineContext 的直接子類。AbstractCoroutineContextElement、Job。這兩個是 Element 的直接子類。CoroutineName、CoroutineExceptionHandler、CoroutineDispatcher (包含 Dispatchers.Main 和 Dispatchers.Default)。它們是 AbstractCoroutineContextElement 的直接子類。圖中紅框處,CombinedContext 定義了 size() 和 contains() 方法,這與集合操作很像,CombinedContext 是 CoroutineContext 對象的集合,而 Element 和 EmptyCoroutineContext 卻沒有定義這些方法,真正實現了集合操作的協程上下文只有 CombinedContext,後文會詳細講解。Persistentcontextforthecoroutine.Itisanindexedsetof[Element]instances.Anindexedsetisamixbetweenasetandamap.Everyelementinthissethasaunique[Key].
CoroutineContext 是協程的上下文。CoroutineContext 是 element 的 set 集合,沒有重複類型的 element 對象。集合中的每個 element 都有唯一的 Key,Key 可以用來檢索元素。相信大多數的人看到這樣的解釋時,都會心生疑惑,既然是 set 類型為啥不直接用 HashSet 來保存 Element。CoroutineContext 的實現原理又是什麼呢?原因是考慮到協程嵌套,用鍊表實現更好。

Key 是一個接口定義在 CoroutineContext 中的接口,作為接口它沒有聲明任何的方法,那麼其實它沒有任何真正有用的意義,它只是用來檢索。我們先來看下,協程庫中是如何使用 Key 接口的。通過觀察協程官方庫中的例子,我們發現 Element 的子類都必須重寫 Key 這個屬性,而且 Key 的泛型類型必須和類名相同。以 CoroutineName 例,Key 是一個伴生對象,同時 Key 的泛型類型也是 CoroutineName。為了方便理解,我仿照寫了 MyElement 類,如下:通過對比 kt 類和反編譯的 java 類我們看到 Key 就是一個靜態變量,而且它的實現類,其實啥也沒幹。它的作用與 HashMap 中的 Key 類似:實現 key-value 功能,為插入和刪除提供檢索功能Key 是 static 靜態變量,全局唯一,為 Element 提供唯一性保障
coroutineContext.get(CoroutineName.Key)coroutineContext.get(CoroutineName)coroutineContext[CoroutineName]coroutineContext[CoroutineName.Key]講解
通過 Key 檢索 Element。返回值只能是 Element 或者 null,鍊表節點中的元素值。
Element get 方法: 只要 Key 與當前 Element 的 Key 匹配上了,返回該 Element 否則返回 null。
CombinedContext get 方法: 遍歷鍊表,查詢與 Key 相等的 Element,如果沒找到返回 null。
講解
將兩個 CoroutineContext 組合成一個 CoroutineContext,如果是兩個類型相同的 Element 會返回一個新的 Element。如果是兩個不同類型的 Element 會返回一個 CombinedContext。如果是多個不同類型的 Element 會返回一條 CombinedContext 鍊表。我將上述算法總結成了 5 種場景,不過在介紹這 5 種場景前,我們先講解 CombinedContext 的數據結構。
因為 CombinedContext 是 CoroutineContext 的子類,left 也是 CoroutineContext 類型的,所以它的數據結構是鍊表。我們經常用 next 來表示鍊表的下一個節點。那麼為什麼這裡取名叫 left 呢?我甚至懷疑寫這段代碼的是個左撇子。真正的原因是,協程可以啟動子協程,子協程又可以啟動孫協程。父協程在左邊,子協程在右邊。越是外層的協程的 Context 越在左邊,大概示意圖如下 (真實並非如此,比這更複雜)鍊表的兩個知識點在此都有體現。CoroutineContext.plus 方法中使用的是頭插法。CombinedContext 的 toString 方法採用的是鍊表倒序打印法。plus EmptyCoroutineContext
plus 相同類型的 Element
plus 方法的調用方沒有 Dispatcher 相關的 Element
plus 方法的調用方只有 Dispatcher 相關的 Element
plus 方法的調用方是包含 Dispatcher 相關 Element 的鍊表
結果如下:
Dispatchers.Main + EmptyCoroutineContext 結果: Dispatchers.Main。
CoroutineName("c1") + CoroutineName("c2") 結果: CoroutineName("c2")。相同類型的直接替換掉。
CoroutineName("c1") + Job() 結果: CoroutineName("c1") <- Job。頭插法被 plus 的 (Job) 放在鍊表頭部。
Dispatchers.Main + Job() 結果: Job <- Dispatchers.Main。雖然是頭插法,但是 ContinuationInterceptor 必須在鍊表頭部。
Dispatchers.Main + Job() + CoroutineName("c5") 結果: Job <- CoroutineName("c5") <- Dispatchers.Main。Dispatchers.Main 在鍊表頭部,其它的採用頭插法。
如果不考慮 Dispatchers.Main 的情況。我們可以把 + 用 <- 代替。CoroutineName("c1")+Job() 等價於 CoroutineName("c1")<-JobCoroutineContext 的 minusKey 方法
講解
Element minusKey 方法:如果 Key 與當前 element 的 Key 相等,返回 EmptyCoroutineContext,否則相當於沒減成功,返回當前 element。
CombinedContext minusKey 方法:刪除鍊表中符合條件的節點,分三種情況。
Job<-CoroutineName("c5")<-Dispatchers.Main
沒找到節點: minusKey(MyElement)。在 Job 節點處走 newLeft === left 分支,依此類推,在 CoroutineName 處走同樣的分支,在 Dispatchers.Main 處走同樣的分支。
節點在尾部: minusKey(Job)。在 CoroutineName("c5") 節點走 newLeft === EmptyCoroutineContext 分支,依此往頭部遞歸。
節點不在尾部: minusKey(CoroutineName)。在 Dispatchers.Main 節點處走 else 分支。
學習 CoroutineContext 首先要搞清楚各類之間的繼承關係,其次,CombinedContext 各具體 Element 的集合,它的數據結構是鍊表,如果讀者對鍊表增刪改查操作熟悉的話,那麼很容易就能搞懂 CoroutineContext 原理,否則想要搞懂 CoroutineContext 那簡直如盲人摸象。"開發者說·DTalk" 面向
中國開發者們徵集 Google 移動應用 (apps & games)相關的產品/技術內容。歡迎大家前來分享您對移動應用的行業洞察或見解、移動開發過程中的心得或新發現、以及應用出海的實戰經驗總結和相關產品的使用反饋等。我們由衷地希望可以給這些出眾的中國開發者們提供更好展現自己、充分發揮自己特長的平台。我們將通過大家的技術內容着重選出優秀案例進行谷歌開發技術專家 (GDE)的推薦。
點擊屏末|閱讀原文|即刻報名參與"開發者說·DTalk"
