【譯】Kotlin coroutines meeting Android

前言

本文翻譯自 Sean McQuillanKotlin coroutines 入門系列。看了他的三篇文章,真正瞭解了協程出現的意義,它能幫開發者解決的問題,並掌握了它的基本用法。 原文地址:html

協程能夠作什麼?

對於 Android 開發者來講,咱們能夠將協程運用在如下兩個場景:android

  • 耗時任務:咱們不該該在主線程作耗時操做。
  • 主線程安全:咱們能夠在主線程中調用 suspend 函數來執行一些操做而不阻塞主線程。

耗時任務 - Callback 實現

咱們都知道不管是請求網絡仍是讀取數據庫都是耗時任務,咱們不能在主線程去執行這些耗時操做。如今的手機 CPU 頻率都是很高的,Pixel 2 的單核 CPU 週期小於 0.0000000004 秒(0.4納秒),而一次網絡請求大約是 0.4 秒(400 毫秒)。能夠這麼說,一眨眼功夫能夠完成一次網絡請求,但同時 CPU 已經執行了 10 億屢次。 Android 平臺,主線程是 UI 線程,主要負責 View 的繪製(16 ms)和響應用戶操做。若是咱們在主線程作耗時操做,就會阻塞主線程,形成 View 不能及時刷新,不能及時響應用戶操做,從而影響用戶體驗。 爲了解決以上問題,咱們通常使用 Callbacks 的方式。舉個例子:git

class ViewModel: ViewModel() {
   fun fetchDocs() {
       get("developer.android.com") { result ->
           show(result)
       }
    }
}
複製代碼

儘管 get() 函數是被主線程調用的,但它的實現確定是要在其餘線程完成網絡請求的。當結果返回時,Callback 又會在主線程被調用,來將結果顯示到 UI 上。github

耗時任務 - 協程實現

協程能夠簡化異步代碼,用協程咱們能夠更方便地重寫上面的例子:數據庫

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.IO
    val result = get("developer.android.com")
    // Dispatchers.Main
    show(result)
}
// look at this in the next section
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}
複製代碼

與普通函數相比,協程添加了 suspendresume 兩種操做。這兩個操做一塊兒完成了 Callback 的工做,但更優雅,就像是用同步代碼完成了異步操做。編程

  • suspend:掛起當前協程,保存全部的本地變量;
  • resume:恢復已經掛起的協程,從它暫停的地方繼續執行。

suspend 是 Kotlin 的一個關鍵字。被 suspend 標記的函數,只能在 suspend 函數內被調用。咱們可使用協程提供的 launchasync 從主線程啓動一個協程來執行 suspend 函數。安全

public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

public fun <T> CoroutineScope.async( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
複製代碼

Coroutines 工做過程

如動畫所示,get() 函數在執行前會掛起(suspend)當前的協程,它內部依舊是經過 IO 線程(Dispatchers.IO)來執行網絡請求。當請求完成時,它不是經過 Callback 的方式,而是恢復(resume)已經掛起的協程繼續執行 show(result) 函數。任何一個協程被掛起時,當前的棧信息都會被複制並保存,以便在恢復時使用。當全部的協程都被掛起時,主線程不會被阻塞,仍然能夠更新 UI 和響應用戶操做。因而可知,協程爲咱們提供了一種異步操做的簡單實現方式。網絡

主線程安全

使用 Kotlin 協程的一個原則是:咱們應該保證咱們寫的 suspend 函數是主線程安全的,也就是能夠在任何線程中調用它,而不用去讓調用者手動切換線程。 須要注意的是:suspend 函數通常是運行在主線程中的,suspend 不是意味着運行的子線程。也就是說,咱們須要在 suspend 內部指定該函數執行的線程,如不指定,它默認運行在調用者的線程。 若是不是執行耗時任務,咱們可使用 Dispatchers.Main.immediate 來啓動一個協程,下一次 UI 刷新時就會將結果顯示到 UI 上。 全部的 Kotlin協程都必須運行在一個 Dispatcher 中,它提供瞭如下幾種 Dispatcher 來運行協程。併發

Dispatchers 用途 使用場景
Dispatchers.Main 主線程、UI交互、執行輕量任務 Call suspend functions, Call UI functions, Update LiveData
Dispatchers.IO 網絡請求、文件訪問 Database, Reading/writing files, Networking
Dispatchers.Default CPU密集型任務 Sorting a list, Parsing JSON, DiffUtils
Dispatchers.Unconfined 不限制任何指定線程 限制恢復後的線程

完整的 get() 函數以下所示:app

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.Main
    val result = get("developer.android.com")
    // Dispatchers.Main
    show(result)
}
// Dispatchers.Main
suspend fun get(url: String) =
    // Dispatchers.IO
    withContext(Dispatchers.IO) {
        // Dispatchers.IO
        /* perform blocking network IO here */
    }
    // Dispatchers.Main
