Android 上的 Kotlin 協(xié)程您机,由淺入深

協(xié)程是一種并發(fā)設(shè)計(jì)模式例嘱,你可以在 Android 平臺上使用它來簡化異步執(zhí)行的代碼蒋得。協(xié)程是在版本 1.3 中添加到 Kotlin 的,它基于來自其他語言的既定概念颠锉。

在 Android 上法牲,協(xié)程有助于管理長時間運(yùn)行的任務(wù),如果管理不當(dāng)琼掠,這些任務(wù)可能會阻塞主線程并導(dǎo)致應(yīng)用無響應(yīng)拒垃。使用協(xié)程的專業(yè)開發(fā)者中有超過 50% 的人反映使用協(xié)程提高了工作效率。本文章介紹如何使用 Kotlin 協(xié)程解決以下問題瓷蛙,從而讓你能夠編寫出更清晰恶复、更簡潔的應(yīng)用代碼。

特點(diǎn)

協(xié)程是我們在 Android 上進(jìn)行異步編程的推薦解決方案速挑。值得關(guān)注的特點(diǎn)包括:

  • 輕量:您可以在單個線程上運(yùn)行多個協(xié)程,因?yàn)閰f(xié)程支持掛起副硅,不會使正在運(yùn)行協(xié)程的線程阻塞姥宝。掛起比阻塞節(jié)省內(nèi)存,且支持多個并行操作恐疲。
  • 內(nèi)存泄漏更少:使用結(jié)構(gòu)化并發(fā)機(jī)制在一個作用域內(nèi)執(zhí)行多項(xiàng)操作腊满。
  • 內(nèi)置取消支持取消操作會自動在運(yùn)行中的整個協(xié)程層次結(jié)構(gòu)內(nèi)傳播套么。
  • Jetpack 集成:許多 Jetpack 庫都包含提供全面協(xié)程支持的擴(kuò)展。某些庫還提供自己的協(xié)程作用域碳蛋,可供您用于結(jié)構(gòu)化并發(fā)胚泌。

Android 平臺上,協(xié)程主要用來解決兩個問題:

  • 處理耗時任務(wù) (Long running tasks)肃弟,這種任務(wù)常常會阻塞住主線程玷室;
  • 保證主線程安全 (Main-safety) ,即確保安全地從主線程調(diào)用任何 suspend 函數(shù)笤受。

創(chuàng)建協(xié)程

創(chuàng)建協(xié)程這里介紹常用的兩種方式:

  • CoroutineScope.launch()
  • CoroutineScope.async()

這是常用的協(xié)程創(chuàng)建方式穷缤,launch 構(gòu)建器適合執(zhí)行 "一勞永逸" 的工作,意思就是說它可以啟動新協(xié)程而不將結(jié)果返回給調(diào)用方箩兽;async 構(gòu)建器可啟動新協(xié)程并允許您使用一個名為 await 的掛起函數(shù)返回 result津肛。 launchasync 之間的很大差異是它們對異常的處理方式不同。如果使用 async 作為最外層協(xié)程的開啟方式汗贫,它期望最終是通過調(diào)用 await 來獲取結(jié)果 (或者異常)身坐,所以默認(rèn)情況下它不會拋出異常。這意味著如果使用 async 啟動新的最外層協(xié)程落包,而不使用 await部蛇,它會靜默地將異常丟棄。
關(guān)于作用域妥色,更推薦的是在UI組件中使用 LifecycleOwner.lifecycleScope搪花,在 ViewModel 中使用 ViewModel.viewModelScope

CoroutineContext - 協(xié)程上下文

CoroutineContext 即協(xié)程的上下文嘹害,是 Kotlin 協(xié)程的一個基本結(jié)構(gòu)單元撮竿。巧妙的運(yùn)用協(xié)程上下文是至關(guān)重要的,以此來實(shí)現(xiàn)正確的線程行為笔呀、生命周期幢踏、異常以及調(diào)試。它包含用戶定義的一些數(shù)據(jù)集合许师,這些數(shù)據(jù)與協(xié)程密切相關(guān)房蝉。它是一個有索引的 Element 實(shí)例集合。這個有索引的集合類似于一個介于 set 和 map之間的數(shù)據(jù)結(jié)構(gòu)微渠。每個 element 在這個集合有一個唯一的 Key 搭幻。當(dāng)多個 element 的 key 的引用相同,則代表屬于集合里同一個 element逞盆。它由如下幾項(xiàng)構(gòu)成:

  • Job: 控制協(xié)程的生命周期檀蹋;
  • CoroutineDispatcher: 向合適的線程分發(fā)任務(wù);
  • CoroutineName: 協(xié)程的名稱云芦,調(diào)試的時候很有用俯逾;
  • CoroutineExceptionHandler: 處理未被捕捉的異常贸桶。

CoroutineContext 有兩個非常重要的元素 — JobDispatcherJob 是當(dāng)前的 Coroutine 實(shí)例而 Dispatcher 決定了當(dāng)前 Coroutine 執(zhí)行的線程桌肴,還可以添加 CoroutineName皇筛,用于調(diào)試,添加 CoroutineExceptionHandler 用于捕獲異常坠七,它們都實(shí)現(xiàn)了 Element 接口水醋。
CoroutineContext 接口的定義如下:

//Persistent context for the coroutine. It is an indexed set of Element instances. An indexed set is a mix between a set and a map. Every element in this set has a unique Key.
public interface CoroutineContext {
    //操作符 get:可以通過 key 來獲取這個 Element。由于這是一個 get 操作符灼捂,所以可以像訪問 map 中的元素一樣使用 context[key] 這種中括號的形式來訪問离例。
    public operator fun <E : Element> get(key: Key<E>): E?
    //和 Collection.fold 擴(kuò)展函數(shù)類似,提供遍歷當(dāng)前 context 中所有 Element 的能力悉稠。
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R
    //操作符 plus:和 Set.plus 擴(kuò)展函數(shù)類似宫蛆,返回一個新的 context 對象,新的對象里面包含了兩個里面的所有 Element的猛,如果遇到重復(fù)的(Key 一樣的)耀盗,那么用+號右邊的 Element 替代左邊的。+  運(yùn)算符可以很容易的用于結(jié)合上下文卦尊,但是有一個很重要的事情需要小心 —— 要注意它們結(jié)合的次序叛拷,因?yàn)檫@個  + 運(yùn)算符是不對稱的。
    public operator fun plus(context: CoroutineContext): CoroutineContext{...}
    //返回一個上下文岂却,其中包含該上下文中的元素忿薇,但不包含具有指定key的元素。
    public fun minusKey(key: Key<*>): CoroutineContext
    //Key for the elements of CoroutineContext. E is a type of element with this key.
    public interface Key<E : Element>
    //An element of the CoroutineContext. An element of the coroutine context is a singleton context by itself.
    public interface Element : CoroutineContext {
        /**
         * A key of this coroutine context element.
         */
        public val key: Key<*>

        public override operator fun <E : Element> get(key: Key<E>): E? =
            @Suppress("UNCHECKED_CAST")
            if (this.key == key) this as E else null

        public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
            operation(initial, this)

        public override fun minusKey(key: Key<*>): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this
    }
}

某些情況需要一個上下文不持有任何元素躏哩,此時就可以使用
EmptyCoroutineContext 對象署浩。可以預(yù)見扫尺,添加這個對象到另一個上下文不會對其有任何影響筋栋。

在任務(wù)層級中,每個協(xié)程都會有一個父級對象正驻,要么是 CoroutineScope 或者另外一個 coroutine弊攘。然而,實(shí)際上協(xié)程的父級 CoroutineContext 和父級協(xié)程的 CoroutineContext 是不一樣的姑曙,因?yàn)橛腥缦碌墓?

父級上下文 = 默認(rèn)值 + 繼承的 CoroutineContext + 參數(shù)

