Kotlin 協同程式 可讓您編寫簡潔、簡化的非同步程式碼 應用程式回應速度,同時管理長時間執行的工作 (例如網路呼叫) 或磁碟作業
本主題會詳細介紹 Android 上的協同程式。如果您還不熟悉協同程式,在閱讀本主題前,請務必先參考「Android 上的 Kotlin 協同程式」。
管理長時間執行的工作
協同程式會在常見函式中增添兩項作業,以處理長時間執行的工作。亦即,除了 invoke
(或 call
) 和 return
外,協同程式還新增了 suspend
和 resume
:
suspend
會暫停執行當前的協同程式,並儲存所有本機變數。resume
會在先前暫停處繼續執行已暫停的協同程式。
您只能透過其他 suspend
函式呼叫 suspend
函式,或使用協同程式建構工具 (例如 launch
) 來啟動新的協同程式。
以下範例顯示,在某個長時間執行的工作中,實作簡易的協同程式:
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("https://developer.android.com") // Dispatchers.IO for `get`
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
在這個範例中,get()
仍在主執行緒上執行,但在執行網路要求前,先讓協同程式暫停了。當網路要求完成時,get
繼續執行已暫停的協同程式,而不是使用回呼來通知主執行緒。
Kotlin 使用「堆疊框架」來管理要搭配本機變數執行的函式。暫停協同程式時,系統會複製和儲存目前的堆疊框架,供日後使用。繼續執行協同程式時,系統會從儲存的位置複製堆疊框架,並再次執行函式。即使程式碼看起來是普通的連續封鎖要求,但協同程式仍能確保網路要求不會封鎖主執行緒。
使用協同程式,確保不影響主執行緒
Kotlin 協同程式使用「調度器」,來判定哪些執行緒會用於執行協同程式。如要在主執行緒外執行程式碼,您可以指示 Kotlin 協同程式在「預設」或「IO」調度器上執行作業。在 Kotlin 中,所有協同程式都必須在調度器內執行,即使在主執行緒上執行也是如此。協同程式可以自行暫停,而調度器負責繼續執行協同程式。
為指定協同程式的執行位置,Kotlin 提供了三個調度器,供您使用:
- Dispatchers.Main - 使用這個調度器在 Android 主執行緒上執行協同程式。這個調度器應只用於與 UI 互動及執行快速作業。例如,呼叫
suspend
函式、執行 Android UI 架構作業,以及更新LiveData
物件。 - Dispatchers.IO - 這個調度器已完成最佳化調整,以便在主執行緒外執行磁碟或網路 I/O。例如,使用 Room 元件、讀取或寫入檔案,以及執行任何網路作業。
- Dispatchers.Default - 這個調度器已完成最佳化調整,以便在主執行緒外執行大量使用 CPU 的工作。用途包括為清單排序和剖析 JSON。
延續上一個範例,您可以使用調度器重新定義 get
函式。在 get
的主體中,呼叫 withContext(Dispatchers.IO)
來建立在 IO 執行緒集區上執行的區塊。您放入該區塊的所有程式碼一律會透過 IO
調度器執行。由於 withContext
本身是暫停函式,所以 get
函式也是暫停函式。
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("developer.android.com") // Dispatchers.Main
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}
透過協同程式,您可以使用精細的控制項來調派執行緒。withContext()
可讓您控管任何程式碼行的執行緒集區,而不會導入回呼,因此您可以將此程式碼套用到極小的函式,例如從資料庫讀取資料,或執行網路要求。建議您使用 withContext()
,確認每個函式都「不影響主執行緒」,也就是可以從主執行緒呼叫函式。如此一來,呼叫端就不必考慮使用哪個執行緒來執行函式。
在上一個範例中,fetchDocs()
會在主執行緒上執行;但它可以安全呼叫 get
,後者會在背景中執行網路要求。由於協同程式支援 suspend
和 resume
,所以在執行 withContext
區塊後,主執行緒上的協同程式會立即繼續執行 get
結果。
withContext() 的效能
withContext()
敬上
與同等的回呼型回呼相比,未增加額外負荷
。而且在某些情況下,還能將 withContext()
呼叫最佳化,使其效能超越同等的回呼型實作。例如,如果函式呼叫網路十次,您可以使用外部 withContext()
,指示 Kotlin 只切換一次執行緒。這樣的話,即使網路程式庫多次使用 withContext()
,程式庫仍會留在同一個調度器上,不會切換執行緒。此外,Kotlin 會對 Dispatchers.Default
和 Dispatchers.IO
之間的切換作業進行最佳化調整,盡量避免切換執行緒。
啟動協同程式
您可以透過下列任一方式啟動協同程式:
launch
敬上 會啟動新的協同程式,但不會將結果傳回呼叫端。任何視為「射後不理」的工作都可以使用launch
啟動。async
敬上 啟動新的協同程式,並透過暫停的方式傳回結果 函式await
。
一般函式無法呼叫 await
,因此您通常從一般函式對新協同程式執行 launch
作業。只有在其他協同程式內時,或在暫停函式內,並執行平行分解時,才使用 async
。
平行分解
在 suspend
函式內啟動的所有協同程式,都必須在函式傳回時停止,因此您可能需要確保,這些協同程式要在傳回值前執行完畢。使用 Kotlin 中的「結構化並行」,您就可以定義 coroutineScope
來啟動一個或多個協同程式。然後,使用 await()
(針對一個協同程式) 或 awaitAll()
(針對多個協同程式),就可確保這些協同程式在從函式傳回值前執行完畢。
例如,我們來定義 coroutineScope
,用於以非同步方式擷取兩份文件。在每個延遲的參照上呼叫 await()
,就能確保,兩項 async
作業都會在傳回值之前執行完畢:
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
您也可以在集合上使用 awaitAll()
,如以下範例所示:
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
即使 fetchTwoDocs()
使用 async
啟動新的協同程式,函式還是會使用 awaitAll()
來等待已啟動的協同程式執行完畢,然後才會傳回值。不過請注意,即使未呼叫 awaitAll()
,coroutineScope
建構工具也不會繼續執行呼叫了 fetchTwoDocs
的協同程式,直到所有新協同程式執行完畢為止。
此外,coroutineScope
會擷取協同程式擲回的例外狀況,並轉送至呼叫端。
如要進一步瞭解平行分解,請參閱「撰寫暫停函式」。
協同程式概念
CoroutineScope
CoroutineScope
可使用 launch
或 async
追蹤其建立的任何協同程式。您可以隨時呼叫 scope.cancel()
,來取消進行中的工作 (即執行中的協同程式)。在 Android 中,部分 KTX 程式庫會為特定的生命週期類別提供專屬的 CoroutineScope
。例如,ViewModel
有 viewModelScope
,Lifecycle
則有 lifecycleScope
。但與調度器不同,CoroutineScope
不執行協同程式。
viewModelScope
也用於使用協同程式的 Android 背景執行緒中列出的範例。不過,如果您需要建立自己的 CoroutineScope
,來控管應用程式特定層中協同程式的生命週期,您可以按照以下方式來建立:
class ExampleClass {
// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine within the scope
scope.launch {
// New coroutine that can call suspend functions
fetchDocs()
}
}
fun cleanUp() {
// Cancel the scope to cancel ongoing coroutines work
scope.cancel()
}
}
已取消的範圍無法建立更多協同程式。因此,只有在刪除控管生命週期的類別時,您才要呼叫 scope.cancel()
。使用 viewModelScope
時,ViewModel
類別會在 ViewModel 的 onCleared()
方法中自動取消範圍。
工作
Job
是協同程式的控制代碼。使用 launch
或 async
建立的每個協同程式都會傳回 Job
執行個體,用來以唯一的方式識別協同程式,並管理生命週期。您也可以將 Job
傳遞至 CoroutineScope
,來進一步管理生命週期,如以下範例所示:
class ExampleClass {
...
fun exampleMethod() {
// Handle to the coroutine, you can control its lifecycle
val job = scope.launch {
// New coroutine
}
if (...) {
// Cancel the coroutine started above, this doesn't affect the scope
// this coroutine was launched in
job.cancel()
}
}
}
CoroutineContext
CoroutineContext
使用以下元素定義協同程式的行為:
Job
:控管協同程式的生命週期。CoroutineDispatcher
:將工作調派給適當的執行緒。CoroutineName
:協同程式的名稱,用於偵錯。CoroutineExceptionHandler
:處理未擷取的例外狀況。
如果是在範圍內建立的新協同程式,系統會將新的 Job
執行個體指派給新的協同程式,並從涵蓋的範圍沿用其他 CoroutineContext
元素。您可以將新的 CoroutineContext
傳遞至 launch
或 async
函式,來覆寫沿用的元素。請注意,將 Job
傳遞至 launch
或 async
不會有任何作用,因為 Job
的新執行個體始終會被指派給新的協同程式。
class ExampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine on Dispatchers.Main as it's the scope's default
val job1 = scope.launch {
// New coroutine with CoroutineName = "coroutine" (default)
}
// Starts a new coroutine on Dispatchers.Default
val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
// New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
}
}
}
其他協同程式資源
如需取得更多協同程式資源,請參閱下列連結: