【譯】使用kotlin協程提高app效能

Carve_Time發表於2019-12-24

原文

協程是一種併發設計模式,您可以在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)和返回之外,協同程式還新增了suspendresume

  • suspend暫停當前協同程式的執行,儲存所有區域性變數。
  • resume恢復從暫停的協同處繼續執行暫停的協同程式。

您只能從其他suspend函式呼叫suspend函式,或者使用諸如啟動之類的協程構建器來啟動新的協程。

在上面的示例中,get()仍然在主執行緒上執行,但它在啟動網路請求之前掛起協同程式。當網路請求完成時,get恢復暫停的協程,而不是使用回撥來通知主執行緒。

Kotlin使用堆疊框架來管理與任何區域性變數一起執行的函式。掛起協程時,將複製並儲存當前堆疊幀以供以後使用。恢復時,堆疊幀將從儲存位置複製回來,並且該函式將再次開始執行。即使程式碼看起來像普通的順序阻塞請求,協程也可以確保網路請求避免阻塞主執行緒。

使用協程確保主執行緒安全

Kotlin協程使用排程程式來確定哪些執行緒用於協程執行。要在主執行緒之外執行程式碼,您可以告訴Kotlin協程在Default或IO排程程式上執行工作。在Kotlin中,所有協同程式必須在排程程式中執行,即使它們在主執行緒上執行。協同程式可以暫停,排程程式負責恢復它們。

要指定協程應該執行的位置,Kotlin提供了三個可以使用的排程程式:

  • Dispatchers.Main - 使用此排程程式在主Android執行緒上執行協同程式。 這應該僅用於與UI互動並執行快速工作。 示例包括呼叫掛起函式,執行Android UI框架操作以及更新LiveData物件。
  • Dispatchers.IO - 此排程程式已經過優化,可在主執行緒外執行磁碟或網路I / O. 示例包括使用Room元件,讀取或寫入檔案以及執行任何網路操作。
  • Dispatchers.Default - 此排程程式已經過優化,可以在主執行緒之外執行CPU密集型工作。 示例用例包括對列表進行排序和解析JSON。

繼續前面的示例,您可以使用排程程式重新定義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實現與元件生命週期相關聯。這樣可以避免洩漏記憶體或為與使用者不再相關的activityfragment執行額外的工作。使用Jetpack元件,它們自然適合ViewModel。由於ViewModel在配置更改(例如螢幕旋轉)期間不會被銷燬,因此您不必擔心協同程式被取消或重新啟動。

範圍知道他們開始的每個協同程式。這意味著您可以隨時取消在作用域中啟動的所有內容。範圍傳播自己,所以如果一個協程開始另一個協同程式,兩個協同程式具有相同的範圍。這意味著即使其他庫從您的範圍啟動協程,您也可以隨時取消它們。如果您在ViewModel中執行協同程式,這一點尤為重要。如果因為使用者離開了螢幕而導致ViewModel被銷燬,則必須停止它正在執行的所有非同步工作。否則,您將浪費資源並可能洩漏記憶體。如果您在銷燬ViewModel後應該繼續進行非同步工作,則應該在應用程式架構的較低層中完成。

警告:通過丟擲CancellationException協同取消協同程式。 在協程取消期間觸發捕獲異常或Throwable的異常處理程式。

使用適用於Android體系結構的KTX庫元件,您還可以使用擴充套件屬性viewModelScope來建立可以執行的協同程式,直到ViewModel被銷燬。

啟動一個協程

您可以通過以下兩種方式之一啟動協同程式:

  • launch會啟動一個新的協程,並且不會將結果返回給呼叫者。 任何被認為是“發射並忘記”的工作都可以使用launch來開始。
  • async啟動一個新的協同程式,並允許您使用名為await的掛起函式返回結果。

通常,您應該從常規函式啟動新協程,因為常規函式無法呼叫等待。 僅在另一個協同程式內部或在掛起函式內部執行並行分解時才使用非同步。

在前面的示例的基礎上,這裡是一個帶有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捕獲協程丟擲的任何異常並將它們路由回撥用者。

有關並行分解的更多資訊,請參閱編寫掛起函式。

具有內建支援的架構元件

一些體系結構元件(包括ViewModelLifecycle)通過其自己的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
}
複製程式碼

相關文章