其中:

  • 一些元素包含默認(rèn)值: Dispatchers.Default 是默認(rèn)的 CoroutineDispatcher襟交,以及 coroutine 作為默認(rèn)的 CoroutineName
  • 繼承的 CoroutineContextCoroutineScope 或者其父協(xié)程的 CoroutineContext伤靠;
  • 傳入?yún)f(xié)程 builder 的參數(shù)的優(yōu)先級高于繼承的上下文參數(shù)婿着,因此會覆蓋對應(yīng)的參數(shù)值。

請注意: CoroutineContext 可以使用 " + " 運(yùn)算符進(jìn)行合并。由于 CoroutineContext 是由一組元素組成的竟宋,所以加號右側(cè)的元素會覆蓋加號左側(cè)的元素,進(jìn)而組成新創(chuàng)建的 CoroutineContext形纺。比如丘侠,(Dispatchers.Main, "name") + (Dispatchers.IO) = (Dispatchers.IO, "name")

Job & Deferred - 任務(wù)

Job 用于處理協(xié)程逐样。對于每一個所創(chuàng)建的協(xié)程 (通過 launch 或者 async)蜗字,它會返回一個 Job 實(shí)例,該實(shí)例是協(xié)程的唯一標(biāo)識脂新,并且負(fù)責(zé)管理協(xié)程的生命周期挪捕。
CoroutineScope.launch 函數(shù)返回的是一個 Job 對象,代表一個異步的任務(wù)争便。Job 具有生命周期并且可以取消级零。 Job 還可以有層級關(guān)系,一個 Job 可以包含多個子 Job滞乙,當(dāng)父 Job 被取消后奏纪,所有的子 Job 也會被自動取消;當(dāng)子 Job被取消或者出現(xiàn)異常后父 Job 也會被取消斩启。
除了通過 CoroutineScope.launch 來創(chuàng)建 Job 對象之外序调,還可以通過 Job() 工廠方法來創(chuàng)建該對象。默認(rèn)情況下兔簇,子 Job 的失敗將會導(dǎo)致父 Job 被取消发绢,這種默認(rèn)的行為可以通過 SupervisorJob 來修改。
具有多個子 Job 的父 Job 會等待所有子 Job 完成(或者取消)后垄琐,自己才會執(zhí)行完成边酒。

Job 的狀態(tài)

一個任務(wù)可以包含一系列狀態(tài): 新創(chuàng)建 (New)、活躍 (Active)此虑、完成中 (Completing)甚纲、已完成 (Completed)、取消中 (Cancelling) 和已取消 (Cancelled)朦前。雖然我們無法直接訪問這些狀態(tài)介杆,但是我們可以訪問 Job 的屬性: isActiveisCancelledisCompleted韭寸。
如果協(xié)程處于活躍狀態(tài)春哨,協(xié)程運(yùn)行出錯或者調(diào)用 job.cancel() 都會將當(dāng)前任務(wù)置為取消中 (Cancelling) 狀態(tài) (isActive = false, isCancelled = true)。當(dāng)所有的子協(xié)程都完成后恩伺,協(xié)程會進(jìn)入已取消 (Cancelled) 狀態(tài)赴背,此時 isCompleted = true

Job 的常用函數(shù)

這些函數(shù)都是線程安全的,所以可以直接在其他 Coroutine 中調(diào)用凰荚。

  • fun start(): Boolean
    調(diào)用該函數(shù)來啟動這個 Coroutine燃观,如果當(dāng)前 Coroutine 還沒有執(zhí)行調(diào)用該函數(shù)返回 true,如果當(dāng)前 Coroutine 已經(jīng)執(zhí)行或者已經(jīng)執(zhí)行完畢便瑟,則調(diào)用該函數(shù)返回 false缆毁。
  • fun cancel(cause: CancellationException? = null)
    通過可選的取消原因取消此作業(yè)。 原因可以用于指定錯誤消息或提供有關(guān)取消原因的其他詳細(xì)信息到涂,以進(jìn)行調(diào)試脊框。
  • fun invokeOnCompletion(handler: CompletionHandler): DisposableHandler
    通過這個函數(shù)可以給 Job 設(shè)置一個完成通知,當(dāng) Job 執(zhí)行完成的時候會同步執(zhí)行這個通知函數(shù)践啄。 回調(diào)的通知對象類型為:typealias CompletionHandler = (cause: Throwable?) -> Unit. CompletionHandler 參數(shù)代表了 Job 是如何執(zhí)行完成的浇雹。 cause 有下面三種情況:
  • 如果 Job 是正常執(zhí)行完成的,則 cause 參數(shù)為 null
  • 如果 Job 是正常取消的屿讽,則 cause 參數(shù)為 CancellationException 對象昭灵。這種情況不應(yīng)該當(dāng)做錯誤處理,這是任務(wù)正常取消的情形聂儒。所以一般不需要在錯誤日志中記錄這種情況虎锚。
  • 其他情況表示 Job 執(zhí)行失敗了。

這個函數(shù)的返回值為 DisposableHandle 對象衩婚,如果不再需要監(jiān)控 Job 的完成情況了窜护, 則可以調(diào)用 DisposableHandle.dispose 函數(shù)來取消監(jiān)聽。如果 Job 已經(jīng)執(zhí)行完了非春, 則無需調(diào)用 dispose 函數(shù)了柱徙,會自動取消監(jiān)聽。

  • suspend fun join()
    join 函數(shù)和前面三個函數(shù)不同奇昙,這是一個 suspend 函數(shù)护侮。所以只能在 Coroutine 內(nèi)調(diào)用。
    這個函數(shù)會暫停當(dāng)前所處的 Coroutine 直到該 Coroutine 執(zhí)行完成储耐。所以 join 函數(shù)一般用來在另外一個 Coroutine 中等待 job 執(zhí)行完成后繼續(xù)執(zhí)行羊初。當(dāng) Job 執(zhí)行完成后, job.join 函數(shù)恢復(fù)什湘,這個時候 job 這個任務(wù)已經(jīng)處于完成狀態(tài)了长赞,而調(diào)用 job.joinCoroutine 還繼續(xù)處于 activie 狀態(tài)。
    請注意闽撤,只有在其所有子級都完成后得哆,作業(yè)才能完成。
    該函數(shù)的掛起是可以被取消的哟旗,并且始終檢查調(diào)用的 CoroutineJob 是否取消贩据。如果在調(diào)用此掛起函數(shù)或?qū)⑵鋻炱饡r栋操,調(diào)用 CoroutineJob 被取消或完成,則此函數(shù)將引發(fā) CancellationException饱亮。

Deferred

public interface Deferred<out T> : Job {
    //用來等待這個Coroutine執(zhí)行完畢并返回結(jié)果矾芙。
    public val onAwait: SelectClause1<T>

    public suspend fun await(): T
    //用來獲取Coroutine執(zhí)行的結(jié)果。如果Coroutine還沒有執(zhí)行完成則會拋出 IllegalStateException 近上,如果任務(wù)被取消了也會拋出對應(yīng)的異常蠕啄。所以在執(zhí)行這個函數(shù)之前,可以通過 isCompleted 來判斷一下當(dāng)前任務(wù)是否執(zhí)行完畢了戈锻。
    @ExperimentalCoroutinesApi
    public fun getCompleted(): T
    //獲取已完成狀態(tài)的Coroutine異常信息,如果任務(wù)正常執(zhí)行完成了和媳,則不存在異常信息格遭,返回null。如果還沒有處于已完成狀態(tài)留瞳,則調(diào)用該函數(shù)同樣會拋出 IllegalStateException拒迅,可以通過 isCompleted 來判斷一下當(dāng)前任務(wù)是否執(zhí)行完畢了。
    @ExperimentalCoroutinesApi
    public fun getCompletionExceptionOrNull(): Throwable?
}

