掌握Kotlin Coroutine之 Job&Deferred
前面一節介紹了 Coroutine 的 scope 概念以及 CoroutineScope 上定義的各種建立不同應用場景 Coroutine 的擴充套件函式。這一節來介紹 Coroutine 如何取消以及 Coroutine 的超時處理。
Coroutine 既然是非同步操作,所以當不需要的時候需要及時取消以便釋放系統資源;同時非同步操作可能用時很久,需要有個超時的概念,當等候一定時間後可以放棄繼續等待,做其他操作。
Coroutine 通過 coroutine builder
函式建立後,返回的是一個代表所建立Coroutine的例項,所以本節主要介紹上節各種 coroutine builder
函式返回的物件,通過這些物件可以實現取消和超時以及其他更豐富的操作。
Job
CoroutineScope.launch 函式返回的是一個 Job 物件,代表一個非同步的任務。Job 具有生命週期並且可以取消。 Job 還可以有層級關係,一個Job可以包含多個子Job,當父Job被取消後,所有的子Job也會被自動取消;當子Job被取消或者出現異常後父Job也會被取消。
除了通過 CoroutineScope.launch 來建立Job物件之外,還可以通過 Job() 工廠方法來建立該物件。預設情況下,子Job的失敗將會導致父Job被取消,這種預設的行為可以通過 SupervisorJob 來修改。
一個 Job 具有如下的幾種狀態:
一般而言,job 建立後都處於 active 狀態,表示這個 Job 已經被建立並且被啟動了。 通過 coroutine builder
函式的 start 引數可以修改這個狀態, 比如如果使用 CoroutineStart.LAZY 作為 start 引數,則建立的 job 處於 new 狀態,這個時候需要通過呼叫 job 的 start 或者 join 函式來把該 job 轉換為 active 狀態。
處於 active 狀態的 job 表示 Coroutine 正在執行。 如果執行過程中丟擲了異常則會把該 job 標記為 cancelling 狀態。除此之外,還可以通過呼叫 cancel 函式來把 job 轉換為 cancelling 狀態。然後當 job 完成後就處於 cancelled 狀態。
下圖是 job 各種狀態之間的轉換示意:

