一、什么是協(xié)程
協(xié)程是一種優(yōu)雅處理異步任務(wù)的解決方案 ,可以理解為是Kotlin對(duì)「線程」和「Handler」 API 的一種封裝形式。它可以在不同的線程間來(lái)回切換,這樣就可以讓代碼通過(guò)編寫(xiě)的順序來(lái)執(zhí)行,并且不會(huì)阻塞當(dāng)前線程,省去了在各種耗時(shí)操作寫(xiě)回調(diào)的情形。
在 JVM 平臺(tái)上,Kotlin 協(xié)程是無(wú)棧協(xié)程,所謂無(wú)棧協(xié)程,是指協(xié)程在 suspend 狀態(tài)時(shí)不需要保存調(diào)用棧。
二、suspend 關(guān)鍵字
可以看成是一種提醒,表示這是一個(gè)耗時(shí)方法,不能直接執(zhí)行,需要在協(xié)程中或者被suspend修飾的方法中調(diào)用,所以我們?cè)趯?xiě)某個(gè)耗時(shí)方法的時(shí)候需要給它加上 suspend 關(guān)鍵字,這樣可以有效避免我們?cè)谥骶€程中調(diào)用耗時(shí)操作造成應(yīng)用卡頓的情況。
比如你在普通函數(shù)中調(diào)用「suspend fun test()」這個(gè)函數(shù),IDE會(huì)提示你:

三、runBlocking、launch、async 操作符
- runBlocking
會(huì)阻塞當(dāng)前線程
println("測(cè)試開(kāi)始 ${Thread.currentThread().name}")
runBlocking {
println("延遲開(kāi)始 ${Thread.currentThread().name}")
delay(2000)
println("延遲結(jié)束")
}
println("測(cè)試結(jié)束")
/** * 結(jié)果:
* 14:46:31.965 12741-12741 I 測(cè)試開(kāi)始 main
* 14:46:31.967 12741-12741 I 延遲開(kāi)始 main
* 14:46:33.969 12741-12741 I 延遲結(jié)束
* 14:46:33.970 12741-12741 I 測(cè)試結(jié)束
*/
我們可以看到:測(cè)試的時(shí)候是主線程,runBlocking中依然是主線程,從執(zhí)行時(shí)間可以看到delay(2000)阻塞了主線程兩秒后才執(zhí)行的后邊代碼,所以這是會(huì)阻塞當(dāng)前線程的。
思考:既然如此那runBlocking到底有什么用呢?
- launch
不會(huì)阻塞當(dāng)前線程,會(huì)異步執(zhí)行
println("測(cè)試開(kāi)始 ${Thread.currentThread().name}")
GlobalScope.launch {
println("延遲開(kāi)始 ${Thread.currentThread().name}")
delay(2000)
println("延遲結(jié)束")
}
println("測(cè)試結(jié)束")
/** * 15:07:22.478 14699-14699 I 測(cè)試開(kāi)始 main
* 15:07:22.489 14699-14699 I 測(cè)試結(jié)束
* 15:07:22.490 14699-14726 I 延遲開(kāi)始 DefaultDispatcher-worker-1
* 15:07:24.498 14699-14726 I 延遲結(jié)束
*/
我們可以看到:測(cè)試的時(shí)候是主線程,但是到了 launch 中就會(huì)變成子線程,從執(zhí)行時(shí)間可以看到delay(2000) 并沒(méi)有阻塞main線程的執(zhí)行。
- async
跟 launch 相似,唯一不同的是它可以有返回值,配合await使用
println("測(cè)試1 ${Thread.currentThread().name}")
val async = GlobalScope.async {
println("延遲1 ${Thread.currentThread().name}")
delay(3000)
println("延遲2")
return@async "延遲結(jié)果666"
}
println("測(cè)試2")
GlobalScope.launch {
println("返回值1 ${Thread.currentThread().name}")
println("返回值2 " + async.await())
println("返回值3 ${Thread.currentThread().name}")
}
println("測(cè)試3")
/** * 第一次執(zhí)行結(jié)果:
* 15:27:02.188 16262-16262 I 測(cè)試1 main
* 15:27:02.198 16262-16262 I 測(cè)試2
* 15:27:02.199 16262-16262 I 測(cè)試3
* 15:27:02.199 16262-16297 I 返回值1 DefaultDispatcher-worker-2
* 15:27:02.200 16262-16296 I 延遲1 DefaultDispatcher-worker-1
* 15:27:05.207 16262-16296 I 延遲2
* 15:27:05.208 16262-16296 I 返回值2 延遲結(jié)果666
* 15:27:05.208 16262-16296 I 返回值3 DefaultDispatcher-worker-1
* 第二次執(zhí)行結(jié)果:
* 15:27:55.650 16527-16527 I 測(cè)試1 main
* 15:27:55.652 16527-16527 I 測(cè)試2
* 15:27:55.652 16527-16527 I 測(cè)試3
* 15:27:55.653 16527-16555 I 延遲1 DefaultDispatcher-worker-1
* 15:27:55.654 16527-16556 I 返回值1 DefaultDispatcher-worker-2
* 15:27:58.657 16527-16556 I 延遲2
* 15:27:58.658 16527-16556 I 返回值2 延遲結(jié)果666
* 15:27:58.658 16527-16556 I 返回值3 DefaultDispatcher-worker-2
*/
我們可以看到:
延遲1和返回值1的執(zhí)行是隨機(jī)的,也就是說(shuō)async和launch是隨機(jī)搶占執(zhí)行的。
當(dāng)執(zhí)行async.await()之后,launch會(huì)等到async的返回結(jié)果回來(lái)后才繼續(xù)向下執(zhí)行。
測(cè)試的時(shí)候是主線程,但是到了 async 中就會(huì)變成子線程。
從執(zhí)行時(shí)間可以看到delay(3000) 并沒(méi)有阻塞main線程的執(zhí)行。而且async 可以有返回值,通過(guò)它的 await 方法進(jìn)行獲取。
需要注意的是這個(gè)方法只能在協(xié)程的操作符或者被 suspend 修飾的方法中才能調(diào)用。
四、delay關(guān)鍵字
首先想一下,在 GlobalScope.launch(Dispatchers.Main) 中執(zhí)行 delay(5000) 為什么不會(huì)阻塞主線程呢?
這需要我們立即它的掛起機(jī)制和恢復(fù)機(jī)制:
- 掛起 (Suspension) 機(jī)制
掛起函數(shù) :掛起函數(shù)(如 delay)是 Kotlin 協(xié)程的核心概念。它們通過(guò) suspend 關(guān)鍵字標(biāo)記,表示這些函數(shù)可以掛起協(xié)程的執(zhí)行,而不阻塞線程。delay 的內(nèi)部實(shí)現(xiàn)并不直接使用 Thread.sleep(),因?yàn)?Thread.sleep() 會(huì)阻塞線程。而是利用協(xié)程調(diào)度器和掛起機(jī)制來(lái)實(shí)現(xiàn)延遲。
掛起操作 :
* 當(dāng)協(xié)程調(diào)用掛起函數(shù)(例如 `delay(5000)`)時(shí),它會(huì)將協(xié)程的執(zhí)行掛起,而不是線程。這意味著線程資源會(huì)被釋放,主線程可以繼續(xù)處理其他任務(wù)。
* 掛起函數(shù)會(huì)將協(xié)程的狀態(tài)和上下文保存,然后釋放線程,使得線程可以用于其他任務(wù)或協(xié)程。
掛起的含義 :掛起是指協(xié)程的執(zhí)行被暫停,線程資源被釋放,而不是線程被阻塞。協(xié)程可以在指定時(shí)間后恢復(fù)執(zhí)行,同時(shí)主線程可以繼續(xù)處理其他任務(wù)。
- 恢復(fù) (Resumption) 機(jī)制
恢復(fù)執(zhí)行 :掛起的協(xié)程會(huì)在指定的時(shí)間(如 delay 完成后)恢復(fù)執(zhí)行。協(xié)程調(diào)度器負(fù)責(zé)將掛起的協(xié)程重新調(diào)度到合適的線程上,繼續(xù)執(zhí)行掛起時(shí)之后的代碼。
掛起隊(duì)列 :在協(xié)程被掛起期間,調(diào)度器維護(hù)一個(gè)掛起隊(duì)列,用于追蹤哪些協(xié)程在等待恢復(fù)。掛起時(shí)間結(jié)束后,協(xié)程會(huì)從隊(duì)列中取出并恢復(fù)執(zhí)行。
- delay和hanler&looper機(jī)制的關(guān)系
在 Android 上,delay 函數(shù)和協(xié)程的掛起恢復(fù)機(jī)制確實(shí)依賴于 Handler 和 Looper 機(jī)制,特別是在使用 Dispatchers.Main 時(shí),這是因?yàn)橹骶€程的調(diào)度通常是通過(guò) Handler 和 Looper 來(lái)實(shí)現(xiàn)的。
當(dāng)你在 Dispatchers.Main 中調(diào)用 delay 時(shí),實(shí)際上是向主線程的消息隊(duì)列中添加了一個(gè)定時(shí)任務(wù)。
delay 的內(nèi)部通過(guò) Handler 向主線程的 Looper 發(fā)送一個(gè)延時(shí)的消息。當(dāng)延時(shí)結(jié)束時(shí),Handler 會(huì)處理這個(gè)消息,觸發(fā)協(xié)程的恢復(fù)。
以 Dispatchers.Main 上的 delay 為例,協(xié)程的掛起與恢復(fù)過(guò)程可以簡(jiǎn)化為以下步驟:
1掛起協(xié)程 :
- 當(dāng)調(diào)用
delay時(shí),協(xié)程的狀態(tài)會(huì)被保存(例如當(dāng)前的執(zhí)行位置、局部變量等)。 -
delay使用Handler.postDelayed向主線程的Looper發(fā)送一個(gè)延時(shí)消息。
2消息隊(duì)列等待 :
-
Looper在等待延時(shí)結(jié)束的同時(shí),繼續(xù)處理其他消息或任務(wù)(例如其他協(xié)程或 UI 事件)。這意味著主線程并沒(méi)有被阻塞。
3恢復(fù)協(xié)程 :
- 當(dāng)延時(shí)結(jié)束,
Handler會(huì)從消息隊(duì)列中取出延時(shí)消息并執(zhí)行。這時(shí),delay觸發(fā)協(xié)程的恢復(fù)。 - 恢復(fù)后,協(xié)程從掛起點(diǎn)繼續(xù)執(zhí)行。
五、線程調(diào)度器
- 主要作用
1、線程分配 :決定了協(xié)程在哪個(gè)線程或線程池上執(zhí)行。
2、上下文切換 :調(diào)度器在協(xié)程掛起和恢復(fù)時(shí)管理上下文切換。它負(fù)責(zé)將協(xié)程的執(zhí)行從一個(gè)線程轉(zhuǎn)移到另一個(gè)線程(如果需要),確保協(xié)程在正確的線程上執(zhí)行。
- 常見(jiàn)的調(diào)度器
1、Dispatchers.Main:在主線程中執(zhí)行。用于需要更新 UI 的任務(wù)或與 Android 的主線程相關(guān)的操作
2、Dispatchers.IO:在一個(gè)共享的 IO 線程池中執(zhí)行協(xié)程,適合執(zhí)行網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)庫(kù)操作或文件讀寫(xiě)等 IO 密集型任務(wù)。
3、Dispatchers.Default:默認(rèn)調(diào)度器,沒(méi)有設(shè)置調(diào)度器時(shí)就用這個(gè),經(jīng)過(guò)測(cè)試效果基本等同于 Dispatchers.IO,在一個(gè) CPU 密集型的線程池中執(zhí)行協(xié)程,用于計(jì)算密集型任務(wù),如復(fù)雜的算法或數(shù)據(jù)處理。
4、Dispatchers.Unconfined:無(wú)指定調(diào)度器,根據(jù)當(dāng)前執(zhí)行的環(huán)境而定,會(huì)在調(diào)用協(xié)程的線程上執(zhí)行,直到第一次掛起?;謴?fù)時(shí)的線程可能與掛起時(shí)的線程不同。不常用,適合一些特定的用途,如需要精確控制線程的場(chǎng)景。另外有一點(diǎn)需要注意,由于是直接拿當(dāng)前線程執(zhí)行,經(jīng)過(guò)實(shí)踐,協(xié)程塊中的代碼執(zhí)行過(guò)程中不會(huì)有延遲,會(huì)被立馬執(zhí)行,除非遇到需要協(xié)程被掛起了,才會(huì)去執(zhí)行協(xié)程外的代碼,這個(gè)也是跟其他類型的調(diào)度器不相同的地方
使用線程調(diào)度器可以控制協(xié)程在哪個(gè)線程上面執(zhí)行,這主要?dú)w功于 Dispatchers(調(diào)度器),如果不指定 launch 語(yǔ)句的調(diào)度器,那么它肯定是要子線程中執(zhí)行的,但是當(dāng)指定了 Dispatchers.Main 之后,它就會(huì)變成在主線程中執(zhí)行了,且不會(huì)阻塞主線程。
六、withContext操作符
- withContext是什么
withContext 是 Kotlin 協(xié)程庫(kù)中的一個(gè)重要函數(shù),用于在不同的協(xié)程上下文中切換執(zhí)行代碼塊。它允許你在協(xié)程內(nèi)切換到另一個(gè)調(diào)度器或更改協(xié)程的執(zhí)行環(huán)境,而無(wú)需手動(dòng)管理線程切換。withContext 是一個(gè)掛起函數(shù),因此它不會(huì)阻塞當(dāng)前線程。
withContext 只能在協(xié)程的操作符或者被 suspend 修飾的方法中才能調(diào)用?。?/p>
- withContext 的作用
切換協(xié)程上下文 :withContext 的主要作用是在當(dāng)前協(xié)程中切換上下文(通常是切換到另一個(gè)調(diào)度器),以便在不同的線程或線程池中執(zhí)行指定的代碼塊。
掛起并恢復(fù) :在切換到新上下文時(shí),withContext 會(huì)掛起當(dāng)前協(xié)程,將其狀態(tài)保存并暫停執(zhí)行。代碼塊執(zhí)行完畢后,協(xié)程會(huì)恢復(fù)到原來(lái)的上下文并繼續(xù)執(zhí)行。
線程切換 :withContext 常用于在不同的線程或調(diào)度器之間切換執(zhí)行環(huán)境。例如,你可以在主線程上啟動(dòng)協(xié)程,然后在 IO 線程池中執(zhí)行網(wǎng)絡(luò)請(qǐng)求,最后切換回主線程更新 UI。 - withContext 使用場(chǎng)景
IO 操作 :在主線程上啟動(dòng)協(xié)程,但在 IO 線程池中執(zhí)行耗時(shí)操作,如網(wǎng)絡(luò)請(qǐng)求或文件讀寫(xiě)。
計(jì)算密集型任務(wù) :在主線程上啟動(dòng)協(xié)程,但將復(fù)雜計(jì)算任務(wù)切換到默認(rèn)調(diào)度器(Dispatchers.Default)中執(zhí)行,以避免阻塞主線程。
線程切換 :在一個(gè)線程上執(zhí)行一部分邏輯,然后切換到另一個(gè)線程執(zhí)行另一部分邏輯。 - withContext 使用示例
println("測(cè)試開(kāi)始 ${Thread.currentThread().name}")
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
delay(2000)
println("測(cè)試是否為主線程 ${Thread.currentThread().name}")
}
println("測(cè)試延遲開(kāi)始 ${Thread.currentThread().name}")
delay(5000)
println("測(cè)試延遲結(jié)束")
}
/** * 結(jié)果:
* 15:55:08.965 8617-8617 I 測(cè)試開(kāi)始 main
* 15:55:08.965 8617-8617 I 測(cè)試結(jié)束
* 15:55:11.031 8617-8675 I 測(cè)試是否為主線程 DefaultDispatcher-worker-1
* 15:55:11.033 8617-8617 I 測(cè)試延遲開(kāi)始 main
* 15:55:16.036 8617-8617 I 測(cè)試延遲結(jié)束
*/
從打印的日志來(lái)看:
withContext 將當(dāng)前線程掛起,切換到其他線程,withContext 執(zhí)行完畢后,協(xié)程恢復(fù)到主線程,繼續(xù)執(zhí)行后續(xù)代碼。
只有當(dāng) withContext 里面的代碼執(zhí)行完了,才會(huì)恢復(fù)當(dāng)前線程的執(zhí)行。
代碼執(zhí)行順序是按照代碼的編順序來(lái)的,這也是協(xié)程的魅力所在,盡管代碼需要在不同線程上面執(zhí)行,但是線程切換的效果十分優(yōu)雅,代碼從上向下執(zhí)行。
其他用法:
suspend fun getUserName(userId: Int): String = withContext(Dispatchers.IO) {
delay(20000)
return@withContext "Hello World"
}
- withContext 的內(nèi)部原理
掛起與恢復(fù) :當(dāng) withContext 被調(diào)用時(shí),協(xié)程掛起并將當(dāng)前的上下文保存。隨后,withContext 切換到目標(biāo)上下文(如另一個(gè)調(diào)度器或線程池)并執(zhí)行代碼塊。代碼塊執(zhí)行完畢后,協(xié)程恢復(fù)到原來(lái)的上下文。
節(jié)約資源 :withContext 在切換上下文時(shí),不會(huì)創(chuàng)建新線程,而是利用已有的線程池。這種機(jī)制避免了傳統(tǒng)線程切換的開(kāi)銷,使得資源利用更加高效。 - withContext 和 launch 的區(qū)別
launch :?jiǎn)?dòng)一個(gè)新的協(xié)程,并立即返回 Job 對(duì)象,它并不會(huì)等待協(xié)程體完成。
withContext :掛起當(dāng)前協(xié)程,并在指定上下文中執(zhí)行代碼塊,等待代碼塊執(zhí)行完成后繼續(xù)執(zhí)行。 - 總結(jié)
withContext 是一種切換協(xié)程上下文的強(qiáng)大工具,使你能夠靈活地在不同線程和調(diào)度器之間切換執(zhí)行環(huán)境,避免線程阻塞并提高代碼的響應(yīng)性和性能。
常見(jiàn)用途 :適用于需要在不同線程間執(zhí)行不同任務(wù)的場(chǎng)景,特別是避免阻塞主線程的任務(wù),如 IO 操作和計(jì)算密集型任務(wù)。
七、協(xié)程的啟動(dòng)模式
總共有四種:
CoroutineStart.DEFAULT
默認(rèn)模式,會(huì)立即執(zhí)行CoroutineStart.LAZY
協(xié)程在調(diào)用 launch 或 async 時(shí)不會(huì)立即執(zhí)行,而是處于未啟動(dòng)狀態(tài)。需要顯式調(diào)用 start()、join() 或等待 async 返回的 Deferred 對(duì)象的結(jié)果時(shí),協(xié)程才會(huì)啟動(dòng)并執(zhí)行。CoroutineStart.ATOMIC
原子模式,跟 CoroutineStart.DEFAULT 類似,協(xié)程在啟動(dòng)后立即進(jìn)入執(zhí)行狀態(tài),且在進(jìn)入執(zhí)行狀態(tài)前不會(huì)響應(yīng)取消操作。也就是說(shuō),協(xié)程的啟動(dòng)和第一個(gè)掛起點(diǎn)之間的代碼將始終被執(zhí)行。需要注意的是,這是一個(gè)實(shí)驗(yàn)性的 api,后面可能會(huì)發(fā)生變更。CoroutineStart.UNDISPATCHED
未指定模式,會(huì)立即執(zhí)行協(xié)程,經(jīng)過(guò)實(shí)踐得出,會(huì)導(dǎo)致原先設(shè)置的線程調(diào)度器失效,一開(kāi)始會(huì)在原來(lái)的線程上執(zhí)行,類似于 Dispatchers.Unconfined,但是一旦協(xié)程被掛起,再恢復(fù)執(zhí)行,會(huì)變成線程調(diào)度器的設(shè)置的線程上面去執(zhí)行。使用時(shí)候要特別注意。
例子:
val job = GlobalScope.launch(Dispatchers.Default, CoroutineStart.LAZY) {
//do something
}
job.start()
八、關(guān)于Job
- job.start
通常情況下,你會(huì)在啟動(dòng)協(xié)程時(shí)隱式地創(chuàng)建并啟動(dòng)一個(gè) Job,但有時(shí)你可能希望顯式地控制 Job 的啟動(dòng)。
Job 是協(xié)程的基本構(gòu)建塊之一,表示一個(gè)可以被取消的異步任務(wù)。它有三種狀態(tài):New 、Active 和 Completed 。當(dāng)你創(chuàng)建一個(gè)協(xié)程時(shí),它會(huì)自動(dòng)啟動(dòng)并進(jìn)入 Active 狀態(tài)。
然而,如果你創(chuàng)建了一個(gè) Job 并不希望立即啟動(dòng)它,你可以在創(chuàng)建時(shí)使用 start = CoroutineStart.LAZY 參數(shù)。這會(huì)使 Job 進(jìn)入 New 狀態(tài),而不會(huì)自動(dòng)啟動(dòng)。此時(shí),你可以通過(guò)調(diào)用 job.start() 來(lái)顯式啟動(dòng)它。
- job.cancel
job.cancel() 的作用是請(qǐng)求取消與該 Job 對(duì)象關(guān)聯(lián)的協(xié)程,但是不會(huì)立馬生效 。
具體來(lái)說(shuō),當(dāng)你調(diào)用 job.cancel() 時(shí),會(huì)向協(xié)程發(fā)送一個(gè)取消信號(hào),請(qǐng)求它停止執(zhí)行。然而,這并不意味著協(xié)程會(huì)立即終止,因?yàn)閰f(xié)程的取消是協(xié)作性的。換句話說(shuō),協(xié)程必須在合適的時(shí)機(jī)檢查取消狀態(tài)并進(jìn)行響應(yīng)。通常協(xié)程會(huì)通過(guò)掛起函數(shù)(如 delay 或 yield)來(lái)檢測(cè)是否被取消,并在取消時(shí)拋出一個(gè) CancellationException。
- job.join
會(huì)掛起調(diào)用該函數(shù)的協(xié)程,直到目標(biāo)協(xié)程執(zhí)行完畢。這樣可以確保當(dāng)前協(xié)程不會(huì)繼續(xù)執(zhí)行后續(xù)的代碼,直到與Job關(guān)聯(lián)的協(xié)程已經(jīng)結(jié)束。join函數(shù)是非阻塞的,即使它會(huì)掛起當(dāng)前協(xié)程,它不會(huì)阻塞底層線程。因此,它在并發(fā)編程中非常有用,可以用來(lái)控制協(xié)程的執(zhí)行順序,確保某些任務(wù)在其他任務(wù)之前完成。
val job = launch {
// 執(zhí)行一些任務(wù)
}
job.join() // 等待job完成
println("Job已完成")
//例如:在這個(gè)例子中,println("Job已完成")只有在job協(xié)程完成后才會(huì)執(zhí)行。
- job.cancelAndJoin
是一個(gè)復(fù)合操作,首先取消關(guān)聯(lián)的協(xié)程,然后等待它完成。這一步通常用于確保協(xié)程的取消過(guò)程順利完成,并且當(dāng)前協(xié)程不會(huì)繼續(xù)執(zhí)行,直到目標(biāo)協(xié)程完全停止。即:等待協(xié)程執(zhí)行完畢然后再取消,需要在協(xié)程中使用[Suspend function 'cancelAndJoin' should be called only from a coroutine or another suspend function]
val job = launch {
// 執(zhí)行一些任務(wù)
delay(1000)
println("任務(wù)完成")
}
// 在某些條件下取消協(xié)程
job.cancelAndJoin() // 取消并等待結(jié)束
println("協(xié)程已取消并結(jié)束")
//在這個(gè)例子中,job.cancelAndJoin() 會(huì)取消job協(xié)程,并等待它的取消過(guò)程完成,之后才會(huì)執(zhí)
//行println("協(xié)程已取消并結(jié)束")。
九、關(guān)于協(xié)程的作用域
在 Kotlin 協(xié)程中,作用域(CoroutineScope) 決定了協(xié)程的生命周期、上下文以及它們的父子關(guān)系。協(xié)程作用域用于管理協(xié)程的生命周期,并確保協(xié)程能夠在適當(dāng)?shù)臅r(shí)機(jī)被取消或完成。
GlobalScope :全局協(xié)程,生命周期與應(yīng)用程序相同,不建議頻繁使用。
CoroutineScope :手動(dòng)創(chuàng)建作用域,適合靈活控制協(xié)程生命周期。
MainScope :適合 Android UI 操作的協(xié)程作用域,默認(rèn)運(yùn)行在主線程。
ViewModelScope :與 ViewModel 生命周期綁定,適合與 ViewModel 相關(guān)的異步操作。
LifecycleScope :與 Lifecycle 綁定,自動(dòng)取消與生命周期相關(guān)的協(xié)程。
SupervisorScope :子協(xié)程獨(dú)立運(yùn)行,適用于多個(gè)協(xié)程同時(shí)運(yùn)行的場(chǎng)景。