通過使用 async 創(chuàng)建協(xié)程可以得到一個有返回值 Deferred她倘,Deferred 接口繼承自 Job 接口璧微,額外提供了獲取 Coroutine 返回結(jié)果的方法。由于 Deferred 繼承自 Job 接口硬梁,所以 Job 相關(guān)的內(nèi)容在 Deferred 上也是適用的前硫。 Deferred 提供了額外三個函數(shù)來處理和 Coroutine 執(zhí)行結(jié)果相關(guān)的操作。

SupervisorJob

/**
 * Creates a _supervisor_ job object in an active state.
 * Children of a supervisor job can fail independently of each other.
 * 
 * A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children,
 * so a supervisor can implement a custom policy for handling failures of its children:
 *
 * * A failure of a child job that was created using [launch][CoroutineScope.launch] can be handled via [CoroutineExceptionHandler] in the context.
 * * A failure of a child job that was created using [async][CoroutineScope.async] can be handled via [Deferred.await] on the resulting deferred value.
 *
 * If [parent] job is specified, then this supervisor job becomes a child job of its parent and is cancelled when its
 * parent fails or is cancelled. All this supervisor's children are cancelled in this case, too. The invocation of
 * [cancel][Job.cancel] with exception (other than [CancellationException]) on this supervisor job also cancels parent.
 *
 * @param parent an optional parent job.
 */
@Suppress("FunctionName")
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)

該函數(shù)創(chuàng)建了一個處于 active 狀態(tài)的 supervisor job 荧止。如前所述屹电, Job 是有父子關(guān)系的,如果子 Job 失敗了父 Job 會自動失敗跃巡,這種默認(rèn)的行為可能不是我們期望的危号。比如在 Activity 中有兩個子 Job 分別獲取一篇文章的評論內(nèi)容和作者信息。如果其中一個失敗了素邪,我們并不希望父 Job 自動取消外莲,這樣會導(dǎo)致另外一個子 Job 也被取消。而 SupervisorJob 就是這么一個特殊的 Job兔朦,里面的子 Job 不相互影響偷线,一個子 Job 失敗了,不影響其他子 Job 的執(zhí)行烘绽。SupervisorJob(parent:Job?) 具有一個 parent 參數(shù)淋昭,如果指定了這個參數(shù),則所返回的 Job 就是參數(shù) parent 的子Job安接。如果 Parent Job 失敗了或者取消了翔忽,則這個 Supervisor Job 也會被取消闪水。當(dāng) Supervisor Job 被取消后振湾,所有 Supervisor Job 的子 Job 也會被取消。
MainScope() 的實(shí)現(xiàn)就使用了 SupervisorJob 和一個 Main Dispatcher

/**
 * Creates the main [CoroutineScope] for UI components.
 *
 * Example of use:
 * ```
 * class MyAndroidActivity {
 *     private val scope = MainScope()
 *
 *     override fun onDestroy() {
 *         super.onDestroy()
 *         scope.cancel()
 *     }
 * }
 * ```
 *
 * The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements.
 * If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator:
 * `val scope = MainScope() + CoroutineName("MyActivity")`.
 */
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

但是SupervisorJob是很容易被誤解的,它和協(xié)程異常處理状囱、子協(xié)程所屬Job類型還有域有很多讓人混淆的地方,具體異常處理可以看Google的這一篇文章:協(xié)程中的取消和異常 | 異常處理詳解

CoroutineDispatcher - 調(diào)度器

CoroutineDispatcher 定義了 Coroutine 執(zhí)行的線程蓝丙。CoroutineDispatcher 可以限定 Coroutine 在某一個線程執(zhí)行邪铲、也可以分配到一個線程池來執(zhí)行、也可以不限制其執(zhí)行的線程龙巨。
CoroutineDispatcher 是一個抽象類笼呆,所有 dispatcher 都應(yīng)該繼承這個類來實(shí)現(xiàn)對應(yīng)的功能。Dispatchers 是一個標(biāo)準(zhǔn)庫中幫我們封裝了切換線程的幫助類旨别,可以簡單理解為一個線程池诗赌。

  • Dispatchers.Default
    默認(rèn)的調(diào)度器,適合處理后臺計(jì)算秸弛,是一個 CPU 密集型任務(wù)調(diào)度器铭若。如果創(chuàng)建 Coroutine 的時候沒有指定 dispatcher,則一般默認(rèn)使用這個作為默認(rèn)值递览。Default dispatcher 使用一個共享的后臺線程池來運(yùn)行里面的任務(wù)叼屠。注意它和IO共享線程池,只不過限制了最大并發(fā)數(shù)不同绞铃。

  • Dispatchers.IO
    顧名思義這是用來執(zhí)行阻塞 IO 操作的镜雨,是和 Default 共用一個共享的線程池來執(zhí)行里面的任務(wù)。根據(jù)同時運(yùn)行的任務(wù)數(shù)量憎兽,在需要的時候會創(chuàng)建額外的線程冷离,當(dāng)任務(wù)執(zhí)行完畢后會釋放不需要的線程。

  • Dispatchers.Unconfined
    由于 Dispatchers.Unconfined 未定義線程池纯命,所以執(zhí)行的時候默認(rèn)在啟動線程西剥。遇到第一個掛起點(diǎn),之后由調(diào)用 resume 的線程決定恢復(fù)協(xié)程的線程亿汞。

  • Dispatchers.Main
    指定執(zhí)行的線程是主線程瞭空,在 Android 上就是 UI 線程·

由于 子Coroutine 會繼承 父Coroutinecontext,所以為了方便使用疗我,我們一般會在 父Coroutine 上設(shè)定一個 Dispatcher咆畏,然后所有 子Coroutine 自動使用這個 Dispatcher

CoroutineStart - 協(xié)程啟動模式

  • CoroutineStart.DEFAULT:
    協(xié)程創(chuàng)建后立即開始調(diào)度吴裤,在調(diào)度前如果協(xié)程被取消旧找,其將直接進(jìn)入取消響應(yīng)的狀態(tài)。
    雖然是立即調(diào)度麦牺,但也有可能在執(zhí)行前被取消钮蛛。

  • CoroutineStart.ATOMIC:
    協(xié)程創(chuàng)建后立即開始調(diào)度鞭缭,協(xié)程執(zhí)行到第一個掛起點(diǎn)之前不響應(yīng)取消。
    雖然是立即調(diào)度魏颓,但其將調(diào)度和執(zhí)行兩個步驟合二為一了岭辣,就像它的名字一樣,其保證調(diào)度和執(zhí)行是原子操作甸饱,因此協(xié)程也一定會執(zhí)行沦童。

  • CoroutineStart.LAZY:
    只要協(xié)程被需要時,包括主動調(diào)用該協(xié)程的 start 叹话、join 或者 await 等函數(shù)時才會開始調(diào)度偷遗,如果調(diào)度前就被取消,協(xié)程將直接進(jìn)入異常結(jié)束狀態(tài)驼壶。

  • CoroutineStart.UNDISPATCHED:
    協(xié)程創(chuàng)建后立即在當(dāng)前函數(shù)調(diào)用棧中執(zhí)行鹦肿,直到遇到第一個真正掛起的點(diǎn)。
    是立即執(zhí)行辅柴,因此協(xié)程一定會執(zhí)行。

這些啟動模式的設(shè)計(jì)主要是為了應(yīng)對某些特殊的場景瞭吃。業(yè)務(wù)開發(fā)實(shí)踐中通常使用 DEFAULTLAZY 這兩個啟動模式就夠了碌嘀。

CoroutineScope - 協(xié)程作用域