複製代碼

使用協程咱們能夠自由控制代碼運行的線程,withContext() 爲咱們提供了相似編寫同步代碼的方式來實現異步編程。如上面提到的:咱們應儘可能使用 withContext 確保每一個函數都是線程安全的,不要讓調用者關心要在哪一個線程才能調用該函數。 上面的例子中,fetchDocs 運行在主線程,而 get() 運行在子線程,因爲協程的 掛起/恢復 機制,當 withContext 返回時,當前協程會恢復執行。 在性能方面,withContext 跟 Callbacks 或 RxJava 不相上下。因此咱們不用擔憂性能問題,相信官方也會持續優化。

結構化併發(Structured concurrency)

協程相比於線程來講,它是很輕量的。咱們能夠啓動上百上千個協程,但沒法啓動這麼多的線程。雖然協程很輕量,但它們的實際進行的任務多是耗時的,好比用於讀取數據庫、請求網絡或讀寫文件的協程。所以,咱們仍須要維護好這些協程的完成和取消,不然可能發生任務泄露,這比內存泄漏更嚴重,由於任務可能浪費 CPU、磁盤或網絡資源。 手動管理成百上千個協程是很困難的,爲了不協程泄露,Kotlin 提供了 結構化併發 來幫助咱們更方便地追蹤全部運行中的協程。在 Android 開發過程當中,咱們能夠用它來完成如下三件事:

  • 取消 再也不須要的任務
  • 追蹤 全部運行中的任務
  • 接收協程的 異常

取消限定範圍內的任務(Cancel work with scopes)

Kotlin 協程必須運行在 CoroutineScope 中,CoroutineScope 能夠追蹤全部運行中和已掛起的協程,不像上文提到的 Dispatchers,它只是保證對全部的協程的追蹤,而不會真正地執行它們。所以爲了確保全部的協程都能被追蹤到,咱們不能在 CoroutineScope 外啓動一個新的協程。同時咱們可使用 CoroutineScope 來取消在它內部啓動的全部協程。 咱們須要在普通函數中啓動一個協程,才能調用 suspend 函數。協程提供了兩種方式來啓動一個新的協程。

  • launch:啓動一個新協程,可是沒法得到它執行的結果。
  • async:啓動一個新協程,能夠經過調用它的 await() 函數得到協程的執行結果。

大多數狀況下,咱們使用 launch 來啓動一個新的協程。launch 函數就像鏈接普通函數和協程的橋樑。

scope.launch {
    // This block starts a new coroutine
    // "in" the scope.
    //
    // It can call suspend functions
   fetchDocs()
}
複製代碼

launchasync 最大的不一樣就是它們處理異常的方式:launch 啓動的協程在發生異常時會馬上拋出,並馬上取消全部協程;而 async 啓動的協程,只有咱們調用 await() 函數時才能獲得內部的異常,若無異常會返回執行結果。

AndroidX Lifecycle KTX 爲咱們提供了 viewModelScope 來方便地在 ViewModel 中啓動協程,並保持對它們的追蹤。

class MyViewModel(): ViewModel() {
    fun userNeedsDocs() {
        // Start a new coroutine in a ViewModel
        viewModelScope.launch {
            fetchDocs()
        }
    }
}
複製代碼

更多詳情可查看Kotlin coroutines meeting Architecture components

咱們能夠在一個 CoroutineScope 中包含若干個 CoroutineScope,若是咱們在一個協程中啓動了另外一個協程,其實它們最終都同屬於一個最頂層的 CoroutineScope,也就是說咱們能夠經過取消最外層的協程來取消全部內部的協程。 若是咱們取消一個已經掛起的協程,它會拋出一個異常 CancellationException。若是咱們捕獲並消費了這個異常,或者取消一個未掛起的協程,該協程會處於一個 半取消(semi-canceled)狀態。 viewModelScope 啓動的協程會在 ViewModel 銷燬(clear)時自動取消,因此即便咱們其內部執行是一個死循環,也會被自動取消。

