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