在今年的三月份小压,我因?yàn)樾枰獮轫?xiàng)目搭建一個新的網(wǎng)絡(luò)請求框架開始接觸 Kotlin 協(xié)程壤靶。那時我司項(xiàng)目中同時存在著兩種網(wǎng)絡(luò)請求方式缚俏,采用的技術(shù)棧各不相同,Java贮乳、Kotlin忧换、RxJava、LiveData 各種混搭向拆,技術(shù)棧的不統(tǒng)一長遠(yuǎn)來看肯定是會造成很多不便的亚茬,所以當(dāng)時就打算封裝一個新的網(wǎng)絡(luò)請求框架來作為項(xiàng)目的統(tǒng)一規(guī)范(前面的人估計也是這么想的,所以就造成了同個項(xiàng)目中的網(wǎng)絡(luò)請求方式越來越多????)浓恳,那么就需要考慮采用什么技術(shù)棧來實(shí)現(xiàn)了
采用 Kotlin 語言來實(shí)現(xiàn)必不可少才写,都這年頭了還用 Java 也說不過去。Retrofit 也必不可少奖蔓,而當(dāng)時 Retrofit 也已經(jīng)原生支持 Kotlin 協(xié)程了赞草,Google 官方推出的 Jetpack 協(xié)程擴(kuò)展庫也越來越多,就最終決定棄用 RxJava 擁抱 Kotlin 協(xié)程吆鹤,將協(xié)程作為技術(shù)棧之一
當(dāng)時我是通過翻譯協(xié)程官方文檔來作為入門手段厨疙,到現(xiàn)在也大半年了,回頭來看感覺官方文檔還是挺晦澀難懂的疑务,就想著再來寫一兩篇入門或進(jìn)階的文章來加深下理解沾凄,希望對你有所幫助
附上我當(dāng)時翻譯的協(xié)程官方文檔:
- Kotlin 協(xié)程官方文檔(1)-協(xié)程基礎(chǔ)(Coroutine Basics)
- Kotlin 協(xié)程官方文檔(2)-取消和超時(Cancellation and Timeouts)
- Kotlin 協(xié)程官方文檔(3)-組合掛起函數(shù)(Coroutine Context and Dispatchers)
- Kotlin 協(xié)程官方文檔(4)-協(xié)程上下文和調(diào)度器(Coroutine Context and Dispatchers)
- Kotlin 協(xié)程官方文檔(5)-異步流(Asynchronous Flow)
- Kotlin 協(xié)程官方文檔(6)-通道(Channels)
- Kotlin 協(xié)程官方文檔(7)-異常處理(Exception Handling)
- Kotlin 協(xié)程官方文檔(8)-共享可變狀態(tài)和并發(fā)性(Shared mutable state and concurrency)
- Kotlin 協(xié)程官方文檔(9)-選擇表達(dá)式(實(shí)驗(yàn)階段)(Select Expression (experimental)
一梗醇、Kotlin 協(xié)程
Kotlin 協(xié)程提供了一種全新處理并發(fā)的方式,你可以在 Android 平臺上使用它來簡化異步執(zhí)行的代碼撒蟀。協(xié)程從 Kotlin 1.3 版本開始引入叙谨,但這一概念在編程世界誕生的黎明之際就有了,最早使用協(xié)程的編程語言可以追溯到 1967 年的 Simula 語言保屯。在過去幾年間手负,協(xié)程這個概念發(fā)展勢頭迅猛,現(xiàn)已經(jīng)被諸多主流編程語言采用姑尺,比如 Javascript竟终、C#、Python切蟋、Ruby 以及 Go 等统捶。Kotlin 協(xié)程是基于來自其他語言的既定概念
Goggle 官方推薦將 Kotlin 協(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í)行多個操作
- 內(nèi)置取消支持:取消功能會自動通過正在運(yùn)行的協(xié)程層次結(jié)構(gòu)傳播
- Jetpack 集成:許多 Jetpack 庫都包含提供全面協(xié)程支持的擴(kuò)展什黑。某些庫還提供自己的協(xié)程作用域,可供你用于結(jié)構(gòu)化并發(fā)
引入依賴:
implementation 'org.jetbrains.Kotlinx:Kotlinx-coroutines-core:1.4.2'
implementation 'org.jetbrains.Kotlinx:Kotlinx-coroutines-android:1.4.2'
復(fù)制代碼
二旺入、第一個協(xié)程
協(xié)程可以稱為輕量級線程兑凿。Kotlin 協(xié)程在 CoroutineScope 的上下文中通過 launch、async 等協(xié)程構(gòu)造器(CoroutineBuilder)來聲明并啟動
fun main() {
GlobalScope.launch(context = Dispatchers.IO) {
//延時一秒
delay(1000)
log("launch")
}
//主動休眠兩秒茵瘾,防止JVM過快退出
Thread.sleep(2000)
log("end")
}
private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg")
復(fù)制代碼
[DefaultDispatcher-worker-1 @coroutine#1] launch
[main] end
復(fù)制代碼
在上面的例子中礼华,通過 GlobalScope(即全局作用域)啟動了一個協(xié)程,在延遲一秒后輸出一行日志拗秘。從輸出結(jié)果可以看出來圣絮,啟動的協(xié)程是運(yùn)行在協(xié)程內(nèi)部的線程池中。雖然從表現(xiàn)結(jié)果上來看雕旨,啟動一個協(xié)程類似于我們直接使用 Thread 來執(zhí)行耗時任務(wù)扮匠,但實(shí)際上協(xié)程和線程有著本質(zhì)上的區(qū)別。通過使用協(xié)程凡涩,可以極大的提高線程的并發(fā)效率棒搜,避免以往的嵌套回調(diào)地獄,極大提高了代碼的可讀性
以上代碼就涉及到了協(xié)程的四個基礎(chǔ)概念:
- suspend function活箕。即掛起函數(shù)力麸,delay 函數(shù)就是協(xié)程庫提供的一個用于實(shí)現(xiàn)非阻塞式延時的掛起函數(shù)
- CoroutineScope。即協(xié)程作用域,GlobalScope 是 CoroutineScope 的一個實(shí)現(xiàn)類克蚂,用于指定協(xié)程的作用范圍闺鲸,可用于管理多個協(xié)程的生命周期,所有協(xié)程都需要通過 CoroutineScope 來啟動
- CoroutineContext埃叭。即協(xié)程上下文摸恍,包含多種類型的配置參數(shù)。Dispatchers.IO 就是 CoroutineContext 這個抽象概念的一種實(shí)現(xiàn)赤屋,用于指定協(xié)程的運(yùn)行載體立镶,即用于指定協(xié)程要運(yùn)行在哪類線程上
- CoroutineBuilder。即協(xié)程構(gòu)建器益缎,協(xié)程在 CoroutineScope 的上下文中通過 launch谜慌、async 等協(xié)程構(gòu)建器來進(jìn)行聲明并啟動然想。launch莺奔、async 等均被聲明 CoroutineScope 的擴(kuò)展方法
三、suspend function
如果上述例子試圖直接在 GlobalScope 外調(diào)用 delay()
函數(shù)的話变泄,IDE 就會提示一個錯誤:Suspend function 'delay' should be called only from a coroutine or another suspend function令哟。意思是:delay()
函數(shù)是一個掛起函數(shù),只能由協(xié)程或者由其它掛起函數(shù)來調(diào)用
delay()
函數(shù)就使用了 suspend 進(jìn)行修飾论寨,用 suspend 修飾的函數(shù)就是掛起函數(shù)
public suspend fun delay(timeMillis: Long)
復(fù)制代碼
讀者在網(wǎng)上看關(guān)于協(xié)程的文章的時候集币,應(yīng)該經(jīng)常會看到這么一句話:掛起函數(shù)不會阻塞其所在線程发皿,而是會將協(xié)程掛起,在特定的時候才再恢復(fù)協(xié)程
對于這句話我的理解是:delay()
函數(shù)類似于 Java 中的 Thread.sleep()
狠半,而之所以說 delay()
函數(shù)是非阻塞的,是因?yàn)樗蛦渭兊木€程休眠有著本質(zhì)的區(qū)別颤难。協(xié)程是運(yùn)行于線程上的神年,一個線程可以運(yùn)行多個(幾千上萬個)協(xié)程。線程的調(diào)度行為是由操作系統(tǒng)來管理的行嗤,而協(xié)程的調(diào)度行為是可以由開發(fā)者來指定并由編譯器來實(shí)現(xiàn)的已日,協(xié)程能夠細(xì)粒度地控制多個任務(wù)的執(zhí)行時機(jī)和執(zhí)行線程,當(dāng)某個特定的線程上的所有協(xié)程被 suspend 后栅屏,該線程便可騰出資源去處理其他任務(wù)
例如飘千,當(dāng)在 ThreadA 上運(yùn)行的 CoroutineA 調(diào)用了delay(1000L)
函數(shù)指定延遲一秒后再運(yùn)行,ThreadA 會轉(zhuǎn)而去執(zhí)行 CoroutineB栈雳,等到一秒后再來繼續(xù)執(zhí)行 CoroutineA护奈。所以,ThreadA 并不會因?yàn)?CoroutineA 的延時而阻塞哥纫,而是能繼續(xù)去執(zhí)行其它任務(wù)霉旗,所以掛起函數(shù)并不會阻塞其所在線程,這樣就極大地提高線程的并發(fā)靈活度,最大化線程的利用效率奖慌。而如果是使用Thread.sleep()
的話抛虫,線程就真的只是白白消耗 CPU 時間片而不會去執(zhí)行其它任務(wù)
四、suspend function 的掛起與恢復(fù)
協(xié)程在常規(guī)函數(shù)的基礎(chǔ)上添加了兩項(xiàng)操作用于處理長時間運(yùn)行的任務(wù)简僧。在invoke
(或 call
)和return
之外建椰,協(xié)程添加了suspend
和 resume
:
-
suspend
用于暫停執(zhí)行當(dāng)前協(xié)程,并保存所有局部變量 -
resume
用于讓已暫停的協(xié)程從暫停處繼續(xù)執(zhí)行
suspend 函數(shù)只能由其它 suspend 函數(shù)調(diào)用岛马,或者是由協(xié)程來調(diào)用
以下示例展示了一項(xiàng)任務(wù)(假設(shè) get 方法是一個網(wǎng)絡(luò)請求任務(wù))的簡單協(xié)程實(shí)現(xiàn):
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("https://developer.android.com") // Dispatchers.IO for `get`
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
復(fù)制代碼
在上面的示例中棉姐,get()
仍在主線程上被調(diào)用,但它會在啟動網(wǎng)絡(luò)請求之前暫停協(xié)程啦逆。get()
主體內(nèi)通過調(diào)用 withContext(Dispatchers.IO)
創(chuàng)建了一個在 IO 線程池中運(yùn)行的代碼塊伞矩,在該塊內(nèi)的任何代碼都始終通過 IO 調(diào)度器執(zhí)行。當(dāng)網(wǎng)絡(luò)請求完成后夏志,get()
會恢復(fù)已暫停的協(xié)程乃坤,使得主線程協(xié)程可以直接拿到網(wǎng)絡(luò)請求結(jié)果而不用使用回調(diào)來通知主線程。Retrofit 就是以這種方式來實(shí)現(xiàn)對協(xié)程的支持的
Kotlin 使用堆棧幀管理要運(yùn)行哪個函數(shù)以及所有局部變量沟蔑。暫停協(xié)程時湿诊,系統(tǒng)會復(fù)制并保存當(dāng)前的堆棧幀以供稍后使用∈莶模恢復(fù)時厅须,會將堆棧幀從其保存位置復(fù)制回來,然后函數(shù)再次開始運(yùn)行食棕。即使代碼可能看起來像普通的順序阻塞請求朗和,協(xié)程也能確保網(wǎng)絡(luò)請求避免阻塞主線程
在主線程進(jìn)行的暫停協(xié)程和恢復(fù)協(xié)程的兩個操作,既實(shí)現(xiàn)了將耗時任務(wù)交由后臺線程完成簿晓,保障了主線程安全眶拉,又以同步代碼的方式完成了實(shí)際上的多線程異步調(diào)用∏朗矗可以說镀层,在 Android 平臺上協(xié)程主要就用來解決兩個問題:
- 處理耗時任務(wù) (Long running tasks),這種任務(wù)常常會阻塞住主線程
- 保證主線程安全 (Main-safety) 皿曲,即確保安全地從主線程調(diào)用任何 suspend 函數(shù)
五唱逢、CoroutineScope
CoroutineScope 即協(xié)程作用域,用于對協(xié)程進(jìn)行追蹤屋休。如果我們啟動了多個協(xié)程但是沒有一個可以對其進(jìn)行統(tǒng)一管理的途徑的話坞古,那么就會導(dǎo)致我們的代碼臃腫雜亂,甚至發(fā)生內(nèi)存泄露或者任務(wù)泄露劫樟。為了確保所有的協(xié)程都會被追蹤痪枫,Kotlin 不允許在沒有使用 CoroutineScope 的情況下啟動新的協(xié)程织堂。CoroutineScope 可被看作是一個具有超能力的 ExecutorService 的輕量級版本。它能啟動新的協(xié)程奶陈,同時這個協(xié)程還具備上文所說的 suspend 和 resume 的優(yōu)勢
所有的協(xié)程都需要通過 CoroutineScope 來啟動易阳,它會跟蹤它使用 launch
或 async
創(chuàng)建的所有協(xié)程,你可以隨時調(diào)用 scope.cancel()
取消正在運(yùn)行的協(xié)程吃粒。CoroutineScope 本身并不運(yùn)行協(xié)程潦俺,它只是確保你不會失去對協(xié)程的追蹤,即使協(xié)程被掛起也是如此徐勃。在 Android 中事示,某些 KTX 庫為某些生命周期類提供了自己的 CoroutineScope
。例如僻肖,ViewModel
有 viewModelScope
肖爵,Lifecycle
有 lifecycleScope
CoroutineScope 大體上可以分為三種:
- GlobalScope。即全局協(xié)程作用域臀脏,在這個范圍內(nèi)啟動的協(xié)程可以一直運(yùn)行直到應(yīng)用停止運(yùn)行劝堪。GlobalScope 本身不會阻塞當(dāng)前線程,且啟動的協(xié)程相當(dāng)于守護(hù)線程谁榜,不會阻止 JVM 結(jié)束運(yùn)行
- runBlocking幅聘。一個頂層函數(shù)凡纳,和 GlobalScope 不一樣窃植,它會阻塞當(dāng)前線程直到其內(nèi)部所有相同作用域的協(xié)程執(zhí)行結(jié)束
- 自定義 CoroutineScope〖雒樱可用于實(shí)現(xiàn)主動控制協(xié)程的生命周期范圍巷怜,對于 Android 開發(fā)來說最大意義之一就是可以避免內(nèi)存泄露
1、GlobalScope
GlobalScope 屬于全局作用域暴氏,這意味著通過 GlobalScope 啟動的協(xié)程的生命周期只受整個應(yīng)用程序的生命周期的限制延塑,只要整個應(yīng)用程序還在運(yùn)行且協(xié)程的任務(wù)還未結(jié)束,協(xié)程就可以一直運(yùn)行
GlobalScope 不會阻塞其所在線程答渔,所以以下代碼中主線程的日志會早于 GlobalScope 內(nèi)部輸出日志关带。此外,GlobalScope 啟動的協(xié)程相當(dāng)于守護(hù)線程沼撕,不會阻止 JVM 結(jié)束運(yùn)行宋雏,所以如果將主線程的休眠時間改為三百毫秒的話,就不會看到 launch A 輸出日志
fun main() {
log("start")
GlobalScope.launch {
launch {
delay(400)
log("launch A")
}
launch {
delay(300)
log("launch B")
}
log("GlobalScope")
}
log("end")
Thread.sleep(500)
}
復(fù)制代碼
[main] start
[main] end
[DefaultDispatcher-worker-1 @coroutine#1] GlobalScope
[DefaultDispatcher-worker-3 @coroutine#3] launch B
[DefaultDispatcher-worker-3 @coroutine#2] launch A
復(fù)制代碼
GlobalScope.launch
會創(chuàng)建一個頂級協(xié)程务豺,盡管它很輕量級磨总,但在運(yùn)行時還是會消耗一些內(nèi)存資源,且可以一直運(yùn)行直到整個應(yīng)用程序停止(只要任務(wù)還未結(jié)束)笼沥,這可能會導(dǎo)致內(nèi)存泄露蚪燕,所以在日常開發(fā)中應(yīng)該謹(jǐn)慎使用 GlobalScope
2娶牌、runBlocking
也可以使用 runBlocking 這個頂層函數(shù)來啟動協(xié)程,runBlocking 函數(shù)的第二個參數(shù)即協(xié)程的執(zhí)行體馆纳,該參數(shù)被聲明為 CoroutineScope 的擴(kuò)展函數(shù)诗良,因此執(zhí)行體就包含了一個隱式的 CoroutineScope,所以在 runBlocking 內(nèi)部可以來直接啟動協(xié)程
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
復(fù)制代碼
runBlocking 的一個方便之處就是:只有當(dāng)內(nèi)部相同作用域的所有協(xié)程都運(yùn)行結(jié)束后鲁驶,聲明在 runBlocking 之后的代碼才能執(zhí)行累榜,即 runBlocking 會阻塞其所在線程
看以下代碼。runBlocking 內(nèi)部啟動的兩個協(xié)程會各自做耗時操作灵嫌,從輸出結(jié)果可以看出來兩個協(xié)程還是在交叉并發(fā)執(zhí)行壹罚,且 runBlocking 會等到兩個協(xié)程都執(zhí)行結(jié)束后才會退出,外部的日志輸出結(jié)果有明確的先后順序寿羞。即 runBlocking 內(nèi)部啟動的協(xié)程是非阻塞式的猖凛,但 runBlocking 阻塞了其所在線程。此外绪穆,runBlocking 只會等待相同作用域的協(xié)程完成才會退出辨泳,而不會等待 GlobalScope 等其它作用域啟動的協(xié)程
所以說,runBlocking 本身帶有阻塞線程的意味玖院,但其內(nèi)部運(yùn)行的協(xié)程又是非阻塞的菠红,讀者需要意會這兩者的區(qū)別
fun main() {
log("start")
runBlocking {
launch {
repeat(3) {
delay(100)
log("launchA - $it")
}
}
launch {
repeat(3) {
delay(100)
log("launchB - $it")
}
}
GlobalScope.launch {
repeat(3) {
delay(120)
log("GlobalScope - $it")
}
}
}
log("end")
}
復(fù)制代碼
[main] start
[main] launchA - 0
[main] launchB - 0
[DefaultDispatcher-worker-1] GlobalScope - 0
[main] launchA - 1
[main] launchB - 1
[DefaultDispatcher-worker-1] GlobalScope - 1
[main] launchA - 2
[main] launchB - 2
[main] end
復(fù)制代碼
基于是否會阻塞線程的區(qū)別,以下代碼中 runBlocking 會早于 GlobalScope 輸出日志
fun main() {
GlobalScope.launch(Dispatchers.IO) {
delay(600)
log("GlobalScope")
}
runBlocking {
delay(500)
log("runBlocking")
}
//主動休眠兩百毫秒难菌,使得和 runBlocking 加起來的延遲時間少于六百毫秒
Thread.sleep(200)
log("after sleep")
}
復(fù)制代碼
[main] runBlocking
[DefaultDispatcher-worker-1] GlobalScope
[main] after sleep
復(fù)制代碼
3试溯、coroutineScope
coroutineScope
函數(shù)用于創(chuàng)建一個獨(dú)立的協(xié)程作用域,直到所有啟動的協(xié)程都完成后才結(jié)束自身郊酒。runBlocking
和 coroutineScope
看起來很像遇绞,因?yàn)樗鼈兌夹枰却鋬?nèi)部所有相同作用域的協(xié)程結(jié)束后才會結(jié)束自己。兩者的主要區(qū)別在于 runBlocking
方法會阻塞當(dāng)前線程燎窘,而 coroutineScope
不會阻塞線程摹闽,而是會掛起并釋放底層線程以供其它協(xié)程使用。由于這個差別褐健,runBlocking
是一個普通函數(shù)付鹿,而 coroutineScope
是一個掛起函數(shù)
fun main() = runBlocking {
launch {
delay(100)
log("Task from runBlocking")
}
coroutineScope {
launch {
delay(500)
log("Task from nested launch")
}
delay(100)
log("Task from coroutine scope")
}
log("Coroutine scope is over")
}
復(fù)制代碼
[main] Task from coroutine scope
[main] Task from runBlocking
[main] Task from nested launch
[main] Coroutine scope is over
復(fù)制代碼
4、supervisorScope
supervisorScope
函數(shù)用于創(chuàng)建一個使用了 SupervisorJob 的 coroutineScope蚜迅,該作用域的特點(diǎn)就是拋出的異常不會連鎖取消同級協(xié)程和父協(xié)程
fun main() = runBlocking {
launch {
delay(100)
log("Task from runBlocking")
}
supervisorScope {
launch {
delay(500)
log("Task throw Exception")
throw Exception("failed")
}
launch {
delay(600)
log("Task from nested launch")
}
}
log("Coroutine scope is over")
}
復(fù)制代碼
[main @coroutine#2] Task from runBlocking
[main @coroutine#3] Task throw Exception
[main @coroutine#4] Task from nested launch
[main @coroutine#1] Coroutine scope is over
復(fù)制代碼
5舵匾、自定義 CoroutineScope
假設(shè)我們在 Activity 中先后啟動了多個協(xié)程用于執(zhí)行異步耗時操作,那么當(dāng) Activity 退出時慢叨,必須取消所有協(xié)程以避免內(nèi)存泄漏纽匙。我們可以通過保留每一個 Job 引用然后在 onDestroy
方法里來手動取消,但這種方式相當(dāng)來說會比較繁瑣和低效拍谐。kotlinx.coroutines 提供了 CoroutineScope 來管理多個協(xié)程的生命周期
我們可以通過創(chuàng)建與 Activity 生命周期相關(guān)聯(lián)的協(xié)程作用域的實(shí)例來管理協(xié)程的生命周期烛缔。CoroutineScope 的實(shí)例可以通過 CoroutineScope()
或 MainScope()
的工廠函數(shù)來構(gòu)建馏段。前者創(chuàng)建通用作用域,后者創(chuàng)建 UI 應(yīng)用程序的作用域并使用 Dispatchers.Main 作為默認(rèn)的調(diào)度器
class Activity {
private val mainScope = MainScope()
fun onCreate() {
mainScope.launch {
repeat(5) {
delay(1000L * it)
}
}
}
fun onDestroy() {
mainScope.cancel()
}
}
復(fù)制代碼
或者践瓷,我們可以通過委托模式來讓 Activity 實(shí)現(xiàn) CoroutineScope 接口院喜,從而可以在 Activity 內(nèi)直接啟動協(xié)程而不必顯示地指定它們的上下文,并且在 onDestroy()
中自動取消所有協(xié)程
class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {
fun onCreate() {
launch {
repeat(5) {
delay(200L * it)
log(it)
}
}
log("Activity Created")
}
fun onDestroy() {
cancel()
log("Activity Destroyed")
}
}
復(fù)制代碼
從輸出結(jié)果可以看出晕翠,當(dāng)回調(diào)了onDestroy()
方法后協(xié)程就不會再輸出日志了
fun main() = runBlocking {
val activity = Activity()
activity.onCreate()
delay(1000)
activity.onDestroy()
delay(1000)
}
復(fù)制代碼
[main @coroutine#1] Activity Created
[DefaultDispatcher-worker-1 @coroutine#2] 0
[DefaultDispatcher-worker-1 @coroutine#2] 1
[DefaultDispatcher-worker-1 @coroutine#2] 2
[main @coroutine#1] Activity Destroyed
復(fù)制代碼
已取消的作用域無法再創(chuàng)建協(xié)程喷舀。因此,僅當(dāng)控制其生命周期的類被銷毀時淋肾,才應(yīng)調(diào)用 scope.cancel()
硫麻。例如,使用 viewModelScope
時樊卓,ViewModel
類會在 ViewModel 的 onCleared()
方法中自動取消作用域
六拿愧、CoroutineBuilder
1、launch
看下 launch
函數(shù)的方法簽名碌尔。launch
是一個作用于 CoroutineScope 的擴(kuò)展函數(shù)浇辜,用于在不阻塞當(dāng)前線程的情況下啟動一個協(xié)程,并返回對該協(xié)程任務(wù)的引用唾戚,即 Job 對象
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
復(fù)制代碼
launch
函數(shù)共包含三個參數(shù):
- context柳洋。用于指定協(xié)程的上下文
- start。用于指定協(xié)程的啟動方式叹坦。默認(rèn)值為
CoroutineStart.DEFAULT
熊镣,即協(xié)程會在聲明的同時就立即進(jìn)入等待調(diào)度的狀態(tài),即可以立即執(zhí)行的狀態(tài)立由≡觯可以通過將其設(shè)置為CoroutineStart.LAZY
來實(shí)現(xiàn)延遲啟動,即懶加載 - block锐膜。用于傳遞協(xié)程的執(zhí)行體,即希望交由協(xié)程執(zhí)行的任務(wù)
可以看到 launchA 和 launchB 是并行交叉執(zhí)行的
fun main() = runBlocking {
val launchA = launch {
repeat(3) {
delay(100)
log("launchA - $it")
}
}
val launchB = launch {
repeat(3) {
delay(100)
log("launchB - $it")
}
}
}
復(fù)制代碼
[main] launchA - 0
[main] launchB - 0
[main] launchA - 1
[main] launchB - 1
[main] launchA - 2
[main] launchB - 2
復(fù)制代碼
2弛房、Job
Job 是協(xié)程的句柄道盏。使用 launch
或 async
創(chuàng)建的每個協(xié)程都會返回一個 Job
實(shí)例,該實(shí)例唯一標(biāo)識協(xié)程并管理其生命周期文捶。Job 是一個接口類型荷逞,這里列舉 Job 幾個比較有用的屬性和函數(shù)
//當(dāng) Job 處于活動狀態(tài)時為 true
//如果 Job 未被取消或沒有失敗,則均處于 active 狀態(tài)
public val isActive: Boolean
//當(dāng) Job 正常結(jié)束或者由于異常結(jié)束粹排,均返回 true
public val isCompleted: Boolean
//當(dāng) Job 被主動取消或者由于異常結(jié)束种远,均返回 true
public val isCancelled: Boolean
//啟動 Job
//如果此調(diào)用的確啟動了 Job,則返回 true
//如果 Job 調(diào)用前就已處于 started 或者是 completed 狀態(tài)顽耳,則返回 false
public fun start(): Boolean
//用于取消 Job坠敷,可同時通過傳入 Exception 來標(biāo)明取消原因
public fun cancel(cause: CancellationException? = null)
//阻塞等待直到此 Job 結(jié)束運(yùn)行
public suspend fun join()
//當(dāng) Job 結(jié)束運(yùn)行時(不管由于什么原因)回調(diào)此方法妙同,可用于接收可能存在的運(yùn)行異常
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
復(fù)制代碼
Job 具有以下幾種狀態(tài)值,每種狀態(tài)對應(yīng)的屬性值各不相同
State | isActive | isCompleted | isCancelled |
---|---|---|---|
New (optional initial state) | false | false | false |
Active (default initial state) | true | false | false |
Completing (transient state) | true | false | false |
Cancelling (transient state) | false | false | true |
Cancelled (final state) | false | true | true |
Completed (final state) | false | true | false |
fun main() {
//將協(xié)程設(shè)置為延遲啟動
val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
for (i in 0..100) {
//每循環(huán)一次均延遲一百毫秒
delay(100)
}
}
job.invokeOnCompletion {
log("invokeOnCompletion:$it")
}
log("1\. job.isActive:${job.isActive}")
log("1\. job.isCancelled:${job.isCancelled}")
log("1\. job.isCompleted:${job.isCompleted}")
job.start()
log("2\. job.isActive:${job.isActive}")
log("2\. job.isCancelled:${job.isCancelled}")
log("2\. job.isCompleted:${job.isCompleted}")
//休眠四百毫秒后再主動取消協(xié)程
Thread.sleep(400)
job.cancel(CancellationException("test"))
//休眠四百毫秒防止JVM過快停止導(dǎo)致 invokeOnCompletion 來不及回調(diào)
Thread.sleep(400)
log("3\. job.isActive:${job.isActive}")
log("3\. job.isCancelled:${job.isCancelled}")
log("3\. job.isCompleted:${job.isCompleted}")
}
復(fù)制代碼
[main] 1. job.isActive:false
[main] 1. job.isCancelled:false
[main] 1. job.isCompleted:false
[main] 2. job.isActive:true
[main] 2. job.isCancelled:false
[main] 2. job.isCompleted:false
[DefaultDispatcher-worker-2] invokeOnCompletion:java.util.concurrent.CancellationException: test
[main] 3. job.isActive:false
[main] 3. job.isCancelled:true
[main] 3. job.isCompleted:true
復(fù)制代碼
3膝迎、async
看下 async
函數(shù)的方法簽名粥帚。async
也是一個作用于 CoroutineScope 的擴(kuò)展函數(shù),和 launch
的區(qū)別主要就在于:async
可以返回協(xié)程的執(zhí)行結(jié)果限次,而 launch
不行
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
復(fù)制代碼
通過await()
方法可以拿到 async 協(xié)程的執(zhí)行結(jié)果芒涡,可以看到兩個協(xié)程的總耗時是遠(yuǎn)少于七秒的,總耗時基本等于耗時最長的協(xié)程
fun main() {
val time = measureTimeMillis {
runBlocking {
val asyncA = async {
delay(3000)
1
}
val asyncB = async {
delay(4000)
2
}
log(asyncA.await() + asyncB.await())
}
}
log(time)
}
復(fù)制代碼
[main] 3
[main] 4070
復(fù)制代碼
由于 launch 和 async 僅能夠在 CouroutineScope 中使用卖漫,所以任何創(chuàng)建的協(xié)程都會被該 scope 追蹤费尽。Kotlin 禁止創(chuàng)建不能夠被追蹤的協(xié)程,從而避免協(xié)程泄漏
4羊始、async 的錯誤用法
修改下上述代碼依啰,可以發(fā)現(xiàn)兩個協(xié)程的總耗時就會變?yōu)槠呙胱笥?/p>
fun main() {
val time = measureTimeMillis {
runBlocking {
val asyncA = async(start = CoroutineStart.LAZY) {
delay(3000)
1
}
val asyncB = async(start = CoroutineStart.LAZY) {
delay(4000)
2
}
log(asyncA.await() + asyncB.await())
}
}
log(time)
}
復(fù)制代碼
[main] 3
[main] 7077
復(fù)制代碼
會造成這不同區(qū)別是因?yàn)?CoroutineStart.LAZY
不會主動啟動協(xié)程,而是直到調(diào)用async.await()
或者async.satrt()
后才會啟動(即懶加載模式)店枣,所以asyncA.await() + asyncB.await()
會導(dǎo)致兩個協(xié)程其實(shí)是在順序執(zhí)行速警。而默認(rèn)值 CoroutineStart.DEFAULT
參數(shù)會使得協(xié)程在聲明的同時就被啟動了(實(shí)際上還需要等待被調(diào)度執(zhí)行,但可以看做是立即就執(zhí)行了)鸯两,所以即使 async.await()
會阻塞當(dāng)前線程直到協(xié)程返回結(jié)果值闷旧,但兩個協(xié)程其實(shí)都是處于運(yùn)行狀態(tài),所以總耗時就是四秒左右
此時可以通過先調(diào)用start()
再調(diào)用await()
來實(shí)現(xiàn)第一個例子的效果
asyncA.start()
asyncB.start()
log(asyncA.await() + asyncB.await())
復(fù)制代碼
5钧唐、async 并行分解
由 suspend
函數(shù)啟動的所有協(xié)程都必須在該函數(shù)返回結(jié)果時停止忙灼,因此你可能需要保證這些協(xié)程在返回結(jié)果之前完成。借助 Kotlin 中的結(jié)構(gòu)化并發(fā)機(jī)制钝侠,你可以定義用于啟動一個或多個協(xié)程的 coroutineScope
该园。然后,你可以使用 await()
(針對單個協(xié)程)或 awaitAll()
(針對多個協(xié)程)保證這些協(xié)程在從函數(shù)返回結(jié)果之前完成
例如帅韧,假設(shè)我們定義一個用于異步獲取兩個文檔的 coroutineScope
里初。通過對每個延遲引用調(diào)用 await()
,我們可以保證這兩項(xiàng) async
操作在返回值之前完成:
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
復(fù)制代碼
你還可以對集合使用 awaitAll()
忽舟,如以下示例所示:
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
復(fù)制代碼
雖然 fetchTwoDocs()
使用 async
啟動新協(xié)程双妨,但該函數(shù)使用 awaitAll()
等待啟動的協(xié)程完成后才會返回結(jié)果。不過請注意叮阅,即使我們沒有調(diào)用 awaitAll()
刁品,coroutineScope
構(gòu)建器也會等到所有新協(xié)程都完成后才恢復(fù)名為 fetchTwoDocs
的協(xié)程。此外浩姥,coroutineScope
會捕獲協(xié)程拋出的所有異常挑随,并將其傳送回調(diào)用方
6、Deferred
async
函數(shù)的返回值是一個 Deferred 對象勒叠。Deferred 是一個接口類型兜挨,繼承于 Job 接口膏孟,所以 Job 包含的屬性和方法 Deferred 都有,其主要就是在 Job 的基礎(chǔ)上擴(kuò)展了 await()
方法
七暑劝、CoroutineContext
CoroutineContext 使用以下元素集定義協(xié)程的行為:
- Job:控制協(xié)程的生命周期
- CoroutineDispatcher:將工作分派到適當(dāng)?shù)木€程
- CoroutineName:協(xié)程的名稱骆莹,可用于調(diào)試
- CoroutineExceptionHandler:處理未捕獲的異常
1、Job
協(xié)程中的 Job 是其上下文 CoroutineContext 中的一部分担猛,可以通過 coroutineContext[Job]
表達(dá)式從上下文中獲取到
以下兩個 log 語句雖然是運(yùn)行在不同的協(xié)程上幕垦,但是其指向的 Job 其實(shí)是同個對象
fun main() = runBlocking {
val job = launch {
log("My job is ${coroutineContext[Job]}")
}
log("My job is $job")
}
復(fù)制代碼
[main @coroutine#1] My job is "coroutine#2":StandaloneCoroutine{Active}@75a1cd57
[main @coroutine#2] My job is "coroutine#2":StandaloneCoroutine{Active}@75a1cd57
復(fù)制代碼
實(shí)際上 CoroutineScope 的 isActive
這個擴(kuò)展屬性只是 coroutineContext[Job]?.isActive == true
的一種簡便寫法
public val CoroutineScope.isActive: Boolean
get() = coroutineContext[Job]?.isActive ?: true
復(fù)制代碼
2、CoroutineDispatcher
CoroutineContext 包含一個 CoroutineDispatcher(協(xié)程調(diào)度器)用于指定執(zhí)行協(xié)程的目標(biāo)載體傅联,即運(yùn)行于哪個線程先改。CoroutineDispatcher 可以將協(xié)程的執(zhí)行操作限制在特定線程上,也可以將其分派到線程池中蒸走,或者讓它無限制地運(yùn)行仇奶。所有的協(xié)程構(gòu)造器(如 launch 和 async)都接受一個可選參數(shù),即 CoroutineContext 比驻,該參數(shù)可用于顯式指定要創(chuàng)建的協(xié)程和其它上下文元素所要使用的 CoroutineDispatcher
要在主線程之外運(yùn)行代碼该溯,可以讓 Kotlin 協(xié)程在 Default 或 IO 調(diào)度程序上執(zhí)行工作。在 Kotlin 中别惦,所有協(xié)程都必須在 CoroutineDispatcher 中運(yùn)行狈茉,即使它們在主線程上運(yùn)行也是如此。協(xié)程可以自行暫停掸掸,而 CoroutineDispatcher 負(fù)責(zé)將其恢復(fù)
Kotlin 協(xié)程庫提供了四個 Dispatcher 用于指定在何處運(yùn)行協(xié)程氯庆,大部分情況下我們只會接觸以下三個:
-
Dispatchers.Main - 使用此調(diào)度程序可在 Android 主線程上運(yùn)行協(xié)程。此調(diào)度程序只能用于與界面交互和執(zhí)行快速工作扰付。示例包括調(diào)用
suspend
函數(shù)堤撵、運(yùn)行 Android 界面框架操作,以及更新LiveData
對象 - Dispatchers.IO - 此調(diào)度程序經(jīng)過了專門優(yōu)化羽莺,適合在主線程之外執(zhí)行磁盤或網(wǎng)絡(luò) I/O实昨。示例包括使用 Room 組件、從文件中讀取數(shù)據(jù)或向文件中寫入數(shù)據(jù)禽翼,以及運(yùn)行任何網(wǎng)絡(luò)操作
- Dispatchers.Default - 此調(diào)度程序經(jīng)過了專門優(yōu)化屠橄,適合在主線程之外執(zhí)行占用大量 CPU 資源的工作。用例示例包括對列表排序和解析 JSON
fun main() = runBlocking<Unit> {
launch {
log("main runBlocking")
}
launch(Dispatchers.Default) {
log("Default")
}
launch(Dispatchers.IO) {
log("IO")
}
launch(newSingleThreadContext("MyOwnThread")) {
log("newSingleThreadContext")
}
}
復(fù)制代碼
[DefaultDispatcher-worker-1 @coroutine#3] Default
[DefaultDispatcher-worker-2 @coroutine#4] IO
[MyOwnThread @coroutine#5] newSingleThreadContext
[main @coroutine#2] main runBlocking
復(fù)制代碼
當(dāng) launch {...}
在不帶參數(shù)的情況下使用時闰挡,它從外部的協(xié)程作用域繼承上下文和調(diào)度器,即和 runBlocking 保持一致礁哄。而在 GlobalScope 中啟動協(xié)程時默認(rèn)使用的調(diào)度器是 Dispatchers.default长酗,并使用共享的后臺線程池,因此 launch(Dispatchers.default){...}
與 GlobalScope.launch{...}
是使用相同的調(diào)度器桐绒。newSingleThreadContext
用于為協(xié)程專門創(chuàng)建一個新的線程來運(yùn)行夺脾,專用線程是一種成本非常昂貴的資源之拨,在實(shí)際的應(yīng)用程序中必須在不再需要時釋放掉,或者存儲在頂級變量中以便在整個應(yīng)用程序中進(jìn)行重用
3咧叭、withContext
對于以下代碼蚀乔,get
方法內(nèi)使用withContext(Dispatchers.IO)
創(chuàng)建了一個指定在 IO 線程池中運(yùn)行的代碼塊,該區(qū)間內(nèi)的任何代碼都始終通過 IO 線程來執(zhí)行菲茬。由于 withContext
方法本身就是一個掛起函數(shù)吉挣,因此 get
方法也必須定義為掛起函數(shù)
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("developer.android.com") // Dispatchers.Main
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}
復(fù)制代碼
借助協(xié)程,你可以細(xì)粒度地來調(diào)度線程婉弹。由于withContext()
支持讓你在不引入回調(diào)的情況下控制任何代碼的執(zhí)行線程池睬魂,因此你可以將其應(yīng)用于非常小的函數(shù),例如從數(shù)據(jù)庫中讀取數(shù)據(jù)或執(zhí)行網(wǎng)絡(luò)請求镀赌。一種不錯的做法是使用 withContext()
來確保每個函數(shù)都是主線程安全的氯哮,這意味著,你可以從主線程調(diào)用每個函數(shù)商佛。這樣喉钢,調(diào)用方就從不需要考慮應(yīng)該使用哪個線程來執(zhí)行函數(shù)了
在前面的示例中,fetchDocs()
方法在主線程上執(zhí)行良姆;不過肠虽,它可以安全地調(diào)用 get
方法,這樣會在后臺執(zhí)行網(wǎng)絡(luò)請求歇盼。由于協(xié)程支持 suspend
和 resume
舔痕,因此 withContext
塊完成后,主線程上的協(xié)程會立即根據(jù) get
結(jié)果恢復(fù)
與基于回調(diào)的等效實(shí)現(xiàn)相比豹缀,withContext()
不會增加額外的開銷伯复。此外在某些情況下,還可以優(yōu)化 withContext()
調(diào)用,使其超越基于回調(diào)的等效實(shí)現(xiàn)。例如牲证,如果某個函數(shù)對一個網(wǎng)絡(luò)進(jìn)行十次調(diào)用潮饱,你可以使用外部 withContext()
讓 Kotlin 只切換一次線程。這樣盗飒,即使網(wǎng)絡(luò)庫多次使用 withContext()
,它也會留在同一調(diào)度程序上,并避免切換線程帘不。此外,Kotlin 還優(yōu)化了 Dispatchers.Default
與 Dispatchers.IO
之間的切換杨箭,以盡可能避免線程切換
利用一個使用線程池的調(diào)度程序(例如
Dispatchers.IO
或Dispatchers.Default
)不能保證代碼塊一直在同一線程上從上到下執(zhí)行寞焙。在某些情況下,Kotlin 協(xié)程在suspend
和resume
后可能會將執(zhí)行工作移交給另一個線程。這意味著捣郊,對于整個withContext()
塊辽狈,線程局部變量可能并不指向同一個值
4、CoroutineName
CoroutineName 用于為協(xié)程指定一個名字呛牲,方便調(diào)試和定位問題
fun main() = runBlocking<Unit>(CoroutineName("RunBlocking")) {
log("start")
launch(CoroutineName("MainCoroutine")) {
launch(CoroutineName("Coroutine#A")) {
delay(400)
log("launch A")
}
launch(CoroutineName("Coroutine#B")) {
delay(300)
log("launch B")
}
}
}
復(fù)制代碼
[main @RunBlocking#1] start
[main @Coroutine#B#4] launch B
[main @Coroutine#A#3] launch A
復(fù)制代碼
5刮萌、CoroutineExceptionHandler
在下文的異常處理會講到
6、組合上下文元素
有時我們需要為協(xié)程上下文定義多個元素娘扩,那就可以用 +
運(yùn)算符着茸。例如,我們可以同時為協(xié)程指定 Dispatcher 和 CoroutineName
fun main() = runBlocking<Unit> {
launch(Dispatchers.Default + CoroutineName("test")) {
log("Hello World")
}
}
復(fù)制代碼
[DefaultDispatcher-worker-1 @test#2] Hello World
復(fù)制代碼
此外畜侦,由于 CoroutineContext 是由一組元素組成的元扔,所以加號右側(cè)的元素會覆蓋加號左側(cè)的元素,進(jìn)而組成新創(chuàng)建的 CoroutineContext旋膳。比如澎语,(Dispatchers.Main, "name") + (Dispatchers.IO) = (Dispatchers.IO, "name")
八、取消協(xié)程
如果用戶退出某個啟動了協(xié)程的 Activity/Fragment 的話验懊,那么大部分情況下就應(yīng)該取消所有協(xié)程
job.cancel()
就用于取消協(xié)程擅羞,job.join()
用于阻塞等待協(xié)程運(yùn)行結(jié)束。因?yàn)?cancel()
函數(shù)調(diào)用后會馬上返回而不是等待協(xié)程結(jié)束后再返回义图,所以此時協(xié)程不一定就是已經(jīng)停止運(yùn)行了减俏。如果需要確保協(xié)程結(jié)束運(yùn)行后再執(zhí)行后續(xù)代碼,就需要調(diào)用 join()
方法來阻塞等待碱工。也可以通過調(diào)用 Job 的擴(kuò)展函數(shù) cancelAndJoin()
來完成相同操作娃承,它結(jié)合了 cancel
和 join
兩個操作
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
log("job: I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L)
log("main: I'm tired of waiting!")
job.cancel()
job.join()
log("main: Now I can quit.")
}
復(fù)制代碼
[main] job: I'm sleeping 0 ...
[main] job: I'm sleeping 1 ...
[main] job: I'm sleeping 2 ...
[main] main: I'm tired of waiting!
[main] main: Now I can quit.
復(fù)制代碼
1、協(xié)程可能無法取消
并不是所有協(xié)程都可以響應(yīng)取消操作怕篷,協(xié)程的取消操作是需要協(xié)作(cooperative)完成的历筝,協(xié)程必須協(xié)作才能取消。協(xié)程庫中的所有掛起函數(shù)都是可取消的廊谓,它們在運(yùn)行時會檢查協(xié)程是否被取消了梳猪,并在取消時拋出 CancellationException 從而結(jié)束整個任務(wù)。但如果協(xié)程正在執(zhí)行計算任務(wù)并且未檢查是否已處于取消狀態(tài)的話蒸痹,就無法取消協(xié)程
所以即使以下代碼主動取消了協(xié)程春弥,協(xié)程也只會在完成既定循環(huán)后才結(jié)束運(yùn)行,因?yàn)閰f(xié)程沒有在每次循環(huán)前先進(jìn)行檢查叠荠,導(dǎo)致任務(wù)不受取消操作的影響
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) {
if (System.currentTimeMillis() >= nextPrintTime) {
log("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L)
log("main: I'm tired of waiting!")
job.cancelAndJoin()
log("main: Now I can quit.")
}
復(fù)制代碼
[DefaultDispatcher-worker-1] job: I'm sleeping 0 ...
[DefaultDispatcher-worker-1] job: I'm sleeping 1 ...
[DefaultDispatcher-worker-1] job: I'm sleeping 2 ...
[main] main: I'm tired of waiting!
[DefaultDispatcher-worker-1] job: I'm sleeping 3 ...
[DefaultDispatcher-worker-1] job: I'm sleeping 4 ...
[main] main: Now I can quit.
復(fù)制代碼
為了實(shí)現(xiàn)取消協(xié)程的目的匿沛,就需要為上述代碼加上判斷協(xié)程是否還處于可運(yùn)行狀態(tài)的邏輯,當(dāng)不可運(yùn)行時就主動退出協(xié)程榛鼎。isActive
是 CoroutineScope 的擴(kuò)展屬性俺祠,就用于判斷協(xié)程是否還處于可運(yùn)行狀態(tài)
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) {
if (isActive) {
if (System.currentTimeMillis() >= nextPrintTime) {
log("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
} else {
return@launch
}
}
}
delay(1300L)
log("main: I'm tired of waiting!")
job.cancelAndJoin()
log("main: Now I can quit.")
}
復(fù)制代碼
取消協(xié)程這個操作類似于在 Java 中調(diào)用Thread.interrupt()
方法來向線程發(fā)起中斷請求公给,這兩個操作都不會強(qiáng)制停止協(xié)程和線程借帘,外部只是相當(dāng)于發(fā)起一個停止運(yùn)行的請求蜘渣,需要依靠協(xié)程和線程響應(yīng)請求后主動停止運(yùn)行。Kotlin 和 Java 之所以均沒有提供一個可以直接強(qiáng)制停止協(xié)程或線程的方法肺然,是因?yàn)檫@個操作可能會帶來各種意想不到的情況蔫缸。在停止協(xié)程和線程的時候,它們可能還持有著某些排他性資源(例如:鎖际起,數(shù)據(jù)庫鏈接)拾碌,如果強(qiáng)制性地停止,它們持有的鎖就會一直無法得到釋放街望,導(dǎo)致其他協(xié)程和線程一直無法得到目標(biāo)資源校翔,最終可能導(dǎo)致線程死鎖。所以Thread.stop()
方法目前也是處于廢棄狀態(tài)灾前,Java 官方并沒有提供可靠的停止線程的方法
2防症、用 finally 釋放資源
可取消的掛起函數(shù)在取消時會拋出 CancellationException,可以依靠try {...} finally {...}
或者 Kotlin 的 use
函數(shù)在取消協(xié)程后釋放持有的資源
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
log("job: I'm sleeping $i ...")
delay(500L)
}
} catch (e: Throwable) {
log(e.message)
} finally {
log("job: I'm running finally")
}
}
delay(1300L)
log("main: I'm tired of waiting!")
job.cancelAndJoin()
log("main: Now I can quit.")
}
復(fù)制代碼
[main] job: I'm sleeping 0 ...
[main] job: I'm sleeping 1 ...
[main] job: I'm sleeping 2 ...
[main] main: I'm tired of waiting!
[main] StandaloneCoroutine was cancelled
[main] job: I'm running finally
[main] main: Now I can quit.
復(fù)制代碼
3哎甲、NonCancellable
如果在上一個例子中的 finally
塊中再調(diào)用掛起函數(shù)的話蔫敲,將會導(dǎo)致拋出 CancellationException,因?yàn)榇藭r協(xié)程已經(jīng)被取消了炭玫。通常我們并不會遇到這種情況奈嘿,因?yàn)槌R姷馁Y源釋放操作都是非阻塞的,且不涉及任何掛起函數(shù)吞加。但在極少數(shù)情況下我們需要在取消的協(xié)程中再調(diào)用掛起函數(shù)裙犹,此時可以使用 withContext
函數(shù)和 NonCancellable
上下文將相應(yīng)的代碼包裝在 withContext(NonCancellable) {...}
代碼塊中,NonCancellable 就用于創(chuàng)建一個無法取消的協(xié)程作用域
fun main() = runBlocking {
log("start")
val launchA = launch {
try {
repeat(5) {
delay(50)
log("launchA-$it")
}
} finally {
delay(50)
log("launchA isCompleted")
}
}
val launchB = launch {
try {
repeat(5) {
delay(50)
log("launchB-$it")
}
} finally {
withContext(NonCancellable) {
delay(50)
log("launchB isCompleted")
}
}
}
//延時一百毫秒衔憨,保證兩個協(xié)程都已經(jīng)被啟動了
delay(200)
launchA.cancel()
launchB.cancel()
log("end")
}
復(fù)制代碼
[main] start
[main] launchA-0
[main] launchB-0
[main] launchA-1
[main] launchB-1
[main] launchA-2
[main] launchB-2
[main] end
[main] launchB isCompleted
復(fù)制代碼
4叶圃、父協(xié)程和子協(xié)程
當(dāng)一個協(xié)程在另外一個協(xié)程的協(xié)程作用域中啟動時,它將通過 CoroutineScope.coroutineContext
繼承其上下文巫财,新啟動的協(xié)程就被稱為子協(xié)程盗似,子協(xié)程的 Job 將成為父協(xié)程 Job 的子 Job。父協(xié)程總是會等待其所有子協(xié)程都完成后才結(jié)束自身平项,所以父協(xié)程不必顯式跟蹤它啟動的所有子協(xié)程赫舒,也不必使用 Job.join
在末尾等待子協(xié)程完成
所以雖然 parentJob 啟動的三個子協(xié)程的延時時間各不相同,但它們最終都會打印出日志
fun main() = runBlocking {
val parentJob = launch {
repeat(3) { i ->
launch {
delay((i + 1) * 200L)
log("Coroutine $i is done")
}
}
log("request: I'm done and I don't explicitly join my children that are still active")
}
}
復(fù)制代碼
[main @coroutine#2] request: I'm done and I don't explicitly join my children that are still active
[main @coroutine#3] Coroutine 0 is done
[main @coroutine#4] Coroutine 1 is done
[main @coroutine#5] Coroutine 2 is done
復(fù)制代碼
5闽瓢、傳播取消操作
一般情況下接癌,協(xié)程的取消操作會通過協(xié)程的層次結(jié)構(gòu)來進(jìn)行傳播。如果取消父協(xié)程或者父協(xié)程拋出異常扣讼,那么子協(xié)程都會被取消缺猛。而如果子協(xié)程被取消,則不會影響同級協(xié)程和父協(xié)程,但如果子協(xié)程拋出異常則也會導(dǎo)致同級協(xié)程和父協(xié)程被取消
對于以下代碼荔燎,子協(xié)程 jon1 被取消并不影響子協(xié)程 jon2 和父協(xié)程繼續(xù)運(yùn)行耻姥,但父協(xié)程被取消后子協(xié)程都會被遞歸取消
fun main() = runBlocking {
val request = launch {
val job1 = launch {
repeat(10) {
delay(300)
log("job1: $it")
if (it == 2) {
log("job1 canceled")
cancel()
}
}
}
val job2 = launch {
repeat(10) {
delay(300)
log("job2: $it")
}
}
}
delay(1600)
log("parent job canceled")
request.cancel()
delay(1000)
}
復(fù)制代碼
[main @coroutine#3] job1: 0
[main @coroutine#4] job2: 0
[main @coroutine#3] job1: 1
[main @coroutine#4] job2: 1
[main @coroutine#3] job1: 2
[main @coroutine#3] job1 canceled
[main @coroutine#4] job2: 2
[main @coroutine#4] job2: 3
[main @coroutine#4] job2: 4
[main @coroutine#1] parent job canceled
復(fù)制代碼
6、withTimeout
withTimeout
函數(shù)用于指定協(xié)程的運(yùn)行超時時間有咨,如果超時則會拋出 TimeoutCancellationException琐簇,從而令協(xié)程結(jié)束運(yùn)行
fun main() = runBlocking {
log("start")
val result = withTimeout(300) {
repeat(5) {
delay(100)
}
200
}
log(result)
log("end")
}
復(fù)制代碼
[main] start
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 300 ms
at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:186)
at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:156)
at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:497)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:69)
at java.lang.Thread.run(Thread.java:748)
復(fù)制代碼
withTimeout
方法拋出的 TimeoutCancellationException 是 CancellationException 的子類,之前我們并未在輸出日志上看到關(guān)于 CancellationException 這類異常的堆棧信息座享,這是因?yàn)閷τ谝粋€已取消的協(xié)程來說婉商,CancellationException 被認(rèn)為是觸發(fā)協(xié)程結(jié)束的正常原因。但對于withTimeout
方法來說渣叛,拋出異常是其上報超時情況的一種手段丈秩,所以該異常不會被協(xié)程內(nèi)部消化掉
如果不希望因?yàn)楫惓?dǎo)致協(xié)程結(jié)束,可以改用withTimeoutOrNull
方法淳衙,如果超時就會返回 null
九蘑秽、異常處理
當(dāng)一個協(xié)程由于異常而運(yùn)行失敗時,它會傳播這個異常并傳遞給它的父協(xié)程滤祖。接下來筷狼,父協(xié)程會進(jìn)行下面幾步操作:
- 取消它自己的子級
- 取消它自己
- 將異常傳播并傳遞給它的父級
異常會到達(dá)層級的根部,而且當(dāng)前 CoroutineScope 所啟動的所有協(xié)程都會被取消匠童,但協(xié)程并非都是一發(fā)現(xiàn)異常就執(zhí)行以上流程埂材,launch 和 async 在處理異常方面有著很大的差異
launch 將異常視為未捕獲異常,類似于 Java 的 Thread.uncaughtExceptionHandler汤求,當(dāng)發(fā)現(xiàn)異常時就會馬上拋出俏险。async 期望最終是通過調(diào)用 await 來獲取結(jié)果 (或者異常),所以默認(rèn)情況下它不會拋出異常扬绪。這意味著如果使用 async 啟動新的協(xié)程竖独,它會靜默地將異常丟棄,直到調(diào)用 async.await()
才會得到目標(biāo)值或者拋出存在的異常
例如挤牛,以下代碼中 launchA 拋出的異常會先連鎖導(dǎo)致 launchB 也被取消(拋出 JobCancellationException)莹痢,然后再導(dǎo)致父協(xié)程 BlockingCoroutine 也被取消
fun main() = runBlocking {
val launchA = launch {
delay(1000)
1 / 0
}
val launchB = launch {
try {
delay(1300)
log("launchB")
} catch (e: CancellationException) {
e.printStackTrace()
}
}
launchA.join()
launchB.join()
}
復(fù)制代碼
kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=BlockingCoroutine{Cancelling}@5eb5c224
Caused by: java.lang.ArithmeticException: / by zero
at coroutines.CoroutinesMainKt$main$1$launchA$1.invokeSuspend(CoroutinesMain.kt:11)
···
Exception in thread "main" java.lang.ArithmeticException: / by zero
at coroutines.CoroutinesMainKt$main$1$launchA$1.invokeSuspend(CoroutinesMain.kt:11)
···
復(fù)制代碼
1、CoroutineExceptionHandler
如果不想將所有的異常信息都打印到控制臺上墓赴,那么可以使用 CoroutineExceptionHandler 作為協(xié)程的上下文元素之一竞膳,在這里進(jìn)行自定義日志記錄或異常處理,它類似于對線程使用 Thread.uncaughtExceptionHandler诫硕。但是坦辟,CoroutineExceptionHandler 只會在預(yù)計不會由用戶處理的異常上調(diào)用,因此在 async 中使用它沒有任何效果章办,當(dāng) async 內(nèi)部發(fā)生了異常且沒有捕獲時锉走,那么調(diào)用 async.await()
依然會導(dǎo)致應(yīng)用崩潰
以下代碼只會捕獲到 launch 拋出的異常
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
log("Caught $exception")
}
val job = GlobalScope.launch(handler) {
throw AssertionError()
}
val deferred = GlobalScope.async(handler) {
throw ArithmeticException()
}
joinAll(job, deferred)
}
復(fù)制代碼
[DefaultDispatcher-worker-2] Caught java.lang.AssertionError
復(fù)制代碼
2滨彻、SupervisorJob
由于異常導(dǎo)致的取消在協(xié)程中是一種雙向關(guān)系,會在整個協(xié)程層次結(jié)構(gòu)中傳播挪蹭,但如果我們需要的是單向取消該怎么實(shí)現(xiàn)呢亭饵?
例如,假設(shè)在 Activity 中啟動了多個協(xié)程嚣潜,如果單個協(xié)程所代表的子任務(wù)失敗了冬骚,此時并不一定需要連鎖終止整個 Activity 內(nèi)部的所有其它協(xié)程任務(wù),即此時希望子協(xié)程的異常不會傳播給同級協(xié)程和父協(xié)程懂算。而當(dāng) Activity 退出后,父協(xié)程的異常(即 CancellationException)又應(yīng)該連鎖傳播給所有子協(xié)程庇麦,終止所有子協(xié)程
可以使用 SupervisorJob 來實(shí)現(xiàn)上述效果计技,它類似于常規(guī)的 Job,唯一的區(qū)別就是取消操作只會向下傳播山橄,一個子協(xié)程的運(yùn)行失敗不會影響到其他子協(xié)程
例如垮媒,以下示例中 firstChild 拋出的異常不會導(dǎo)致 secondChild 被取消,但當(dāng) supervisor 被取消時 secondChild 也被同時取消了
fun main() = runBlocking {
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {
log("First child is failing")
throw AssertionError("First child is cancelled")
}
val secondChild = launch {
firstChild.join()
log("First child is cancelled: ${firstChild.isCancelled}, but second one is still active")
try {
delay(Long.MAX_VALUE)
} finally {
log("Second child is cancelled because supervisor is cancelled")
}
}
firstChild.join()
log("Cancelling supervisor")
//取消所有協(xié)程
supervisor.cancel()
secondChild.join()
}
}
復(fù)制代碼
[main] First child is failing
[main] First child is cancelled: true, but second one is still active
[main] Cancelling supervisor
[main] Second child is cancelled because supervisor is cancelled
復(fù)制代碼
但是航棱,如果異常沒有被處理且 CoroutineContext 沒有包含一個 CoroutineExceptionHandler 的話睡雇,異常會到達(dá)默認(rèn)線程的 ExceptionHandler。在 JVM 中饮醇,異常會被打印在控制臺它抱;而在 Android 中,無論異常在那個 Dispatcher 中發(fā)生朴艰,都會直接導(dǎo)致應(yīng)用崩潰观蓄。所以如果上述例子中移除了 firstChild 包含的 CoroutineExceptionHandler 的話,就會導(dǎo)致 Android 應(yīng)用崩潰
?? 未被捕獲的異常一定會被拋出祠墅,無論使用的是哪種 Job
十侮穿、Android KTX
Android KTX 是包含在 Android Jetpack 及其他 Android 庫中的一組 Kotlin 擴(kuò)展程序。KTX 擴(kuò)展程序可以為 Jetpack毁嗦、Android 平臺及其他 API 提供簡潔的慣用 Kotlin 代碼亲茅。為此,這些擴(kuò)展程序利用了多種 Kotlin 語言功能狗准,其中就包括了對 Kotlin 協(xié)程的支持
1克锣、ViewModel KTX
ViewModel KTX 庫提供了一個 viewModelScope
,用于在 ViewModel 啟動協(xié)程驶俊,該作用域的生命周期和 ViewModel 相等娶耍,當(dāng) ViewModel 回調(diào)了 onCleared()
方法后會自動取消所有當(dāng)前 ViewModel 中的所有協(xié)程
引入依賴:
dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
}
復(fù)制代碼
例如,以下 fetchDocs()
方法內(nèi)就依靠 viewModelScope
啟動了一個協(xié)程饼酿,用于在后臺線程發(fā)起網(wǎng)絡(luò)請求
class MyViewModel : ViewModel() {
fun fetchDocs() {
viewModelScope.launch {
val result = get("https://developer.android.com")
show(result)
}
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
}
復(fù)制代碼
2榕酒、Lifecycle KTX
Lifecycle KTX 為每個 Lifecycle
對象定義了一個 LifecycleScope
胚膊,該作用域具有生命周期安全的保障,在此范圍內(nèi)啟動的協(xié)程會在 Lifecycle
被銷毀時同時取消想鹰,可以使用 lifecycle.coroutineScope
或 lifecycleOwner.lifecycleScope
屬性來拿到該 CoroutineScope
引入依賴:
dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
}
復(fù)制代碼
以下示例演示了如何使用 lifecycleOwner.lifecycleScope
異步創(chuàng)建預(yù)計算文本:
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)
}
}
}
復(fù)制代碼
3紊婉、LiveData KTX
使用 LiveData 時,你可能需要異步計算值辑舷。例如喻犁,你可能需要檢索用戶的偏好設(shè)置并將其傳送給界面。在這些情況下何缓,LiveData KTX 提供了一個 liveData
構(gòu)建器函數(shù)肢础,該函數(shù)會調(diào)用 suspend 函數(shù)并將結(jié)果賦值給 LiveData
引入依賴:
dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
}
復(fù)制代碼
在以下示例中,loadUser()
是在其他地方聲明的 suspend 函數(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)
}