協程中的取消和異常 | 核心概念介紹
在之前的文章里,我們為各位開發者分享了在 Android 中使用協程的一些基礎知識,包括在 Android 協程的背景介紹、上手指南和代碼實戰。本次系列文章 "協程中的取消和異常" 也是 Android 協程相關的內容,我們將與大家深入探討協程中關于取消操作和異常處理的知識點和技巧。
當我們需要避免多余的處理來減少內存浪費并節省電量時,取消操作就顯得尤為重要;而妥善的異常處理也是提高用戶體驗的關鍵。本篇是另外兩篇文章的基礎 (第二篇和第三篇將為大家分別詳解協程取消操作和異常處理), 所以有必要先講解一些協程的核心概念,比如 CoroutineScope (協程作用域)、Job (任務) 和 CoroutineContext (協程上下文),這樣我們才能夠進行更深入的學習。
CoroutineScope
CoroutineScope 會追蹤每一個您通過 launch 或者 async 創建的協程 (這兩個是 CoroutineScope 的擴展函數)。任何時候都可通過調用 scope.cancel() 來取消正在進行的工作 (正在運行的協程)。
- CoroutineScope:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/
- launch:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
- async:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
當您希望在應用程序的某一個層次開啟或者控制協程的生命周期時,您需要創建一個 CoroutineScope。對于一些平臺,比如 Android,已經有 KTX 這樣的庫在一些類的生命周期里提供了 CoroutineScope,比如 viewModelScope 和 lifecycleScope。
- viewModelScope:https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/package-summary#(androidx.lifecycle.ViewModel).viewModelScope:kotlinx.coroutines.CoroutineScope
- lifecycleScope:https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/package-summary#lifecyclescope
當創建 CoroutineScope 的時候,它會將 CoroutineContext 作為構造函數的參數。您可以通過下面代碼創建一個新的 scope 和協程:
- //Job 和 Dispatcher 已經被集成到了 CoroutineContext
- //后面我們詳細介紹
- val scope = CoroutineScope(Job() + Dispatchers.Main)
- val job = scope.launch {
- //新的協程
- }
Job
Job 用于處理協程。對于每一個您所創建的協程 (通過 launch 或者 async),它會返回一個 Job 實例,該實例是協程的唯一標識,并且負責管理協程的生命周期。正如我們上面看到的,您可以將 Job 實例傳遞給 CoroutineScope 來控制其生命周期。
Job:
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
CoroutineContext
CoroutineContext 是一組用于定義協程行為的元素。它由如下幾項構成:
- Job:控制協程的生命周期;
- CoroutineDispatcher:向合適的線程分發任務;
- CoroutineName:協程的名稱,調試的時候很有用;
- CoroutineExceptionHandler:處理未被捕捉的異常,在未來的第三篇文章里會有詳細的講解。
CoroutineContex:
thttps://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-coroutine-context/index.html
那么對于新創建的協程,它的 CoroutineContext 是什么呢?我們已經知道一個 Job 的實例會被創建,它會幫助我們控制協程的生命周期。而剩下的元素會從 CoroutineContext 的父類繼承,該父類可能是另外一個協程或者創建該協程的 CoroutineScope。
由于 CoroutineScope 可以創建協程,而且您可以在協程內部創建更多的協程,因此內部就會隱含一個任務層級。在下面的代碼片段中,除了通過 CoroutineScope 創建新的協程,來看看如何在協程中創建更多協程:
- val scope = CoroutineScope(Job() + Dispatchers.Main)
- val job = scope.launch {
- // 新的協程會將 CoroutineScope 作為父級
- val result = async {
- // 通過 launch 創建的新協程會將當前協程作為父級
- }.await()
- }
層級的根通常是 CoroutineScope。圖形化該層級后如下圖所示:
△ 協程是以任務層級為序執行的。
父級是 CoroutineScope 或者其它協程
Job 的生命周期
一個任務可以包含一系列狀態: 新創建 (New)、活躍 (Active)、完成中 (Completing)、已完成 (Completed)、取消中 (Cancelling) 和已取消 (Cancelled)。雖然我們無法直接訪問這些狀態,但是我們可以訪問 Job 的屬性: isActive、isCancelled 和 isCompleted。
△ Job 的生命周期
如果協程處于活躍狀態,協程運行出錯或者調用 job.cancel() 都會將當前任務置為取消中 (Cancelling) 狀態 (isActive = false, isCancelled = true)。當所有的子協程都完成后,協程會進入已取消 (Cancelled) 狀態,此時 isCompleted = true。
解析父級 CoroutineContext
在任務層級中,每個協程都會有一個父級對象,要么是 CoroutineScope 或者另外一個 coroutine。然而,實際上協程的父級 CoroutineContext 和父級協程的 CoroutineContext 是不一樣的,因為有如下的公式:
父級上下文 = 默認值 + 繼承的 CoroutineContext + 參數
其中:
- 一些元素包含默認值: Dispatchers.Default 是默認的 CoroutineDispatcher,以及 "coroutine" 作為默認的 CoroutineName;
- 繼承的 CoroutineContext 是 CoroutineScope 或者其父協程的 CoroutineContext;
- 傳入協程 builder 的參數的優先級高于繼承的上下文參數,因此會覆蓋對應的參數值。
請注意: CoroutineContext 可以使用 " + " 運算符進行合并。由于 CoroutineContext 是由一組元素組成的,所以加號右側的元素會覆蓋加號左側的元素,進而組成新創建的 CoroutineContext。比如,(Dispatchers.Main, "name") + (Dispatchers.IO) = (Dispatchers.IO, "name")。
Dispatchers.IO:http://dispatchers.io/
該 CoroutineScope 所創建的每一個協程,CoroutineContext 至少會包含這些元素。這里的 CoroutineName 是灰色的,因為該值源于默認參數值。那么現在我們明白新協程的父級 CoroutineContext 是什么樣的了,它實際的 CoroutineContext 是:
新的 CoroutineContext = 父級 CoroutineContext + Job()
如果使用上圖中的 CoroutineScope ,我們可以像下面這樣創建新的協程:
- val job = scope.launch(Dispatchers.IO) {
- //新協程
- }
而該協程的父級 CoroutineContext 和它實際的 CoroutineContext 是什么樣的呢?請看下面這張圖。
CoroutineContext 里的 Job 和父級上下文里的不可能是通過一個實例,因為新的協程總會拿到一個 Job 的新實例。
最終的父級 CoroutineContext 會內含 Dispatchers.IO 而不是 scope 對象里的 CoroutineDispatcher,因為它被協程的 builder 里的參數覆蓋了。此外,注意一下父級 CoroutineContext 里的 Job 是 scope 對象的 Job (紅色),而新的 Job 實例 (綠色) 會賦值給新的協程的 CoroutineContext。
在我們這個系列的第三部分中,CoroutineScope 會有另外一個 Job 的實現稱為 SupervisorJob 被包含在其 CoroutineContext 中,該對象改變了 CoroutineScope 處理異常的方式。因此,由該 scope 對象創建的新協程會將一個 SupervisorJob 作為其父級 Job。不過,當一個協程的父級是另外一個協程時,父級的 Job 會仍然是 Job 類型。
現在,大家了解了協程的一些基本概念,在接下來的文章中,我們將在第二篇繼續深入探討協程的取消、第三篇探討協程的異常處理,感興趣的讀者請繼續關注我們的更新。
【本文是51CTO專欄機構“谷歌開發者”的原創稿件,轉載請聯系原作者(微信公眾號:Google_Developers)】