這是關於 協程的取消和異常 的一系列文章,寫的很不錯。一直準備翻譯來着,種種原因給耽誤了,一直拖到現在。
原文作者:manuelvicnt
原文地址:Coroutines: First things first
譯者:秉心說
該系列博客深入探索了協程的取消和異常。取消 可以避免進行預期以外的工作,從而節省內存和電量;合適的異常處理 可以帶來良好的用戶體驗。作爲該系列另外兩篇文章的基礎,通過本文搞清楚協程的一些基本概念,例如 CoroutineScope
、Job
、CoroutineContext
等,是非常重要的。
如果你更喜歡視頻,可以看看 Florina Muntenescu 和我在 KotlinConf'19 上的演講。
https://www.youtube.com/watch?v=w0kfnydnFWI&feature=emb_logo
CoroutineScope(協程作用域)
CoroutineScope
可以幫助你追蹤任何通過 launch
和 async
啓動的協程。它們都是 CoroutineScope
的擴展函數。正在運行的協程可以通過調用 scope.cancel()
在任意時間點停止。
無論你在 App 的任何頁面啓動協程,並控制其生命週期,都應該創建 CoroutineScope
。在 Android 中,KTX 類庫已經爲特定的生命週期類提供了 CoroutineScope
,例如 viewModelScope
和 lifecycleScope
。
創建 CoroutineScope
時需要給構造函數提供 CoroutineContext
(協程上下文) 參數。下面的代碼演示瞭如何新建一個作用域和協程。
// Job 和 Dispatcher 合併在一起作爲 CoroutineContext,稍後會進行說明
val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {
// 新協程
}
Job
Job 代表了一個協程。通過 launch
和 async
啓動的每一個協程,都會返回一個 Job
實例來唯一標識,並且管理該協程的生命週期。如上一節所示,你可以給 CoroutineScope
傳遞一個 Job
來控制它的生命週期。
CoroutineContext(協程上下文)
可以翻譯成協程上下文。但我還是用英文吧。
CoroutineContext
是定義協程行爲的一系列元素。它由以下幾部分組成:
Job,管理協程的生命週期
CoroutineDispatcher,分發任務到合適的線程
CoroutineName,協程的名稱,用於調試
CoroutineExceptionHandler,處理未捕獲的異常,這是第三篇文章的內容
一個新協程的 CoroutineContext
是什麼?我們已經知道會創建一個新的 Job
來幫助我們管理生命週期,剩下的元素將繼承自它的父親的 CoroutineContext
(可能是另一個協程,或者是創建它的 CoroutineScope)。
由於 CoroutineScope
可以創建協程,並且你可以在一個協程內部創建多個協程。這就形成了一個隱式的層級結構。在下面的代碼中,除了使用 CoroutineScope
創建新協程之外,還展示瞭如何在一個協程中創建多個協程。
val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {
// 這裏的新協程的父親是 scope
val result = async {
// 這裏的新協程的父親是上面的 scope.launch 啓動的協程
}.await()
}
層級結構的根通常是 CoroutineScope
。我們可以把層級結構想象成下面這樣:
Job 生命週期
Job
會經歷以下生命週期:
New, Active, Completing, Completed, Cancelling , Cancelled
通過 Job 的這幾個屬性可以獲取它的狀態:isActive
、isCancelled
和 isCompleted
。
當協程處於 Active
狀態,失敗或者取消都會讓協程移動到 Cancelling
狀態(isActive = false
, isCancelled = true
)。當協程中的所有子協程都完成了任務,協程將會進入 Cancelled
狀態 (isCompleted = true) 。
關於 Parent CoroutineContext
在協程的繼承結構中,每一個協程都會有一個父親,這個父親可能是 CoroutineScope
或者另一個協程。但是子協程最終的父 CoroutineContext 可能和其父親原本的 CoroutineContext 不一樣。
父 CoroutineContext 的計算公式如下:
Parent context = Defaults + 繼承的 CoroutineContext + arguments
其中:
其中一些元素具有默認值:
CoroutineDispatcher
的默認值是Dispatchers.Default
,CoroutineName
的默認值是coroutine
繼承的
CoroutineContext
是父親的 CoroutineContext傳遞到協程構建器中的參數優先於繼承自上下文的參數
注意:多個 CoroutineContext
可以通過 “+” 操作符合並。由於 CoroutineContext
包含一系列元素,當創建新的 CoroutineContext
時,“+” 右側的元素將會覆蓋左側的元素。例如:(Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.IO, “name”)
。
通過此協程作用域創建的協程的 CoroutineContext 將至少包含上圖中這些元素。CoroutineName
是灰色的,因爲它是默認值。
現在我們知道一個新協程的父 CoroutineContext
是什麼了。它自己的 CoroutineContext
實際上是這樣的:
New coroutine context = parent CoroutineContext + Job()
通常上面的協程作用域創建一個新的協程:
val job = scope.launch(Dispatchers.IO) {
//新協程
}
那麼它的父 CoroutineContext
和自己的 CoroutineContext
是什麼樣的呢?請看下面的圖片。
注意上下兩個 Job
並不是同一個實例,新協程總會得到一個新的 Job
實例。
最終的父 CoroutineContext
的協程調度器是 Dispatchers.IO
,因爲它被協程構建器中的參數覆蓋了。(譯者注:scope.launch(Dispatchers.IO)
) 。
同時,注意父 CoroutineContext
中的 Job 實例就是 scope 的 Job 實例(紅色),而傳遞到新協程的 CoroutineContext
中的 Job 是一個新的實例(綠色)。
在系列第三篇文章中我們將看到,CoroutineScope
可以擁有其他的 Job 實現類,SupervisorJob
,它會改變協程作用域的異常處理。因此,在這樣的 CoroutineScope
中創建的子協程也將繼承 SupervisorJob
類型的 Job 。但是,如果當父協程是另一個協程的時候,將總是 Job 類型。
現在你已經瞭解了協程的基礎知識,在系列的後面兩篇文章中學習更多 取消和異常 的知識吧!
今天的文章就到這裏了,這個系列還有三篇文章,都很精彩,掃描下方二維碼持續關注吧!