定義協(xié)程必須指定其 CoroutineScopeCoroutineScope 可以對協(xié)程進(jìn)行追蹤歪架,即使協(xié)程被掛起也是如此股冗。同調(diào)度程序 (Dispatcher) 不同,CoroutineScope 并不運(yùn)行協(xié)程和蚪,它只是確保您不會失去對協(xié)程的追蹤止状。為了確保所有的協(xié)程都會被追蹤,Kotlin 不允許在沒有使用 CoroutineScope 的情況下啟動新的協(xié)程攒霹。CoroutineScope 可被看作是一個具有超能力的 ExecutorService 的輕量級版本怯疤。CoroutineScope 會跟蹤所有協(xié)程,同樣它還可以取消由它所啟動的所有協(xié)程催束。這在 Android 開發(fā)中非常有用集峦,比如它能夠在用戶離開界面時停止執(zhí)行協(xié)程。
??Coroutine 是輕量級的線程抠刺,并不意味著就不消耗系統(tǒng)資源塔淤。 當(dāng)異步操作比較耗時的時候,或者當(dāng)異步操作出現(xiàn)錯誤的時候速妖,需要把這個 Coroutine 取消掉來釋放系統(tǒng)資源高蜂。在 Android 環(huán)境中,通常每個界面(Activity罕容、Fragment 等)啟動的 Coroutine 只在該界面有意義备恤,如果用戶在等待 Coroutine 執(zhí)行的時候退出了這個界面稿饰,則再繼續(xù)執(zhí)行這個 Coroutine 可能是沒必要的。另外 Coroutine 也需要在適當(dāng)?shù)?context 中執(zhí)行烘跺,否則會出現(xiàn)錯誤湘纵,比如在非 UI 線程去訪問 View。 所以 Coroutine 在設(shè)計(jì)的時候滤淳,要求在一個范圍(Scope)內(nèi)執(zhí)行梧喷,這樣當(dāng)這個 Scope 取消的時候,里面所有的子 Coroutine 也自動取消脖咐。所以要使用 Coroutine 必須要先創(chuàng)建一個對應(yīng)的 CoroutineScope铺敌。

CoroutineScope 接口

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

CoroutineScope 只是定義了一個新 Coroutine 的執(zhí)行 Scope。每個 coroutine builder 都是 CoroutineScope 的擴(kuò)展函數(shù)屁擅,并且自動的繼承了當(dāng)前 ScopecoroutineContext 偿凭。

分類及行為規(guī)則

官方框架在實(shí)現(xiàn)復(fù)合協(xié)程的過程中也提供了作用域,主要用以明確寫成之間的父子關(guān)系派歌,以及對于取消或者異常處理等方面的傳播行為弯囊。該作用域包括以下三種:

  • 頂級作用域
    沒有父協(xié)程的協(xié)程所在的作用域?yàn)轫敿壸饔糜颉?/p>

  • 協(xié)同作用域
    協(xié)程中啟動新的協(xié)程,新協(xié)程為所在協(xié)程的子協(xié)程胶果,這種情況下匾嘱,子協(xié)程所在的作用域默認(rèn)為協(xié)同作用域。此時子協(xié)程拋出的未捕獲異常早抠,都將傳遞給父協(xié)程處理霎烙,父協(xié)程同時也會被取消。

  • 主從作用域
    與協(xié)同作用域在協(xié)程的父子關(guān)系上一致蕊连,區(qū)別在于悬垃,處于該作用域下的協(xié)程出現(xiàn)未捕獲的異常時,不會將異常向上傳遞給父協(xié)程甘苍。

除了三種作用域中提到的行為以外尝蠕,父子協(xié)程之間還存在以下規(guī)則:

  • 父協(xié)程被取消,則所有子協(xié)程均被取消载庭。由于協(xié)同作用域和主從作用域中都存在父子協(xié)程關(guān)系趟佃,因此此條規(guī)則都適用。
  • 父協(xié)程需要等待子協(xié)程執(zhí)行完畢之后才會最終進(jìn)入完成狀態(tài)昧捷,不管父協(xié)程自身的協(xié)程體是否已經(jīng)執(zhí)行完闲昭。
  • 子協(xié)程會繼承父協(xié)程的協(xié)程上下文中的元素,如果自身有相同key的成員靡挥,則覆蓋對應(yīng)的key序矩,覆蓋的效果僅限自身范圍內(nèi)有效。

常用作用域

官方庫給我們提供了一些作用域可以直接來使用跋破,并且 Android 的Lifecycle Ktx庫也封裝了更好用的作用域簸淀,下面看一下各種作用域

GlobalScope - 不推薦使用

public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

GlobalScope 是一個單例實(shí)現(xiàn)瓶蝴,源碼十分簡單,上下文是 EmptyCoroutineContext租幕,是一個空的上下文舷手,切不包含任何 Job,該作用域常被拿來做示例代碼劲绪,由于 GlobalScope 對象沒有和應(yīng)用生命周期組件相關(guān)聯(lián)男窟,需要自己管理 GlobalScope 所創(chuàng)建的 Coroutine,且 GlobalScope 的生命周期是 process 級別的贾富,所以一般而言我們不推薦使用 GlobalScope 來創(chuàng)建 Coroutine歉眷。

runBlocking{} - 主要用于測試

/**
 * Runs a new coroutine and **blocks** the current thread _interruptibly_ until its completion.
 * This function should not be used from a coroutine. It is designed to bridge regular blocking code
 * to libraries that are written in suspending style, to be used in `main` functions and in tests.
 *
 * The default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes continuations
 * in this blocked thread until the completion of this coroutine.
 * See [CoroutineDispatcher] for the other implementations that are provided by `kotlinx.coroutines`.
 *
 * When [CoroutineDispatcher] is explicitly specified in the [context], then the new coroutine runs in the context of
 * the specified dispatcher while the current thread is blocked. If the specified dispatcher is an event loop of another `runBlocking`,
 * then this invocation uses the outer event loop.
 *
 * If this blocked thread is interrupted (see [Thread.interrupt]), then the coroutine job is cancelled and
 * this `runBlocking` invocation throws [InterruptedException].
 *
 * See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging facilities that are available
 * for a newly created coroutine.
 *
 * @param context the context of the coroutine. The default value is an event loop on the current thread.
 * @param block the coroutine code.
 */
@Throws(InterruptedException::class)
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    val currentThread = Thread.currentThread()
    val contextInterceptor = context[ContinuationInterceptor]
    val eventLoop: EventLoop?
    val newContext: CoroutineContext
    if (contextInterceptor == null) {
        // create or use private event loop if no dispatcher is specified
        eventLoop = ThreadLocalEventLoop.eventLoop
        newContext = GlobalScope.newCoroutineContext(context + eventLoop)
    } else {
        // See if context's interceptor is an event loop that we shall use (to support TestContext)
        // or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
        eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
            ?: ThreadLocalEventLoop.currentOrNull()
        newContext = GlobalScope.newCoroutineContext(context)
    }
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}

這是一個頂層函數(shù),從源碼的注釋中我們可以得到一些信息颤枪,運(yùn)行一個新的協(xié)程并且阻塞當(dāng)前可中斷的線程直至協(xié)程執(zhí)行完成汗捡,該函數(shù)不應(yīng)從一個協(xié)程中使用,該函數(shù)被設(shè)計(jì)用于橋接普通阻塞代碼到以掛起風(fēng)格(suspending style)編寫的庫畏纲,以用于主函數(shù)與測試扇住。該函數(shù)主要用于測試,不適用于日常開發(fā)盗胀,該協(xié)程會阻塞當(dāng)前線程直到協(xié)程體執(zhí)行完成台囱。

MainScope() - 可用于開發(fā)

/**
 * Creates the main [CoroutineScope] for UI components.
 *
 * Example of use:
 * ```
 * class MyAndroidActivity {
 *     private val scope = MainScope()
 *
 *     override fun onDestroy() {
 *         super.onDestroy()
 *         scope.cancel()
 *     }
 * }
 * ```
 *
 * The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements.
 * If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator:
 * `val scope = MainScope() + CoroutineName("MyActivity")`.
 */
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

