協(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
津肛。 launch
和 async
之間的很大差異是它們對異常的處理方式不同。如果使用 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
有兩個非常重要的元素 — Job
和 Dispatcher
,Job
是當(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
; - 繼承的
CoroutineContext
是CoroutineScope
或者其父協(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
的屬性: isActive
、isCancelled
和 isCompleted
韭寸。
如果協(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.join
的Coroutine
還繼續(xù)處于activie
狀態(tài)。
請注意闽撤,只有在其所有子級都完成后得哆,作業(yè)才能完成。
該函數(shù)的掛起是可以被取消的哟旗,并且始終檢查調(diào)用的Coroutine
的Job
是否取消贩据。如果在調(diào)用此掛起函數(shù)或?qū)⑵鋻炱饡r栋操,調(diào)用Coroutine
的Job
被取消或完成,則此函數(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
會繼承 父Coroutine
的 context
,所以為了方便使用疗我,我們一般會在 父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í)踐中通常使用
DEFAULT
和LAZY
這兩個啟動模式就夠了碌嘀。
CoroutineScope - 協(xié)程作用域
定義協(xié)程必須指定其 CoroutineScope
。CoroutineScope
可以對協(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)前 Scope
的 coroutineContext
偿凭。
分類及行為規(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ò)展屬性是 Android
的 Lifecycle Ktx
庫提供的具有生命周期感知的協(xié)程作用域,它與 LifecycleOwner
的 Lifecycle
綁定琼了,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ò)展屬性燥爷,也是來自 Android
的 Lifecycle 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ù)扇单。coroutineScope
和 supervisorScope
都會返回一個作用域,它倆的差別就是異常傳播: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)用 MutableLiveData
的 setValue()
函數(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)會自動取消工作以避免消耗資源款熬。
您可以通過 ViewModel
的 viewModelScope
屬性訪問 ViewModel
的 CoroutineScope
,如以下示例所示:
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
LifecycleScope
為每個 Lifecycle
對象定義了 LifecycleScope
攘乒。在此范圍內(nèi)啟動的協(xié)程會在 Lifecycle
被銷毀時取消贤牛。您可以通過 lifecycle.coroutineScope
或 lifecycleOwner.lifecycleScope
屬性訪問 Lifecycle
的 CoroutineScope
。
以下示例演示了如何使用 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)用崩潰問題。
對于這些情況盲链,Lifecycle
和 LifecycleOwner
提供了掛起 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.whenStarted
和 lifecycle.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))
}
}
您也可以將 liveData
與 Transformations
結(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