協程是一種并發設計模式,您可以在Android上使用它來簡化異步執行的代碼。Kotlin1.3版本添加了 Coroutines,并基于其他語言的既定概念。
在Android上,協程有助于解決兩個主要問題:
本主題描述了如何使用Kotlin協程解決這些問題,使您能夠編寫更清晰,更簡潔的應用程序代碼。
管理長時間運行的任務
在Android上,每個應用程序都有一個主線程來處理用戶界面并管理用戶交互。如果您的應用程序為主線程分配了太多工作,那么應用程序可能會明顯卡頓或運行緩慢。網絡請求,JSON解析,從數據庫讀取或寫入,甚至只是迭代大型列表都可能導致應用程序運行緩慢,導致可見的緩慢或凍結的UI對觸摸事件響應緩慢。這些長時間運行的操作應該在主線程之外運行。
以下示例顯示了假設的長期運行任務的簡單協程實現:
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) { /* ... */ }
協同程序通過添加兩個操作來處理長時間運行的任務,從而構建常規功能。除了invoke(或call)和返回之外,協同程序還添加了suspend和resume:
您只能從其他suspend函數調用suspend函數,或者使用諸如啟動之類的協程構建器來啟動新的協程。
在上面的示例中,get()仍然在主線程上運行,但它在啟動網絡請求之前掛起協同程序。當網絡請求完成時,get恢復暫停的協程,而不是使用回調來通知主線程。
Kotlin使用堆??蚣軄砉芾砼c任何局部變量一起運行的函數。掛起協程時,將復制并保存當前堆棧幀以供以后使用?;謴蜁r,堆棧幀將從保存位置復制回來,并且該函數將再次開始運行。即使代碼看起來像普通的順序阻塞請求,協程也可以確保網絡請求避免阻塞主線程。
Use coroutines for main-safety
Kotlin協程使用調度程序來確定哪些線程用于協程執行。要在主線程之外運行代碼,您可以告訴Kotlin協程在Default或IO調度程序上執行工作。在Kotlin中,所有協同程序必須在調度程序中運行,即使它們在主線程上運行。協同程序可以暫停,調度程序負責恢復它們。
要指定協程應該運行的位置,Kotlin提供了三個可以使用的調度程序:
繼續前面的示例,您可以使用調度程序重新定義get函數。 在get的主體內部,調用withContext(Dispatchers.IO)來創建一個在IO線程池上運行的塊。 放在該塊中的任何代碼總是通過IO調度程序執行。 由于withContext本身是一個掛起函數,因此函數get也是一個掛起函數。
使用協同程序,您可以調度具有細粒度控制的線程。 因為withContext()允許您控制任何代碼行的線程池而不引入回調,所以您可以將它應用于非常小的函數,例如從數據庫讀取或執行網絡請求。 一個好的做法是使用withContext()來確保每個函數都是主安全的,這意味著您可以從主線程調用該函數。 這樣,調用者永遠不需要考慮應該使用哪個線程來執行該函數。
在前面的示例中,fetchDocs()在主線程上執行; 但是,它可以安全地調用get,后者在后臺執行網絡請求。 因為協同程序支持掛起和恢復,所以只要withContext塊完成,主線程上的協程就會以get結果恢復。
重要說明:使用suspend并不能告訴Kotlin在后臺線程上運行函數。 暫停函數在主線程上運行是正常的。 在主線程上啟動協同程序也很常見。 當您需要主安全時,例如在讀取或寫入磁盤,執行網絡操作或運行CPU密集型操作時,應始終在掛起函數內使用withContext()。
與等效的基于回調的實現相比,withContext()不會增加額外的開銷。 此外,在某些情況下,可以優化withContext()調用,而不是基于等效的基于回調的實現。 例如,如果一個函數對網絡進行十次調用,則可以通過使用外部withContext()告訴Kotlin只切換一次線程。 然后,即使網絡庫多次使用withContext(),它仍然停留在同一個調度程序上,并避免切換線程。 此外,Kotlin優化了Dispatchers.Default和Dispatchers.IO之間的切換,以盡可能避免線程切換。
要點:使用使用Dispatchers.IO或Dispatchers.Default等線程池的調度程序并不能保證該塊從上到下在同一個線程上執行。 在某些情況下,Kotlin協程可能會在暫停和恢復后將執行移動到另一個線程。 這意味著線程局部變量可能不會指向整個withContext()塊的相同值。
指定CoroutineScope
定義協程時,還必須指定其CoroutineScope。 CoroutineScope管理一個或多個相關協程。 您還可以使用CoroutineScope在該范圍內啟動新協程。 但是,與調度程序不同,CoroutineScope不會運行協同程序。
CoroutineScope的一個重要功能是當用戶離開應用程序中的內容區域時停止協程執行。 使用CoroutineScope,您可以確保正確停止任何正在運行的操作。
將CoroutineScope與Android架構組件配合使用
在Android上,您可以將CoroutineScope實現與組件生命周期相關聯。這樣可以避免泄漏內存或為與用戶不再相關的activity或fragment執行額外的工作。使用Jetpack組件,它們自然適合ViewModel。由于ViewModel在配置更改(例如屏幕旋轉)期間不會被銷毀,因此您不必擔心協同程序被取消或重新啟動。
范圍知道他們開始的每個協同程序。這意味著您可以隨時取消在作用域中啟動的所有內容。范圍傳播自己,所以如果一個協程開始另一個協同程序,兩個協同程序具有相同的范圍。這意味著即使其他庫從您的范圍啟動協程,您也可以隨時取消它們。如果您在ViewModel中運行協同程序,這一點尤為重要。如果因為用戶離開了屏幕而導致ViewModel被銷毀,則必須停止它正在執行的所有異步工作。否則,您將浪費資源并可能泄漏內存。如果您在銷毀ViewModel后應該繼續進行異步工作,則應該在應用程序架構的較低層中完成。
警告:通過拋出CancellationException協同取消協同程序。 在協程取消期間觸發捕獲異?;騎hrowable的異常處理程序。
使用適用于Android體系結構的KTX庫組件,您還可以使用擴展屬性viewModelScope來創建可以運行的協同程序,直到ViewModel被銷毀。
啟動一個協程
您可以通過以下兩種方式之一啟動協同程序:
通常,您應該從常規函數啟動新協程,因為常規函數無法調用等待。 僅在另一個協同程序內部或在掛起函數內部執行并行分解時才使用異步。
在前面的示例的基礎上,這里是一個帶有viewModelScope KTX擴展屬性的協程,它使用launch從常規函數切換到協同程序:
fun onDocsNeeded() { viewModelScope.launch { // Dispatchers.Main fetchDocs() // Dispatchers.Main (suspend function call) } }
警告:啟動和異步處理異常的方式不同。 由于async期望在某個時刻最終調用await,它會保留異常并在await調用中重新拋出它們。 這意味著如果您使用await從常規函數啟動新的協同程序,則可能會以靜默方式刪除異常。 這些丟棄的異常不會出現在崩潰指標中,也不會出現在logcat中。
并行分解
當函數返回時,必須停止由掛起函數啟動的所有協同程序,因此您可能需要保證這些協程在返回之前完成。 通過Kotlin中的結構化并發,您可以定義一個啟動一個或多個協同程序的coroutineScope。 然后,使用await()(對于單個協同程序)或awaitAll()(對于多個協程),可以保證這些協程在從函數返回之前完成。
例如,讓我們定義一個以異步方式獲取兩個文檔的coroutineScope。 通過在每個延遲引用上調用await(),我們保證在返回值之前兩個異步操作都完成:
suspend fun fetchTwoDocs() = coroutineScope { val deferredOne = async { fetchDoc(1) } val deferredTwo = async { fetchDoc(2) } deferredOne.await() deferredTwo.await() }
即使fetchTwoDocs()使用異步啟動新的協同程序,該函數也會使用awaitAll()等待那些啟動的協同程序在返回之前完成。 但請注意,即使我們沒有調用awaitAll(),coroutineScope構建器也不會恢復調用fetchTwoDocs的協程,直到所有新的協程完成。
此外,coroutineScope捕獲協程拋出的任何異常并將它們路由回調用者。
有關并行分解的更多信息,請參閱編寫掛起函數。
具有內置支持的架構組件
一些體系結構組件(包括ViewModel和Lifecycle)通過其自己的CoroutineScope成員包含對協同程序的內置支持。
例如,ViewModel包含一個內置的viewModelScope。 這提供了在ViewModel范圍內啟動協同程序的標準方法,如以下示例所示:
class MyViewModel : ViewModel() { fun launchDataLoad() { viewModelScope.launch { sortList() // Modify UI } } /** * Heavy operation that cannot be done in the Main Thread */ suspend fun sortList() = withContext(Dispatchers.Default) { // Heavy work } } LiveData還使用帶有liveData塊的協同程序: liveData { // runs in its own LiveData-specific scope }
原文
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。