該函數(shù)是一個頂層函數(shù),用于返回一個上下文是 SupervisorJob() + Dispatchers.Main 的作用域读整,該作用域常被使用在 Activity/Fragment,并且在界面銷毀時要調(diào)用 fun CoroutineScope.cancel(cause: CancellationException? = null) 對協(xié)程進(jìn)行取消咱娶,這是官方庫中可以在開發(fā)中使用的一個用于獲取作用域的頂層函數(shù)米间,使用示例在官方庫的代碼注釋中已經(jīng)給出,上面的源碼中也有膘侮,使用起來也是十分的方便屈糊。

LifecycleOwner.lifecycleScope - 推薦使用

/**
 * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
 *
 * This scope will be cancelled when the [Lifecycle] is destroyed.
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
 */
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

該擴(kuò)展屬性是 AndroidLifecycle Ktx 庫提供的具有生命周期感知的協(xié)程作用域,它與 LifecycleOwnerLifecycle 綁定琼了,Lifecycle 被銷毀時逻锐,此作用域?qū)⒈蝗∠_@是在 Activity/Fragment 中推薦使用的作用域雕薪,因?yàn)樗鼤c當(dāng)前的UI組件綁定生命周期昧诱,界面銷毀時該協(xié)程作用域?qū)⒈蝗∠粫斐蓞f(xié)程泄漏所袁,相同作用的還有下文提到的 ViewModel.viewModelScope盏档。

ViewModel.viewModelScope - 推薦使用

/**
 * [CoroutineScope] tied to this [ViewModel].
 * This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
 */
val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if (scope != null) {
                return scope
            }
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
        }

該擴(kuò)展屬性和上文中提到的 LifecycleOwner.lifecycleScope 基本一致,它是 ViewModel 的擴(kuò)展屬性燥爷,也是來自 AndroidLifecycle Ktx 庫蜈亩,它能夠在此 ViewModel 銷毀時自動取消懦窘,同樣不會造成協(xié)程泄漏。該擴(kuò)展屬性返回的作用域的上下文同樣是 SupervisorJob() + Dispatchers.Main.immediate 稚配。

coroutineScope & supervisorScope

/**
 * Creates a [CoroutineScope] with [SupervisorJob] and calls the specified suspend block with this scope.
 * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
 * context's [Job] with [SupervisorJob].
 *
 * A failure of a child does not cause this scope to fail and does not affect its other children,
 * so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for details.
 * A failure of the scope itself (exception thrown in the [block] or cancellation) fails the scope with all its children,
 * but does not cancel parent job.
 */
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = SupervisorCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}



/**
 * Creates a [CoroutineScope] and calls the specified suspend block with this scope.
 * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
 * the context's [Job].
 *
 * This function is designed for _parallel decomposition_ of work. When any child coroutine in this scope fails,
 * this scope fails and all the rest of the children are cancelled (for a different behavior see [supervisorScope]).
 * This function returns as soon as the given block and all its children coroutines are completed.
 * A usage example of a scope looks like this:
 *
 * ```
 * suspend fun showSomeData() = coroutineScope {
 *     val data = async(Dispatchers.IO) { // <- extension on current scope
 *      ... load some UI data for the Main thread ...
 *     }
 *
 *     withContext(Dispatchers.Main) {
 *         doSomeWork()
 *         val result = data.await()
 *         display(result)
 *     }
 * }
 * ```
 *
 * The scope in this example has the following semantics:
 * 1) `showSomeData` returns as soon as the data is loaded and displayed in the UI.
 * 2) If `doSomeWork` throws an exception, then the `async` task is cancelled and `showSomeData` rethrows that exception.
 * 3) If the outer scope of `showSomeData` is cancelled, both started `async` and `withContext` blocks are cancelled.
 * 4) If the `async` block fails, `withContext` will be cancelled.
 *
 * The method may throw a [CancellationException] if the current job was cancelled externally
 * or may throw a corresponding unhandled [Throwable] if there is any unhandled exception in this scope
 * (for example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope).
 */
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = ScopeCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}

首先這兩個函數(shù)都是掛起函數(shù)畅涂,需要運(yùn)行在協(xié)程內(nèi)或掛起函數(shù)內(nèi)。supervisorScope 屬于主從作用域道川,會繼承父協(xié)程的上下文午衰,它的特點(diǎn)就是子協(xié)程的異常不會影響父協(xié)程,它的設(shè)計(jì)應(yīng)用場景多用于子協(xié)程為獨(dú)立對等的任務(wù)實(shí)體的時候愤惰,比如一個下載器苇经,每一個子協(xié)程都是一個下載任務(wù),當(dāng)一個下載任務(wù)異常時宦言,它不應(yīng)該影響其他的下載任務(wù)扇单。coroutineScopesupervisorScope 都會返回一個作用域,它倆的差別就是異常傳播:coroutineScope 內(nèi)部的異常會向上傳播奠旺,子協(xié)程未捕獲的異常會向上傳遞給父協(xié)程蜘澜,任何一個子協(xié)程異常退出,會導(dǎo)致整體的退出响疚;supervisorScope 內(nèi)部的異常不會向上傳播鄙信,一個子協(xié)程異常退出,不會影響父協(xié)程和兄弟協(xié)程的運(yùn)行忿晕。

協(xié)程的取消和異常

普通協(xié)程如果產(chǎn)生未處理異常會將此異常傳播至它的父協(xié)程装诡,然后父協(xié)程會取消所有的子協(xié)程、取消自己践盼、將異常繼續(xù)向上傳遞

這種情況有的時候并不是我們想要的鸦采,我們更希望一個協(xié)程在產(chǎn)生異常時,不影響其他協(xié)程的執(zhí)行咕幻,在上文中我們也提到了一些解決方案渔伯,下面我們就在實(shí)踐一下。

使用SupervisorJob**

在上文中我們也對這個頂層函數(shù)做了講解肄程,那如何使用呢锣吼?直接上代碼:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import kotlinx.coroutines.*

class MainActivity : AppCompatActivity() {

    /**
     * 使用官方庫的 MainScope()獲取一個協(xié)程作用域用于創(chuàng)建協(xié)程
     */
    private val mScope = MainScope()

    companion object {
        const val TAG = "Kotlin Coroutine"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mScope.launch(Dispatchers.Default) {
            delay(500)
            Log.e(TAG, "Child 1")
        }

        mScope.launch(Dispatchers.Default) {
            delay(1000)
            Log.e(TAG, "Child 2")
            throw RuntimeException("--> RuntimeException <--")
        }

        mScope.launch(Dispatchers.Default) {
            delay(1500)
            Log.e(TAG, "Child 3")
        }
    }
}


打印結(jié)果:
E/Kotlin Coroutine: Child 1
E/Kotlin Coroutine: Child 2
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-3
    Process: com.quyunshuo.kotlincoroutine, PID: 24240
    java.lang.RuntimeException: --> RuntimeException <--
        at com.quyunshuo.kotlincoroutine.MainActivity$onCreate$2.invokeSuspend(MainActivity.kt:31)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
E/Kotlin Coroutine: Child 3

MainScope() 我們之前提到過了,它的實(shí)現(xiàn)就是用了 SupervisorJob 蓝厌。執(zhí)行結(jié)果就是 Child 2 拋出異常后玄叠,Child 3 正常執(zhí)行了,但是程序崩了拓提,因?yàn)槲覀儧]有處理這個異常诸典,下面完善一下代碼

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    mScope.launch(Dispatchers.Default) {
        delay(500)
        Log.e(TAG, "Child 1")
    }

    // 在Child 2的上下文添加了異常處理
    mScope.launch(Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.e(TAG, "CoroutineExceptionHandler: $throwable")
    }) {
        delay(1000)
        Log.e(TAG, "Child 2")
        throw RuntimeException("--> RuntimeException <--")
    }

    mScope.launch(Dispatchers.Default) {
        delay(1500)
        Log.e(TAG, "Child 3")
    }
}


