【譯】使用kotlin協程提升app性能

原文android

協程是一種併發設計模式,您能夠在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使用堆棧框架來管理與任何局部變量一塊兒運行的函數。掛起協程時,將複製並保存當前堆棧幀以供之後使用。恢復時,堆棧幀將從保存位置複製回來,而且該函數將再次開始運行。即便代碼看起來像普通的順序阻塞請求,協程也能夠確保網絡請求避免阻塞主線程。async

Use coroutines for main-safety

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捕獲協程拋出的任何異常並將它們路由回調用者。

有關並行分解的更多信息,請參閱編寫掛起函數。

具備內置支持的架構組件

一些體系結構組件(包括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
}
複製代碼
相關文章
相關標籤/搜索