具有多個子 job 的父job 會等待所有子job完成(或者取消)後,自己才會執行完成。
在Coroutine內,通過 coroutineContext 也可以訪問該 Coroutine 的job 物件, coroutineContext[Job.Key] 或者 coroutineContext[Job]。等後面介紹到 coroutineContext 的時候再來詳細瞭解細節。
下面介紹 Job 介面定義的一些常用的函式,這些函式都是執行緒安全的,所以可以直接在其他 Coroutine 中呼叫。
start
呼叫該函式來啟動這個 Coroutine,如果當前 Coroutine還沒有執行呼叫該函式返回 true,如果當前 Coroutine 已經執行或者已經執行完畢,則呼叫該函式返回 false。
cancel
呼叫該函式來取消這個 Coroutine 的執行。
invokeOnCompletion
通過這個函式可以給 job 設定一個完成通知,當 job 執行完成的時候會執行這個通知函式。 回撥的通知物件型別為: typealias CompletionHandler = (cause: Throwable?) -> Unit
.
需要注意的是,這個 CompletionHandler 回撥是 同步 執行的。如果 job 已經完成了,則會立刻呼叫 CompletionHandler 。 CompletionHandler 引數代表了 job 是如何執行完成的。 cause 有下面三種情況:
– 如果 job 是正常執行完成的,則 cause 引數為 null
– 如果 job 是正常取消的,則 cause 引數為 CancellationException 物件。這種情況不應該當做錯誤處理,這是任務正常取消的情形。所以一般不需要在錯誤日誌中記錄這種情況。
– 其他情況表示 job 執行失敗了。
這個函式的返回值為 DisposableHandle 物件,如果不再需要監控 job 的完成情況了, 則可以呼叫 DisposableHandle.dispose 函式來取消監聽。如果 job 已經執行完了, 則無需呼叫 dispose 函數了,會自動取消監聽。
join
join 函式和前面三個函式不同,這是一個 suspend 函式。所以只能在 Coroutine 內呼叫。
這個函式會暫停當前的 Coroutine直到其執行完成。所以 join 函式一般用來在另外一個 Coroutine 中等待 job 執行完成後繼續執行。當 job 執行完成後, job.join 函式恢復,這個時候 job 這個任務已經處於完成狀態了,而呼叫 job.join 的Coroutine還繼續處於 activie 狀態。
下面是一個演示 join 函式的示例:
class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val onComplete: CompletionHandler = { Log.e(TAG, "CompletionHandler: $it") } val job: Job = launch(BG, CoroutineStart.LAZY) { for (x in 1..3) { delay(1000) Log.d(TAG, "inside launch x is $x") } } Log.d(TAG, "onCreate() called") job.invokeOnCompletion(onComplete) launch { delay(3000) Log.e(TAG, "launch before join isActive ${job.isActive} , this is $isActive") job.join() Log.e(TAG, "launch after join isCompleted ${job.isCompleted} , this is $isActive") } fab.setOnClickListener { view -> // 點選 FAB 按鈕可以取消 launch { job.cancel() } } }
上面程式碼執行 log 如下:
03-02 17:49:38.001 24202-24202 D/ onCreate() called 03-02 17:49:41.041 24202-24202 E/ launch before join isActive false , this is true 03-02 17:49:42.049 24202-24242 D/ inside launch x is 1 03-02 17:49:43.050 24202-24242 D/ inside launch x is 2 03-02 17:49:44.051 24202-24242 D/ inside launch x is 3 03-02 17:49:44.052 24202-24242 E/ CompletionHandler: null 03-02 17:49:44.053 24202-24202 E/ launch after join isCompleted true , this is true
在實現 Coroutine 中支援取消操作
呼叫 cancel 函式會取消一個 Coroutine,但是具體正在執行中的Coroutine 能否取消取決於這個 Coroutine 的實現。Coroutine標準庫中定義的 suspending function 都是支援取消操作的(比如 delay)。當執行到這些函式的時候,都會去檢測當前的Coroutine是否被取消了,如果發現被取消了則取消繼續執行。如果自定義的 Coroutine中沒有檢測當前任務是否被取消了,則這種 Coroutine是無法取消的。 在實現 Coroutine 的時候可以通過 isActive 屬性來判斷當前任務是否被取消了,如果發現被取消了則停止繼續執行;還可以通過呼叫其他標準庫中的 suspending function 來處理取消。
比如下面的示例中,Coroutine 程式碼通過不停的判斷 isActive 來支援取消操作
val startTime = System.currentTimeMillis() val job = launch(Dispatchers.Default) { var nextTime = startTime var i = 0 while (isActive) { // 在任務執行的時候,不停的判斷當前任務是否處於active 狀態,如果不是則停止執行 if (System.currentTimeMillis() >= nextTime) { Log.e(TAG, "I'm sleeping ${i++} ...") nextTime += 500L } } } delay(1300L) Log.e(TAG, "main: I'm tired of waiting!") // 取消任務並且等待任務執行完成, cancelAndJoin 是 Job 的一個擴充套件函式,同時執行 cancel 和join job.cancelAndJoin() Log.e(TAG, "main: Now I can quit.")
超時
在實際應用中, 通常在任務執行超時後我們來取消這個任務。在 Coroutine 中超時是通過 withTimeout 和 withTimeoutOrNull 這兩個函式來實現的。比如:
launch { val value: String? = withTimeoutOrNull(1000L) { Log.d(TAG, "inside withTimeout 1") delay(2000) Log.d(TAG, "inside withTimeout 2") "return value" } Log.d(TAG, "timeout value = [$value]") }
上面的程式碼log如下:
03-02 18:12:16.992 25173-25173 D inside withTimeout 1 03-02 18:12:17.995 25173-25173 D timeout value = [null]
NonCancellable
如果有些任務你希望該任務執行完,不能被呼叫者取消。可以通過 NonCancellable 這個 context 單例物件來實現。也就是使用 NonCancellable 作為任務的 context,這樣這個任務就是不可取消的:
withContext(NonCancellable) { // this code will not be cancelled }
SupervisorJob
SupervisorJob
是一個函式,定義如下:
fun SupervisorJob(parent: Job? = null): Job
該函式建立了一個處於 active 狀態的 supervisor job
。如前所述, Job 是有父子關係的,如果子Job 失敗了父Job會自動失敗,這種預設的行為可能不是我們期望的。比如在 Activity 中有兩個子Job分別獲取一篇文章的評論內容和作者資訊。如果其中一個失敗了,我們並不希望父Job自動取消,這樣會導致另外一個子Job也被取消。
而 supervisor job
就是這麼一個特殊的 Job,裡面的子Job不相互影響,一個子Job失敗了,不影響其他子Job的執行。
SupervisorJob(parent:Job?)
具有一個 parent
引數,如果指定了這個引數,則所返回的 job 就是引數 parent
的子job。如果 parent job 失敗了或者取消了,則這個 supervisor job 也會被取消。當 supervisor job 被取消後,所有 supervisor job 的子Job也會被取消。
前面 Activity 所用的 MainScope()
實現中就使用了 SupervisorJob
和一個 Main Dispatcher:
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
Dispatcher 將會在下一節介紹。
Deferred
Deferred 介面就比較簡單了,繼承自 Job 介面,額外提供了獲取 Coroutine 返回結果的方法。
由於 Deferred 繼承自 Job 介面,所以 Job 相關的內容在 Deferred 上也是適用的。 Deferred 提供了額外三個函式來處理和Coroutine執行結果相關的操作。
await()
await() 函式等待這個Coroutine執行完畢並返回結果,當 Coroutine 正常執行完畢、被取消了或者出現異常執行失敗的時候, await() 恢復執行。
getCompleted()
getCompleted() 函式用來獲取Coroutine執行的結果。如果Coroutine還沒有執行完成則會丟擲 IllegalStateException ,如果任務被取消了也會丟擲對應的異常。所以在執行這個函式之前,可以通過 isCompleted 來判斷一下當前任務是否執行完畢了。
getCompletionExceptionOrNull()
getCompletionExceptionOrNull() 函式用來獲取已完成狀態的Coroutine異常資訊,如果任務正常執行完成了,則不存在異常資訊,返回null。如果還沒有處於已完成狀態,則呼叫該函式同樣會丟擲 IllegalStateException。
總結
通過 Job 介面提供的函式可以控制 Coroutine 的執行並查詢 Coroutine 的狀態,通過 Deferred 介面可以獲取 Coroutine 執行的結果。
Deferred API 文件 https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html