輸出結(jié)果:
E/Kotlin Coroutine: Child 1
E/Kotlin Coroutine: Child 2
E/Kotlin Coroutine: CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <--
E/Kotlin Coroutine: Child 3

這一次,程序沒有崩潰,并且異常處理的打印也輸出了狐粱,這就達(dá)到了我們想要的效果舀寓。但是要注意一個事情,這幾個子協(xié)程的父級是 SupervisorJob肌蜻,但是他們再有子協(xié)程的話互墓,他們的子協(xié)程的父級就不是 SupervisorJob了,所以當(dāng)它們產(chǎn)生異常時蒋搜,就不是我們演示的效果了篡撵。
新的協(xié)程被創(chuàng)建時,會生成新的 Job 實(shí)例替代 SupervisorJob豆挽。

使用supervisorScope

這個作用域我們上文中也有提到育谬,使用 supervisorScope 也可以達(dá)到我們想要的效果,上代碼:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import kotlinx.coroutines.*

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "Kotlin Coroutine"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val scope = CoroutineScope(Job() + Dispatchers.Default)

        scope.launch(CoroutineExceptionHandler { coroutineContext, throwable ->
            Log.e(TAG, "CoroutineExceptionHandler: $throwable")
        }) {
            supervisorScope {
                launch {
                    delay(500)
                    Log.e(TAG, "Child 1 ")
                }
                launch {
                    delay(1000)
                    Log.e(TAG, "Child 2 ")
                    throw  RuntimeException("--> RuntimeException <--")
                }
                launch {
                    delay(1500)
                    Log.e(TAG, "Child 3 ")
                }
            }
        }
    }
}

輸出結(jié)果:
E/Kotlin Coroutine: Child 1 
E/Kotlin Coroutine: Child 2 
E/Kotlin Coroutine: CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <--
E/Kotlin Coroutine: Child 3 

可以看到已經(jīng)達(dá)到了我們想要的效果帮哈,但是如果將 supervisorScope 換成 coroutineScope膛檀,結(jié)果就不是這樣了。

在后臺線程中執(zhí)行

如果在主線程上發(fā)出網(wǎng)絡(luò)請求娘侍,則主線程會處于等待或阻塞狀態(tài)咖刃,直到收到響應(yīng)。由于線程處于阻塞狀態(tài)憾筏,因此操作系統(tǒng)無法調(diào)用 onDraw()嚎杨,這會導(dǎo)致應(yīng)用凍結(jié),并有可能導(dǎo)致彈出“應(yīng)用無響應(yīng)”(ANR) 對話框氧腰。為了提供更好的用戶體驗(yàn)枫浙,我們在后臺線程上執(zhí)行此操作。

首先古拴,我們來了解一下 Repository 類箩帚,看看它是如何發(fā)出網(wǎng)絡(luò)請求的:

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

makeLoginRequest 是同步的,并且會阻塞發(fā)起調(diào)用的線程斤富。為了對網(wǎng)絡(luò)請求的響應(yīng)建模,我們創(chuàng)建了自己的 Result 類锻狗。

ViewModel 會在用戶點(diǎn)擊(例如满力,點(diǎn)擊按鈕)時觸發(fā)網(wǎng)絡(luò)請求:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

使用上述代碼,LoginViewModel 會在網(wǎng)絡(luò)請求發(fā)出時阻塞界面線程轻纪。如需將執(zhí)行操作移出主線程油额,最簡單的方法是創(chuàng)建一個新的協(xié)程,然后在 I/O 線程上執(zhí)行網(wǎng)絡(luò)請求:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

下面我們仔細(xì)分析一下 login 函數(shù)中的協(xié)程代碼:

viewModelScope 是預(yù)定義的 CoroutineScope刻帚,包含在 ViewModel KTX 擴(kuò)展中潦嘶。請注意,所有協(xié)程都必須在一個作用域內(nèi)運(yùn)行崇众。一個 CoroutineScope 管理一個或多個相關(guān)的協(xié)程掂僵。
launch 是一個函數(shù)航厚,用于創(chuàng)建協(xié)程并將其函數(shù)主體的執(zhí)行分派給相應(yīng)的調(diào)度程序。
Dispatchers.IO 指示此協(xié)程應(yīng)在為 I/O 操作預(yù)留的線程上執(zhí)行锰蓬。
login 函數(shù)按以下方式執(zhí)行:

應(yīng)用從主線程上的 View 層調(diào)用 login 函數(shù)幔睬。
launch 會創(chuàng)建一個新的協(xié)程,并且網(wǎng)絡(luò)請求在為 I/O 操作預(yù)留的線程上獨(dú)立發(fā)出芹扭。
在該協(xié)程運(yùn)行時麻顶,login 函數(shù)會繼續(xù)執(zhí)行,并可能在網(wǎng)絡(luò)請求完成前返回舱卡。請注意辅肾,為簡單起見,我們暫時忽略掉網(wǎng)絡(luò)響應(yīng)轮锥。
由于此協(xié)程通過 viewModelScope 啟動矫钓,因此在 ViewModel 的作用域內(nèi)執(zhí)行。如果 ViewModel 因用戶離開屏幕而被銷毀交胚,則 viewModelScope 會自動取消份汗,且所有運(yùn)行的協(xié)程也會被取消。

前面的示例存在的一個問題是蝴簇,調(diào)用 makeLoginRequest 的任何項(xiàng)都需要記得將執(zhí)行操作顯式移出主線程杯活。下面我們來看看如何修改 Repository 以解決這一問題。

使用協(xié)程確保主線程安全

如果函數(shù)不會在主線程上阻止界面更新熬词,我們即將其視為是主線程安全的旁钧。makeLoginRequest 函數(shù)不是主線程安全的,因?yàn)閺闹骶€程調(diào)用 makeLoginRequest 確實(shí)會阻塞界面互拾⊥峤瘢可以使用協(xié)程庫中的 withContext() 函數(shù)將協(xié)程的執(zhí)行操作移至其他線程:

class LoginRepository(...) {
    ...
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {

        // 將協(xié)程的執(zhí)行移至 I/O 調(diào)度器
        return withContext(Dispatchers.IO) {
            // 阻止網(wǎng)絡(luò)請求代碼
        }
    }
}

withContext(Dispatchers.IO) 將協(xié)程的執(zhí)行操作移至一個 I/O 線程,這樣一來颜矿,我們的調(diào)用函數(shù)便是主線程安全的寄猩,并且支持根據(jù)需要更新界面。

makeLoginRequest 還會用 suspend 關(guān)鍵字進(jìn)行標(biāo)記骑疆。Kotlin 利用此關(guān)鍵字強(qiáng)制從協(xié)程內(nèi)調(diào)用函數(shù)田篇。

注意:為更輕松地進(jìn)行測試,我們建議將 Dispatchers 注入 Repository 層箍铭。如需了解詳情泊柬,請參閱在 Android 上測試協(xié)程

在以下示例中诈火,協(xié)程是在 LoginViewModel 中創(chuàng)建的兽赁。由于 makeLoginRequest 將執(zhí)行操作移出主線程,login 函數(shù)中的協(xié)程現(xiàn)在可以在主線程中執(zhí)行:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {

        // Create a new coroutine on the UI thread
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // Make the network call and suspend execution until it finishes
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

請注意,此處仍需要協(xié)程刀崖,因?yàn)?makeLoginRequest 是一個 suspend 函數(shù)惊科,而所有 suspend 函數(shù)都必須在協(xié)程中執(zhí)行。

此代碼與前面的 login 示例的不同之處體現(xiàn)在以下幾個方面:

  • launch 不接受 Dispatchers.IO 參數(shù)蒲跨。如果您未將 Dispatcher 傳遞至 launch译断,則從 viewModelScope 啟動的所有協(xié)程都會在主線程中運(yùn)行。
  • 系統(tǒng)現(xiàn)在會處理網(wǎng)絡(luò)請求的結(jié)果或悲,以顯示成功或失敗界面孙咪。