fun runForever() {
    // start a new coroutine in the ViewModel
    viewModelScope.launch {
        // cancelled when the ViewModel is cleared
        while(true) {
            delay(1_000)
            // do something every second
        }
    }
}
複製代碼

追蹤進行中的任務(Keep track of work)

咱們可使用協程進行網絡請求、讀寫數據庫等耗時操做。但有時咱們可能須要在一個協程中同時進行兩個網絡請求,這時咱們須要再啓動兩個協程來共同工做。咱們能夠在任何一個 suspend 函數中使用 coroutineScopesupervisorScope 來啓動更多的協程。 在一個協程中啓動新的協程可能會形成潛在的任務泄露,由於調用者可能不知道咱們內部的實現。好消息是,結構化併發能夠保證:若是一個 suspend 函數返回了,那麼它內部的全部代碼都已經執行完畢。 這仍然是同步調用的影子。 舉個例子:

suspend fun fetchTwoDocs() {
    coroutineScope {
        launch { fetchDoc(1) }
        async { fetchDoc(2) }
    }
}
複製代碼

如上所示:fetchTwoDocs() 內部經過 coroutineScope 又啓動了兩個協程來同時加載兩個文檔,一個方式是 launch,一種方式是 async。爲了不 fetchTwoDocs() 任務泄露,coroutineScope 會一直保持掛起狀態,直到內部的全部協程都執行完畢,這時 fetchTwoDocs() 函數纔會返回。

coroutineScope keep track of 1_000 coroutines

以上示例,咱們同時啓動了 1000 個協程來請求網絡,loadLots() 內部的 coroutineScope 是 該函數調用者的 CoroutineScope 的子集,內部的 coroutineScope 會一直保持這 1000 個協程的追蹤,只有當全部協程都執行完畢,loadLots() 函數纔會返回。

coroutineScopesupervisorScope 可讓咱們在任意 suspend 函數內安全啓動協程,直到內部的全部協程都執行完畢,它們纔會返回。此外,若是咱們取消了外層的 scope,內部的子協程也會被取消。 coroutineScopesupervisorScope 的區別是:只要 coroutineScope 內的任一協程執行失敗,整個 scope 都會被取消,內部的其餘子協程也會馬上被取消;而 supervisorScope 內的某一協程失敗,不會取消其餘的子協程。

接收協程執行失敗拋出的異常(Signal errors when a coroutine fails)

和普通函數同樣,協程在執行失敗時也會拋出異常。suspend 函數內拋出的異常是會向上傳遞的,咱們也可使用 try/catch 語法或其餘方式捕獲異常。可是下面這種異常可能會丟失:

val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
    // async without structured concurrency
    unrelatedScope.async {
        throw InAsyncNoOneCanHearYou("except")
    }
}
複製代碼

以上代碼中,咱們在一個不相關的限定範圍內啓動了一個協程,它並非結構化併發的。因爲 async 函數啓動的協程只有在調用 await() 時纔會拋出異常,因此這個異常可能會丟失,它會被一直保存着直到咱們調用 await()結構化併發能夠保證當一個協程發生異常時,它的調用者或 scope 能夠收到這個異常。 上面代碼用結構化併發的方式改寫以下:

suspend fun foundError() {
    coroutineScope {
        async {
            throw StructuredConcurrencyWill("throw")
        }
    }
}
複製代碼

總結一下:

  • coroutineScopesupervisorScope 是結構化併發的,能夠追蹤內部的全部協程,包括異常處理、任務取消等。
  • GlobalScope 不是結構化併發的,它是一個全局的 scope,跟 Application 同生命週期。

Kotlin Coroutines VS RxJava&RxAndroid

Kotlin Coroutines 與 RxJava&RxAndroid 均可以方便的幫咱們進行異步編程,我的以爲它們在異步編程最大的區別是:Coroutines 的編寫方式更像是同步調用,而 RxJava 是流式編程。但本質上,它們內部都是經過線程池來處理耗時任務。RxJava 的有不少個操做符能夠輔助實現各式各樣的需求,並能保證鏈式調用;Coroutines 是與 Kotlin 結合的最好異步編程方式,目前也有不少的官方支持,相信未來 Coroutines 會有很好的使用體驗和執行性能。

Reference

聯繫

我是 xiaobailong24,您能夠經過如下平臺找到我:

相關文章
相關標籤/搜索