一、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)注的功能點包括:
- 輕量:你可以在單個線程上運行多個協(xié)程生巡,因為協(xié)程支持掛起耙蔑,不會使正在運行協(xié)程的線程阻塞。掛起比阻塞節(jié)省內(nèi)存孤荣,且支持多個并行操作
- 內(nèi)存泄露更少:使用結(jié)構(gòu)化并發(fā)機(jī)制在一個作用域內(nèi)執(zhí)行多個操作
- 內(nèi)置取消支持:取消功能會自動通過正在運行的協(xié)程層次結(jié)構(gòu)傳播
- Jetpack 集成:許多 Jetpack 庫都包含提供全面協(xié)程支持的擴(kuò)展甸陌。某些庫還提供自己的協(xié)程作用域,可供你用于結(jié)構(gòu)化并發(fā)
引入依賴:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
二盐股、協(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")
[DefaultDispatcher-worker-1 @coroutine#1] launch
[main] end
在上面的例子中牲尺,通過 GlobalScope(即全局作用域)啟動了一個協(xié)程,在延遲一秒后輸出一行日志。從輸出結(jié)果可以看出來谤碳,啟動的協(xié)程是運行在協(xié)程內(nèi)部的線程池中溃卡。雖然從表現(xiàn)結(jié)果上來看,啟動一個協(xié)程類似于我們直接使用 Thread 來執(zhí)行耗時任務(wù)蜒简,但實際上協(xié)程和線程有著本質(zhì)上的區(qū)別瘸羡。通過使用協(xié)程,可以極大的提高線程的并發(fā)效率臭蚁,避免以往的嵌套回調(diào)地獄最铁,極大提高了代碼的可讀性
以上代碼就涉及到了協(xié)程的四個基礎(chǔ)概念:
- suspend function。即掛起函數(shù)垮兑,delay 函數(shù)就是協(xié)程庫提供的一個用于實現(xiàn)非阻塞式延時的掛起函數(shù)
- CoroutineScope冷尉。即協(xié)程作用域,GlobalScope 是 CoroutineScope 的一個實現(xiàn)類系枪,用于指定協(xié)程的作用范圍雀哨,可用于管理多個協(xié)程的生命周期,所有協(xié)程都需要通過 CoroutineScope 來啟動
- CoroutineContext私爷。即協(xié)程上下文雾棺,包含多種類型的配置參數(shù)。Dispatchers.IO 就是 CoroutineContext 這個抽象概念的一種實現(xiàn)衬浑,用于指定協(xié)程的運行載體捌浩,即用于指定協(xié)程要運行在哪類線程上
- 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)
讀者在網(wǎng)上看關(guān)于協(xié)程的文章的時候俭缓,應(yīng)該經(jīng)常會看到這么一句話:掛起函數(shù)不會阻塞其所在線程克伊,而是會將協(xié)程掛起,在特定的時候才再恢復(fù)協(xié)程
對于這句話我的理解是:delay()
函數(shù)類似于 Java 中的 Thread.sleep()
华坦,而之所以說 delay()
函數(shù)是非阻塞的愿吹,是因為它和單純的線程休眠有著本質(zhì)的區(qū)別。協(xié)程是運行于線程上的季春,一個線程可以運行多個(幾千上萬個)協(xié)程洗搂。線程的調(diào)度行為是由操作系統(tǒng)來管理的消返,而協(xié)程的調(diào)度行為是可以由開發(fā)者來指定并由編譯器來實現(xiàn)的载弄,協(xié)程能夠細(xì)粒度地控制多個任務(wù)的執(zhí)行時機(jī)和執(zhí)行線程耘拇,當(dāng)某個特定的線程上的所有協(xié)程被 suspend 后,該線程便可騰出資源去處理其他任務(wù)
例如宇攻,當(dāng)在 ThreadA 上運行的 CoroutineA 調(diào)用了delay(1000L)
函數(shù)指定延遲一秒后再運行惫叛,ThreadA 會轉(zhuǎn)而去執(zhí)行 CoroutineB,等到一秒后再來繼續(xù)執(zhí)行 CoroutineA逞刷。所以嘉涌,ThreadA 并不會因為 CoroutineA 的延時而阻塞,而是能繼續(xù)去執(zhí)行其它任務(wù)夸浅,所以掛起函數(shù)并不會阻塞其所在線程仑最,這樣就極大地提高了線程的并發(fā)靈活度,最大化了線程的利用效率帆喇。而如果是使用Thread.sleep()
的話警医,線程就只能干等著而不能去執(zhí)行其它任務(wù),降低了線程的利用效率
四坯钦、suspend function 的掛起與恢復(fù)
協(xié)程在常規(guī)函數(shù)的基礎(chǔ)上添加了兩項操作用于處理長時間運行的任務(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)用
以下示例展示了一項任務(wù)(假設(shè) get 方法是一個網(wǎng)絡(luò)請求任務(wù))的簡單協(xié)程實現(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) { /* ... */ }
在上面的示例中,get()
仍在主線程上被調(diào)用突颊,但它會在啟動網(wǎng)絡(luò)請求之前暫停協(xié)程鲁豪。get()
主體內(nèi)通過調(diào)用 withContext(Dispatchers.IO)
創(chuàng)建了一個在 IO 線程池中運行的代碼塊,在該塊內(nèi)的任何代碼都始終通過 IO 調(diào)度器執(zhí)行洋丐。當(dāng)網(wǎng)絡(luò)請求完成后呈昔,get()
會恢復(fù)已暫停的協(xié)程,使得主線程協(xié)程可以直接拿到網(wǎng)絡(luò)請求結(jié)果而不用使用回調(diào)來通知主線程友绝。Retrofit 就是以這種方式來實現(xiàn)對協(xié)程的支持的
Kotlin 使用堆棧幀管理要運行哪個函數(shù)以及所有局部變量堤尾。暫停協(xié)程時,系統(tǒng)會復(fù)制并保存當(dāng)前的堆棧幀以供稍后使用迁客」Γ恢復(fù)時,會將堆棧幀從其保存位置復(fù)制回來,然后函數(shù)再次開始運行嵌灰。即使代碼可能看起來像普通的順序阻塞請求栓辜,協(xié)程也能確保網(wǎng)絡(luò)請求避免阻塞主線程
在主線程進(jìn)行的暫停協(xié)程和恢復(fù)協(xié)程的兩個操作,既實現(xiàn)了將耗時任務(wù)交由后臺線程完成衔统,保障了主線程安全,又以同步代碼的方式完成了實際上的多線程異步調(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()
取消正在運行的協(xié)程鹃骂。CoroutineScope 本身并不運行協(xié)程,它只是確保你不會失去對協(xié)程的追蹤罢绽,即使協(xié)程被掛起也是如此畏线。在 Android 中,某些 KTX 庫為某些生命周期類提供了自己的 CoroutineScope
良价。例如寝殴,ViewModel
有 viewModelScope
,Lifecycle
有 lifecycleScope
CoroutineScope 大體上可以分為三種:
- GlobalScope明垢。即全局協(xié)程作用域蚣常,在這個范圍內(nèi)啟動的協(xié)程可以一直運行直到應(yīng)用停止運行。GlobalScope 本身不會阻塞當(dāng)前線程痊银,且啟動的協(xié)程相當(dāng)于守護(hù)線程抵蚊,不會阻止 JVM 結(jié)束運行
- runBlocking。一個頂層函數(shù)溯革,和 GlobalScope 不一樣贞绳,它會阻塞當(dāng)前線程直到其內(nèi)部所有相同作用域的協(xié)程執(zhí)行結(jié)束
- 自定義 CoroutineScope≈孪。可用于實現(xiàn)主動控制協(xié)程的生命周期范圍冈闭,對于 Android 開發(fā)來說最大意義之一就是可以避免內(nèi)存泄露
1、GlobalScope
GlobalScope 屬于全局作用域抖单,這意味著通過 GlobalScope 啟動的協(xié)程的生命周期只受整個應(yīng)用程序的生命周期的限制萎攒,只要整個應(yīng)用程序還在運行且協(xié)程的任務(wù)還未結(jié)束遇八,協(xié)程就可以一直運行
GlobalScope 不會阻塞其所在線程,所以以下代碼中主線程的日志會早于 GlobalScope 內(nèi)部輸出日志耍休。此外押蚤,GlobalScope 啟動的協(xié)程相當(dāng)于守護(hù)線程,不會阻止 JVM 結(jié)束運行羹应,所以如果將主線程的休眠時間改為三百毫秒的話,就不會看到 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)
}
[main] start
[main] end
[DefaultDispatcher-worker-1 @coroutine#1] GlobalScope
[DefaultDispatcher-worker-3 @coroutine#3] launch B
[DefaultDispatcher-worker-3 @coroutine#2] launch A
GlobalScope.launch
會創(chuàng)建一個頂級協(xié)程次屠,盡管它很輕量級园匹,但在運行時還是會消耗一些內(nèi)存資源,且可以一直運行直到整個應(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
runBlocking 的一個方便之處就是:只有當(dāng)內(nèi)部相同作用域的所有協(xié)程都運行結(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)部運行的協(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")
}
[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
基于是否會阻塞線程的區(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")
}
[main] runBlocking
[DefaultDispatcher-worker-1] GlobalScope
[main] after sleep
3、coroutineScope
coroutineScope
函數(shù)用于創(chuàng)建一個獨立的協(xié)程作用域萍聊,直到所有啟動的協(xié)程都完成后才結(jié)束自身问芬。runBlocking
和 coroutineScope
看起來很像,因為它們都需要等待其內(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")
}
[main] Task from coroutine scope
[main] Task from runBlocking
[main] Task from nested launch
[main] Coroutine scope is over
4、supervisorScope
supervisorScope
函數(shù)用于創(chuàng)建一個使用了 SupervisorJob 的 coroutineScope墨微,該作用域的特點就是拋出的異常不會連鎖取消同級協(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")
}
[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
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é)程作用域的實例來管理協(xié)程的生命周期镀脂。CoroutineScope 的實例可以通過 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()
}
}
或者薄翅,我們可以通過委托模式來讓 Activity 實現(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")
}
}
從輸出結(jié)果可以看出翘魄,當(dāng)回調(diào)了onDestroy()
方法后協(xié)程就不會再輸出日志了
fun main() = runBlocking {
val activity = Activity()
activity.onCreate()
delay(1000)
activity.onDestroy()
delay(1000)
}
[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
已取消的作用域無法再創(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
來實現(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")
}
}
}
[main] launchA - 0
[main] launchB - 0
[main] launchA - 1
[main] launchB - 1
[main] launchA - 2
[main] launchB - 2
2、Job
Job 是協(xié)程的句柄乘综。使用 launch
或 async
創(chuàng)建的每個協(xié)程都會返回一個 Job
實例憎账,該實例唯一標(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é)束運行
public suspend fun join()
//當(dāng) Job 結(jié)束運行時(不管由于什么原因)回調(diào)此方法,可用于接收可能存在的運行異常
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
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}")
}
[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
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>
通過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)
}
[main] 3
[main] 4070
由于 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)
}
[main] 3
[main] 7077
會造成這不同區(qū)別是因為 CoroutineStart.LAZY
不會主動啟動協(xié)程耸携,而是直到調(diào)用async.await()
或者async.satrt()
后才會啟動(即懶加載模式)棵癣,所以asyncA.await() + asyncB.await()
會導(dǎo)致兩個協(xié)程其實是在順序執(zhí)行。而默認(rèn)值 CoroutineStart.DEFAULT
參數(shù)會使得協(xié)程在聲明的同時就被啟動了(實際上還需要等待被調(diào)度執(zhí)行夺衍,但可以看做是立即就執(zhí)行了)狈谊,所以即使 async.await()
會阻塞當(dāng)前線程直到協(xié)程返回結(jié)果值,但兩個協(xié)程其實都是處于運行狀態(tài)沟沙,所以總耗時就是四秒左右
此時可以通過先調(diào)用start()
再調(diào)用await()
來實現(xiàn)第一個例子的效果
asyncA.start()
asyncB.start()
log(asyncA.await() + asyncB.await())
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()
,我們可以保證這兩項 async
操作在返回值之前完成:
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
你還可以對集合使用 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
}
雖然 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 語句雖然是運行在不同的協(xié)程上,但是其指向的 Job 其實是同個對象
fun main() = runBlocking {
val job = launch {
log("My job is ${coroutineContext[Job]}")
}
log("My job is $job")
}
[main @coroutine#1] My job is "coroutine#2":StandaloneCoroutine{Active}@75a1cd57
[main @coroutine#2] My job is "coroutine#2":StandaloneCoroutine{Active}@75a1cd57
實際上 CoroutineScope 的 isActive
這個擴(kuò)展屬性只是 coroutineContext[Job]?.isActive == true
的一種簡便寫法
public val CoroutineScope.isActive: Boolean
get() = coroutineContext[Job]?.isActive ?: true
2缎岗、CoroutineDispatcher
CoroutineContext 包含一個 CoroutineDispatcher(協(xié)程調(diào)度器)用于指定執(zhí)行協(xié)程的目標(biāo)載體静尼,即運行于哪個線程。CoroutineDispatcher 可以將協(xié)程的執(zhí)行操作限制在特定線程上传泊,也可以將其分派到線程池中鼠渺,或者讓它無限制地運行。所有的協(xié)程構(gòu)造器(如 launch 和 async)都接受一個可選參數(shù)眷细,即 CoroutineContext 系冗,該參數(shù)可用于顯式指定要創(chuàng)建的協(xié)程和其它上下文元素所要使用的 CoroutineDispatcher
要在主線程之外運行代碼,可以讓 Kotlin 協(xié)程在 Default 或 IO 調(diào)度程序上執(zhí)行工作薪鹦。在 Kotlin 中掌敬,所有協(xié)程都必須在 CoroutineDispatcher 中運行,即使它們在主線程上運行也是如此池磁。協(xié)程可以自行暫停奔害,而 CoroutineDispatcher 負(fù)責(zé)將其恢復(fù)
Kotlin 協(xié)程庫提供了四個 Dispatcher 用于指定在何處運行協(xié)程,大部分情況下我們只會接觸以下三個:
-
Dispatchers.Main - 使用此調(diào)度程序可在 Android 主線程上運行協(xié)程地熄。此調(diào)度程序只能用于與界面交互和執(zhí)行快速工作华临。示例包括調(diào)用
suspend
函數(shù)、運行 Android 界面框架操作端考,以及更新LiveData
對象 - Dispatchers.IO - 此調(diào)度程序經(jīng)過了專門優(yōu)化雅潭,適合在主線程之外執(zhí)行磁盤或網(wǎng)絡(luò) I/O揭厚。示例包括使用 Room 組件、從文件中讀取數(shù)據(jù)或向文件中寫入數(shù)據(jù)扶供,以及運行任何網(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")
}
}
[DefaultDispatcher-worker-1 @coroutine#3] Default
[DefaultDispatcher-worker-2 @coroutine#4] IO
[MyOwnThread @coroutine#5] newSingleThreadContext
[main @coroutine#2] main runBlocking
當(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īng)用程序中必須在不再需要時釋放掉,或者存儲在頂級變量中以便在整個應(yīng)用程序中進(jìn)行重用
3夯巷、withContext
對于以下代碼赛惩,get
方法內(nèi)使用withContext(Dispatchers.IO)
創(chuàng)建了一個指定在 IO 線程池中運行的代碼塊,該區(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
}
借助協(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)的等效實現(xiàn)相比符匾,withContext()
不會增加額外的開銷。此外在某些情況下瘩例,還可以優(yōu)化 withContext()
調(diào)用啊胶,使其超越基于回調(diào)的等效實現(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")
}
}
}
[main @RunBlocking#1] start
[main @Coroutine#B#4] launch B
[main @Coroutine#A#3] launch A
5观蜗、CoroutineExceptionHandler
在下文的異常處理會講到
6臊恋、組合上下文元素
有時我們需要為協(xié)程上下文定義多個元素,那就可以用 +
運算符墓捻。例如抖仅,我們可以同時為協(xié)程指定 Dispatcher 和 CoroutineName
fun main() = runBlocking<Unit> {
launch(Dispatchers.Default + CoroutineName("test")) {
log("Hello World")
}
}
[DefaultDispatcher-worker-1 @test#2] Hello World
此外,由于 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é)程運行結(jié)束渡紫。因為 cancel()
函數(shù)調(diào)用后會馬上返回而不是等待協(xié)程結(jié)束后再返回,所以此時協(xié)程不一定就是已經(jīng)停止運行了考赛。如果需要確保協(xié)程結(jié)束運行后再執(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.")
}
[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.
1集灌、協(xié)程可能無法取消
并不是所有協(xié)程都可以響應(yīng)取消操作,協(xié)程的取消操作是需要協(xié)作(cooperative)完成的,協(xié)程必須協(xié)作才能取消欣喧。協(xié)程庫中的所有掛起函數(shù)都是可取消的腌零,它們在運行時會檢查協(xié)程是否被取消了,并在取消時拋出 CancellationException 從而結(jié)束整個任務(wù)唆阿。但如果協(xié)程正在執(zhí)行計算任務(wù)并且未檢查是否已處于取消狀態(tài)的話益涧,就無法取消協(xié)程
所以即使以下代碼主動取消了協(xié)程,協(xié)程也只會在完成既定循環(huán)后才結(jié)束運行驯鳖,因為協(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.")
}
[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.
為了實現(xiàn)取消協(xié)程的目的,就需要為上述代碼加上判斷協(xié)程是否還處于可運行狀態(tài)的邏輯浅辙,當(dāng)不可運行時就主動退出協(xié)程扭弧。isActive
是 CoroutineScope 的擴(kuò)展屬性,就用于判斷協(xié)程是否還處于可運行狀態(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.")
}
取消協(xié)程這個操作類似于在 Java 中調(diào)用Thread.interrupt()
方法來向線程發(fā)起中斷請求记舆,這兩個操作都不會強(qiáng)制停止協(xié)程和線程鸽捻,外部只是相當(dāng)于發(fā)起一個停止運行的請求,需要依靠協(xié)程和線程響應(yīng)請求后主動停止運行泽腮。Kotlin 和 Java 之所以均沒有提供一個可以直接強(qiáng)制停止協(xié)程或線程的方法御蒲,是因為這個操作可能會帶來各種意想不到的情況。在停止協(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.")
}
[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.
3、NonCancellable
如果在上一個例子中的 finally
塊中再調(diào)用掛起函數(shù)的話逻恐,將會導(dǎo)致拋出 CancellationException像吻,因為此時協(xié)程已經(jīng)被取消了。通常我們并不會遇到這種情況复隆,因為常見的資源釋放操作都是非阻塞的拨匆,且不涉及任何掛起函數(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")
}
[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
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")
}
}
[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
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ù)運行蔚携,但父協(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)
}
[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
6希太、withTimeout
withTimeout
函數(shù)用于指定協(xié)程的運行超時時間,如果超時則會拋出 TimeoutCancellationException酝蜒,從而令協(xié)程結(jié)束運行
fun main() = runBlocking {
log("start")
val result = withTimeout(300) {
repeat(5) {
delay(100)
}
200
}
log(result)
log("end")
}
[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)
withTimeout
方法拋出的 TimeoutCancellationException 是 CancellationException 的子類誊辉,之前我們并未在輸出日志上看到關(guān)于 CancellationException 這類異常的堆棧信息,這是因為對于一個已取消的協(xié)程來說亡脑,CancellationException 被認(rèn)為是觸發(fā)協(xié)程結(jié)束的正常原因堕澄。但對于withTimeout
方法來說,拋出異常是其上報超時情況的一種手段霉咨,所以該異常不會被協(xié)程內(nèi)部消化掉
如果不希望因為異常導(dǎo)致協(xié)程結(jié)束蛙紫,可以改用withTimeoutOrNull
方法,如果超時就會返回 null
九途戒、異常處理
當(dāng)一個協(xié)程由于異常而運行失敗時坑傅,它會傳播這個異常并傳遞給它的父協(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()
}
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)
···
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)
}
[DefaultDispatcher-worker-2] Caught java.lang.AssertionError
2肮帐、SupervisorJob
由于異常導(dǎo)致的取消在協(xié)程中是一種雙向關(guān)系咖驮,會在整個協(xié)程層次結(jié)構(gòu)中傳播,但如果我們需要的是單向取消該怎么實現(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 來實現(xiàn)上述效果,它類似于常規(guī)的 Job耸采,唯一的區(qū)別就是取消操作只會向下傳播兴泥,一個子協(xié)程的運行失敗不會影響到其他子協(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()
}
}
[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
但是搓彻,如果異常沒有被處理且 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"
}
例如,以下 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) { /* ... */ }
}
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"
}
以下示例演示了如何使用 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)
}
}
}
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"
}
在以下示例中页屠,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)
}