login 函數(shù)現(xiàn)在按以下方式執(zhí)行:

  • 應(yīng)用從主線程上的 View 層調(diào)用 login() 函數(shù)。
  • launch 創(chuàng)建一個新的協(xié)程巡语,以在主線程上發(fā)出網(wǎng)絡(luò)請求翎蹈,然后該協(xié)程開始執(zhí)行。
  • 在協(xié)程內(nèi)男公,調(diào)用 loginRepository.makeLoginRequest() 現(xiàn)在會掛起協(xié)程的進(jìn)一步執(zhí)行操作荤堪,直至 makeLoginRequest() 中的 withContext 塊結(jié)束運(yùn)行。
  • withContext 塊結(jié)束運(yùn)行后枢赔,login() 中的協(xié)程在主線程上恢復(fù)執(zhí)行操作澄阳,并返回網(wǎng)絡(luò)請求的結(jié)果。

注意:如需與 ViewModel 層中的 View 通信踏拜,請按照應(yīng)用架構(gòu)指南中的建議碎赢,使用 LiveData。遵循此模式時速梗,ViewModel 中的代碼會在主線程上執(zhí)行肮塞,因此您可以直接調(diào)用 MutableLiveDatasetValue() 函數(shù)。

處理異常

為了處理 Repository 層可能拋出的異常姻锁,請使用 Kotlin 對異常的內(nèi)置支持枕赵。在以下示例中,我們使用的是 try-catch 塊:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun makeLoginRequest(username: String, token: String) {
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            val result = try {
                loginRepository.makeLoginRequest(jsonBody)
            } catch(e: Exception) {
                Result.Error(Exception("Network request failed"))
            }
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

在此示例中位隶,makeLoginRequest() 調(diào)用拋出的任何意外異常都會處理為界面錯誤拷窜。

將 Kotlin 協(xié)程與生命周期感知型組件一起使用

Kotlin 協(xié)程提供了一個可供您編寫異步代碼的 API。通過 Kotlin 協(xié)程涧黄,您可以定義 CoroutineScope篮昧,以幫助您管理何時應(yīng)運(yùn)行協(xié)程。每個異步操作都在特定范圍內(nèi)運(yùn)行弓熏。

生命周期感知型組件針對應(yīng)用中的邏輯范圍以及與 LiveData 的互操作層為協(xié)程提供了一流的支持恋谭。本文章會介紹如何有效地結(jié)合使用協(xié)程與生命周期感知型組件糠睡。

生命周期感知型協(xié)程范圍

命周期感知型組件定義了以下內(nèi)置范圍供您在應(yīng)用中使用挽鞠。

ViewModelScope

為應(yīng)用中的每個 ViewModel 定義了 ViewModelScope。如果 ViewModel 已清除,則在此范圍內(nèi)啟動的協(xié)程都會自動取消信认。如果您具有僅在 ViewModel 處于活動狀態(tài)時才需要完成的工作材义,此時協(xié)程非常有用。例如嫁赏,如果要為布局計(jì)算某些數(shù)據(jù)其掂,則應(yīng)將工作范圍限定至 ViewModel,以便在 ViewModel 清除后潦蝇,系統(tǒng)會自動取消工作以避免消耗資源款熬。

您可以通過 ViewModelviewModelScope 屬性訪問 ViewModelCoroutineScope,如以下示例所示:

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

LifecycleScope

為每個 Lifecycle 對象定義了 LifecycleScope攘乒。在此范圍內(nèi)啟動的協(xié)程會在 Lifecycle 被銷毀時取消贤牛。您可以通過 lifecycle.coroutineScopelifecycleOwner.lifecycleScope 屬性訪問 LifecycleCoroutineScope

以下示例演示了如何使用 lifecycleOwner.lifecycleScope 異步創(chuàng)建預(yù)計(jì)算文本:

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params)
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

可重啟生命周期感知型協(xié)程

即使 lifecycleScope 提供了適當(dāng)?shù)姆椒ㄒ栽?Lifecycle 處于 DESTROYED 狀態(tài)時自動取消長時間運(yùn)行的操作则酝,但在某些情況下殉簸,您可能需要在 Lifecycle 處于某個特定狀態(tài)時開始執(zhí)行代碼塊,并在其處于其他狀態(tài)時取消沽讹。例如般卑,您可能希望在 Lifecycle 處于 STARTED 狀態(tài)時收集數(shù)據(jù)流,并在其處于 STOPPED 狀態(tài)時取消收集爽雄。此方法僅在界面顯示在屏幕上時才處理數(shù)據(jù)流發(fā)出操作蝠检,這樣可節(jié)省資源并可能會避免發(fā)生應(yīng)用崩潰問題。

對于這些情況盲链,LifecycleLifecycleOwner 提供了掛起 repeatOnLifecycle API 來確切實(shí)現(xiàn)相應(yīng)操作蝇率。以下示例中的代碼塊會在每次關(guān)聯(lián)的 Lifecycle 至少處于 STARTED 狀態(tài)時運(yùn)行,并且會在 Lifecycle 處于 STOPPED 狀態(tài)時取消運(yùn)行:

class MyFragment : Fragment() {

    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // 在生命周期范圍內(nèi)創(chuàng)建一個新的協(xié)程
        viewLifecycleOwner.lifecycleScope.launch {
            // 每次生命周期處于 STARTED 狀態(tài)(或更高)時刽沾,
            // repeatOnLifecycle 在新的協(xié)程中啟動塊本慕,并在它停止時取消它。
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // 觸發(fā)流程并開始監(jiān)聽值侧漓。
                // 當(dāng)生命周期開始時會發(fā)生這種情況锅尘,當(dāng)生命周期停止時會停止收集
                viewModel.someDataFlow.collect {
                    // Process item
                }
            }
        }
    }
}

生命周期感知型數(shù)據(jù)流收集

如果你只需要對單個數(shù)據(jù)流執(zhí)行生命周期感知型收集,可以使用 Flow.flowWithLifecycle() 方法簡化代碼:

viewLifecycleOwner.lifecycleScope.launch {
    exampleProvider.exampleFlow()
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .collect {
            // 處理值
        }
}

但是布蔗,如果你需要并行對多個數(shù)據(jù)流執(zhí)行生命周期感知型收集藤违,則必須在不同的協(xié)程中收集每個數(shù)據(jù)流。在這種情況下纵揍,直接使用 repeatOnLifecycle() 會更加高效:

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // 因?yàn)?collect 是一個掛起函數(shù)顿乒,所以如果要并行收集多個流,則需要在不同的協(xié)程中進(jìn)行泽谨。
        launch {
            flow1.collect {
                // 處理值
            }
        }

        launch {
            flow2.collect {
                // 處理值
            }
        }
    }
}

掛起生命周期感知型協(xié)程
即使 CoroutineScope 提供了適當(dāng)?shù)姆椒▉碜詣尤∠L時間運(yùn)行的操作璧榄,在某些情況下特漩,你可能需要暫停執(zhí)行代碼塊(除非 Lifecycle 處于特定狀態(tài))。例如骨杂,如需運(yùn)行 FragmentTransaction涂身,您必須等到 Lifecycle 至少為 STARTED。對于這些情況搓蚪,Lifecycle 提供了其他方法:lifecycle.whenCreated蛤售、lifecycle.whenStartedlifecycle.whenResumed。如果 Lifecycle 未至少處于所需的最低狀態(tài)妒潭,則會掛起在這些塊內(nèi)運(yùn)行的任何協(xié)程悴能。

以下示例包含僅當(dāng)關(guān)聯(lián)的 Lifecycle 至少處于 STARTED 狀態(tài)時才會運(yùn)行的代碼塊:

class MyFragment: Fragment {
    init { // 請注意,我們可以在 Fragment 的構(gòu)造函數(shù)中安全地啟動雳灾。
        lifecycleScope.launch {
            whenStarted {
                // 只有在 Lifecycle 至少 STARTED 時搜骡,內(nèi)部的塊才會運(yùn)行。
                // 它將在片段啟動時開始執(zhí)行佑女,并且可以調(diào)用其他掛起方法记靡。
                loadingView.visibility = View.VISIBLE
                val canAccess = withContext(Dispatchers.IO) {
                    checkUserAccess()
                }

                // 當(dāng) checkUserAccess 返回時,如果生命周期沒有至少 STARTED团驱,則下一行將自動掛起摸吠。             
                // 我們可以安全地運(yùn)行片段事務(wù)误算,因?yàn)槲覀冎莱巧芷谥辽僖验_始胸墙,否則代碼不會運(yùn)行。
                loadingView.visibility = View.GONE
                if (canAccess == false) {
                    findNavController().popBackStack()
                } else {
                    showContent()
                }
            }

            // 此行僅在上面的 whenStarted 塊完成后運(yùn)行印衔。

        }
    }
}

如果在協(xié)程處于活動狀態(tài)時通過某種 when 方法銷毀了 Lifecycle紊选,協(xié)程會自動取消啼止。在以下示例中,一旦 Lifecycle 狀態(tài)變?yōu)?DESTROYED兵罢,finally 塊即會運(yùn)行:

class MyFragment: Fragment {
    init {
        lifecycleScope.launchWhenStarted {
            try {
                // 調(diào)用一些掛起函數(shù)献烦。
            } finally {
                // 此行可能會在 Lifecycle 被 DESTROYED 后執(zhí)行。
                if (lifecycle.state >= STARTED) {
                    // 在這里卖词,由于我們已經(jīng)檢查過巩那,運(yùn)行任何 Fragment 事務(wù)都是安全的。
                }
            }
        }
    }
}

注意:盡管這些方法為使用 Lifecycle 提供了便利此蜈,但只有當(dāng)信息在 Lifecycle 的范圍(例如預(yù)計(jì)算文本)內(nèi)有效時才應(yīng)使用它們即横。請注意,協(xié)程不會隨著 activity 重啟而重啟裆赵。
警告:傾向于使用 repeatOnLifecycle API 收集數(shù)據(jù)流东囚,而不是在 launchWhenX API 內(nèi)部進(jìn)行收集。由于后面的 API 會掛起協(xié)程战授,而不是在 Lifecycle 處于 STOPPED 狀態(tài)時取消页藻。上游數(shù)據(jù)流會在后臺保持活躍狀態(tài)抛蚁,并可能會發(fā)出新的項(xiàng)并耗用資源。

將協(xié)程與 LiveData 一起使用

使用 LiveData 時惕橙,您可能需要異步計(jì)算值。例如钉跷,您可能需要檢索用戶的偏好設(shè)置并將其傳送給界面弥鹦。在這些情況下,您可以使用 liveData 構(gòu)建器函數(shù)調(diào)用 suspend 函數(shù)爷辙,并將結(jié)果作為 LiveData 對象傳送彬坏。

在以下示例中,loadUser() 是在其他位置聲明的掛起函數(shù)膝晾。使用 liveData 構(gòu)建器函數(shù)異步調(diào)用 loadUser()栓始,然后使用 emit() 發(fā)出結(jié)果:

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

liveData 構(gòu)建塊用作協(xié)程和 LiveData 之間的結(jié)構(gòu)化并發(fā)基元。當(dāng) LiveData 變?yōu)榛顒訝顟B(tài)時血当,代碼塊開始執(zhí)行幻赚;當(dāng) LiveData 變?yōu)榉腔顒訝顟B(tài)時,代碼塊會在可配置的超時過后自動取消臊旭。如果代碼塊在完成前取消落恼,則會在 LiveData 再次變?yōu)榛顒訝顟B(tài)后重啟;如果在上次運(yùn)行中成功完成离熏,則不會重啟佳谦。請注意,代碼塊只有在自動取消的情況下才會重啟滋戳。如果代碼塊由于任何其他原因(例如钻蔑,拋出 CancellationException)而取消,則不會重啟奸鸯。

你還可以從代碼塊中發(fā)出多個值咪笑。每次 emit() 調(diào)用都會掛起代碼塊的執(zhí)行,直到在主線程上設(shè)置 LiveData 值娄涩。

val user: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(fetchUser()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

您也可以將 liveDataTransformations 結(jié)合使用蒲肋,如以下示例所示:

class MyViewModel: ViewModel() {
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(database.loadUserById(id))
        }
    }
}

您可以從 LiveData 中發(fā)出多個值,方法是在每次想要發(fā)出新值時調(diào)用 emitSource() 函數(shù)钝满。請注意兜粘,每次調(diào)用 emit()emitSource() 都會移除之前添加的來源。

class UserDao: Dao {
    @Query("SELECT * FROM User WHERE id = :id")
    fun getUser(id: String): LiveData<User>
}

class MyRepository {
    fun getUser(id: String) = liveData<User> {
        val disposable = emitSource(
            userDao.getUser(id).map {
                Result.loading(it)
            }
        )
        try {
            val user = webservice.fetchUser(id)
            // 停止先前的發(fā)射以避免將更新的用戶作為“加載”調(diào)度弯蚜。
            disposable.dispose()
            // 更新數(shù)據(jù)庫孔轴。
            userDao.insert(user)
            // 使用成功類型重新建立發(fā)射路鹰。
            emitSource(
                userDao.getUser(id).map {
                    Result.success(it)
                }
            )
        } catch(exception: IOException) {
            // 任何對 `emit` 的調(diào)用都會自動釋放前一個,因此我們不需要在此處釋放它晋柱,
            // 因?yàn)槲覀儧]有獲得更新的值。
            emitSource(
                userDao.getUser(id).map {
                    Result.error(exception, it)
                }
            )
        }
    }
}

相關(guān)官方文檔钦椭、文章鏈接:
https://kotlinlang.org/docs/coroutines-guide.html
https://developer.android.com/kotlin/coroutines
https://juejin.cn/post/6950616789390721037

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末碑诉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子德挣,更是在濱河造成了極大的恐慌快毛,老刑警劉巖格嗅,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異唠帝,居然都是意外死亡吗浩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門没隘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來懂扼,“玉大人,你說我怎么就攤上這事右蒲》” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵瑰妄,是天一觀的道長陷嘴。 經(jīng)常有香客問我,道長间坐,這世上最難降的妖魔是什么灾挨? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮竹宋,結(jié)果婚禮上劳澄,老公的妹妹穿的比我還像新娘。我一直安慰自己蜈七,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布作谚。 她就那樣靜靜地躺著妹懒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上曹宴,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天笛坦,我揣著相機(jī)與錄音,去河邊找鬼礁芦。 笑死柿扣,一個胖子當(dāng)著我的面吹牛未状,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播埋虹,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼辣辫!你這毒婦竟也來了急灭?” 一聲冷哼從身側(cè)響起葬馋,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤蛋逾,失蹤者是張志新(化名)和其女友劉穎区匣,沒想到半個月后亏钩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡栅哀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了间驮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片竞帽。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖堆巧,靈堂內(nèi)的尸體忽然破棺而出啦租,到底是詐尸還是另有隱情篷角,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布嘉蕾,位于F島的核電站,受9級特大地震影響颁糟,放射性物質(zhì)發(fā)生泄漏棱貌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一障贸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧殃姓,春花似錦、人聲如沸篷牌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坐求。三九已至桥嗤,卻和暖如春泛领,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背锡宋。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留役首,地道東北人衡奥。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像档址,于是被迫代替她去往敵國和親辰晕。 傳聞我的和親對象是個殘疾皇子含友,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容