kotlin中關(guān)于協(xié)程的使用詳解
一、什么是協(xié)程?
協(xié)程是Kotlin提供的一種輕量級(jí)的線程管理框架,它允許我們以同步的方式編寫異步代碼,讓代碼更加簡(jiǎn)潔易讀。與線程相比,協(xié)程的創(chuàng)建和切換開銷更小,一個(gè)應(yīng)用程序可以輕松創(chuàng)建數(shù)千個(gè)協(xié)程而不會(huì)導(dǎo)致性能問(wèn)題。
二、為什么在Android中使用協(xié)程?
- ??避免回調(diào)地獄??:以順序的方式編寫異步代碼
- ??主線程安全??:輕松切換線程,確保UI操作在主線程執(zhí)行
- ??簡(jiǎn)化錯(cuò)誤處理??:使用try-catch處理異步操作中的異常
- ??生命周期感知??:與Android組件生命周期自動(dòng)綁定
三、協(xié)程的使用
1、常用Api
先列舉下關(guān)于協(xié)程的常用的六種的Api以及對(duì)用的各種功能,如下表:
| 函數(shù) | 作用 | 啟動(dòng)協(xié)程 |
| launch { } 串行 | 啟動(dòng)無(wú)返回的協(xié)程、異常會(huì)??立即拋出??給父級(jí),導(dǎo)致整個(gè)作用域取消。 | ? |
| async { } 并行 | 啟動(dòng)有返回的協(xié)程(并行)異常只在調(diào)用 | ? |
| withContext(Dispatchers.x) { } | 一次性切換線程并返回結(jié)果 | ? |
| runBlocking { } | 阻塞當(dāng)前線程直到協(xié)程完成(不常用) | ? |
| coroutineScope { } | 子作用域,失敗時(shí)全部取消 | ? |
| supervisorScope { } | 子作用域,失敗時(shí)不影響兄弟協(xié)程 | ? |
這6種api各自有各自的功能:
- ??launch、async用于啟動(dòng)新協(xié)程的構(gòu)建器?? (真正意義上的“開啟協(xié)程”)
- ??withContext、coroutineScope、supervisorScope用于控制線程和作用的域構(gòu)建器?? (在已有協(xié)程內(nèi)劃分作用域)
- runBlocking??一個(gè)特殊的阻塞式構(gòu)建器?? (主要用于測(cè)試,非Android日常開發(fā))
2、使用示例
(1)、launch(無(wú)返回)
// 在 Activity / ViewModel 中
lifecycleScope.launch {
Log.d("TAG", "launch 開始")
delay(1000)
Log.d("TAG", "launch 結(jié)束") // 1 秒后打印
}(2)、async(有返回,并行)
suspend fun main() = coroutineScope {
val a = async { delay(800); 1 }
val b = async { delay(600); 2 }
val sum = a.await() + b.await() // 兩個(gè) delay 并行跑
println(sum) // 輸出 3
}suspend fun main() = coroutineScope {
val a = async { delay(800); 1 }
val b = async { delay(600); 2 }
// 一行等全部,返回 List<Int>
val list = awaitAll(a, b) // 并行等待,順序與入?yún)⒁恢?
println(list.sum()) // 輸出 3
}async開啟協(xié)程的方式寫了兩個(gè)示例,因?yàn)檫@里 awaitAll() 和 await() 的使用上有點(diǎn)區(qū)別
| 方式 | 代碼 | 異常傳播 | 適用場(chǎng)景 |
|---|---|---|---|
逐個(gè) await() | a.await()+b.await() | 第一個(gè)異常會(huì)阻斷第二個(gè) | 數(shù)量少 |
awaitAll() | awaitAll(a, b) | 合并異常,一次性拋出 | 數(shù)量多,更整潔 |
3.1在開啟協(xié)程時(shí)可以指定線程的作用域
launch和 async函數(shù)本身可以接受一個(gè) CoroutineContext類型的參數(shù)(通過(guò)參數(shù)指定上下文),你可以通過(guò)這個(gè)參數(shù)來(lái)??指定協(xié)程的調(diào)度器、異常處理器等??。從廣義上講,這也是在“設(shè)置”協(xié)程運(yùn)行的上下文環(huán)境。
// 在 ViewModel 的 viewModelScope 這個(gè)“父作用域”中啟動(dòng)新協(xié)程
viewModelScope.launch { // 這個(gè) launch 是 viewModelScope 的子協(xié)程
// 代碼
}
viewModelScope.async { // 這個(gè) async 也是 viewModelScope 的子協(xié)程
// 代碼
}
//指定線程
// 設(shè)置調(diào)度器:在IO線程池運(yùn)行
viewModelScope.launch(Dispatchers.IO) {
// 網(wǎng)絡(luò)請(qǐng)求等IO操作
}
// 設(shè)置異常處理器
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
viewModelScope.launch(exceptionHandler) {
// 可能會(huì)拋出異常的代碼
}
// 可以組合多個(gè)上下文元素
viewModelScope.launch(Dispatchers.Default + exceptionHandler) {
// ...
}(3)、withcontext(一次性切線程并拿結(jié)果)
withContext 可以切到 任何 Dispatcher,常用只有這3個(gè):
| Dispatcher類型 | 線程 | 使用場(chǎng)景 |
| Dispatchers.Main | 主線程(UI) | 更新界面 |
| Dispatchers.IO | 子線程池 | 網(wǎng)絡(luò) / 文件 / 數(shù)據(jù)庫(kù) |
| Dispatchers.Default | 子線程池 | CPU 密集計(jì)算 |
Dispatchers.IO和 Dispatchers.Default管理的都是子線程(后臺(tái)線程),但它們是為??完全不同類型的工作任務(wù)?而設(shè)計(jì)的,因此它們的底層線程池策略有顯著區(qū)別。
suspend fun main() {
val threadName = withContext(Dispatchers.IO) {
Thread.currentThread().name // 在 IO 線程池里執(zhí)行
}
println(threadName) // 例如:DefaultDispatcher-worker-1
}(4)、runBlocking(阻塞主線程,僅測(cè)試用)
fun main() = runBlocking {
println("start")
delay(1000)
println("end") // 整個(gè) main 會(huì)等 1 秒
}(5)、coroutineScope(子作用域,任一失敗全部取消)
suspend fun main() = coroutineScope {
launch {
delay(200)
throw RuntimeException("boom") // 異常
}
launch {
delay(1000)
println("never reach") // 被取消
}
}(6)、supervisorScope(子作用域,失敗互不影響)
suspend fun main() = supervisorScope {
launch {
delay(200)
throw RuntimeException("boom") // 兄弟不受影響
}
launch {
delay(500)
println("still alive") // 會(huì)打印
}
}- launch / async / withContext:業(yè)務(wù)代碼用的最多
- runBlocking:?jiǎn)卧獪y(cè)試時(shí)使用
- coroutineScope / supervisorScope:并發(fā)任務(wù)時(shí)控制異常
四、掛起函數(shù)
1、什么是掛起函數(shù)
說(shuō)到協(xié)程,就不得不說(shuō)提到掛起函數(shù),什么是掛起函數(shù)?
- 標(biāo)記:suspend 關(guān)鍵字。
- 能力:只能在協(xié)程或另一個(gè)掛起函數(shù)里調(diào)用。
- 本質(zhì):是一種可以被協(xié)程掛起(暫停執(zhí)行),而不會(huì)阻塞其所在線程的函數(shù)。編譯器把函數(shù)切成「狀態(tài)機(jī)」,遇到 delay()、withContext() 等掛起點(diǎn)就掛起,線程空出來(lái)干別的,等結(jié)果回來(lái)再恢復(fù)繼續(xù)執(zhí)行。
2、掛起函數(shù)是如何工作的?
掛起函數(shù)的背后是 ??狀態(tài)機(jī)?? 和 ??Continuation?? 概念。
??編譯器魔法??:當(dāng)你編譯一個(gè) suspend函數(shù)時(shí),編譯器會(huì)做額外的轉(zhuǎn)換。它會(huì)為協(xié)程體生成一個(gè)狀態(tài)機(jī)(State Machine)。每個(gè)掛起點(diǎn)(即調(diào)用另一個(gè) suspend函數(shù)的地方)都成為狀態(tài)機(jī)的一個(gè)可能狀態(tài)。
??Continuation??:可以把它理解為一個(gè)??回調(diào)對(duì)象??,它封裝了 “協(xié)程在掛起之后該如何恢復(fù)執(zhí)行” 的信息,包括它應(yīng)該從哪一行代碼繼續(xù)、當(dāng)時(shí)的局部變量是什么等等。
當(dāng)你調(diào)用 withContext(Dispatchers.IO) { ... }時(shí),實(shí)際上發(fā)生了:
- 協(xié)程在執(zhí)行到 withContext時(shí),會(huì)??掛起??。
- 它將 withContext塊內(nèi)的代碼和 Continuation(恢復(fù)信息)一起提交給協(xié)程調(diào)度器。
- 調(diào)度器安排一個(gè)線程(IO線程池中的線程)來(lái)執(zhí)行這個(gè)塊。
- 執(zhí)行完畢后,調(diào)度器再通知協(xié)程:“你交代的任務(wù)完成了”,并把結(jié)果和 Continuation一起,安排回原來(lái)的線程(或者你指定的調(diào)度器,如 Dispatchers.Main)。
- 協(xié)程根據(jù) Continuation的信息,??恢復(fù)??到掛起點(diǎn)之后的狀態(tài),繼續(xù)執(zhí)行。
上面這種描述太抽象,我們不如想象成一個(gè)快遞員(??一個(gè)線程??)在送包裹(??執(zhí)行任務(wù)??)。
??1、普通函數(shù)(阻塞)??:快遞員到了一個(gè)辦公室樓下,需要等收件人下來(lái)簽字。在收件人下來(lái)之前,他什么都不做,就干等著(??阻塞??)。這期間他沒(méi)法去送別的包裹,效率很低。
2、?回調(diào)函數(shù)(非阻塞但復(fù)雜)??:快遞員把包裹交給前臺(tái),并留下一個(gè)紙條(??回調(diào)函數(shù)??)說(shuō):“等收件人來(lái)了,打電話叫我回來(lái)簽字”。然后他就去送別的包裹了。等前臺(tái)打電話來(lái),他再回來(lái)處理。這樣效率高了,但流程變得復(fù)雜,如果包裹多,需要留很多紙條,管理起來(lái)很混亂(??回調(diào)地獄??)。
??3、掛起函數(shù)(掛起-恢復(fù))??:快遞員到了辦公室樓下,他給收件人打了個(gè)電話說(shuō):“我到了,你下來(lái)吧”。??在收件人下樓的這段時(shí)間里,他并沒(méi)有干等著,而是被派去送隔壁樓的另一個(gè)小包裹(掛起當(dāng)前任務(wù),線程去干別的事了)??。等收件人快到樓下了,快遞員也送完隔壁的小包裹回來(lái)了,然后順利簽字完成主要任務(wù)。
在這個(gè)比喻中:
- ??快遞員??:就是一個(gè)線程。
- ??送主要包裹??:就是執(zhí)行協(xié)程體里的代碼。
- ??打電話讓收件人下樓??:就是調(diào)用一個(gè)
suspend函數(shù)(比如 delay, withContext, 或者你的自定義掛起函數(shù))。 - ??去送隔壁的小包裹??:線程被釋放,可以去執(zhí)行其他任務(wù)(可能是其他協(xié)程的任務(wù))。
- ??收件人下樓完成,快遞員回來(lái)簽字??:掛起的條件滿足,協(xié)程在(可能是原來(lái)的,也可能是另一個(gè))線程上??恢復(fù)??,繼續(xù)執(zhí)行后面的代碼。
??關(guān)鍵點(diǎn):??
1、掛起函數(shù)不會(huì)阻塞線程,而是釋放線程去干別的活,等它等待的操作(如網(wǎng)絡(luò)請(qǐng)求、磁盤IO、延遲)完成后,協(xié)程會(huì)在合適的時(shí)機(jī)和線程上??恢復(fù)??執(zhí)行。
2、掛起函數(shù)本身不指定線程??:suspend關(guān)鍵字只是一個(gè)標(biāo)記,告訴編譯器這個(gè)函數(shù)可以在協(xié)程中使用并可能掛起。它本身并不包含任何線程信息。線程由調(diào)度器(Dispatcher)決定??:真正決定代碼在哪個(gè)線程上運(yùn)行的是協(xié)程的上下文中的 CoroutineDispatcher(協(xié)程調(diào)度器)??(Dispatchers.Main / IO / Default)。
3、掛起函數(shù)的作用域不一定在子線程中。它的執(zhí)行線程完全取決于它在被調(diào)用時(shí)所在的協(xié)程上下文(CoroutineContext),以及它內(nèi)部使用的調(diào)度器(Dispatcher)。?掛起函數(shù)的核心是“掛起”(suspend),而不是“切換線程”。線程切換只是實(shí)現(xiàn)掛起的一種常用手段。
3、掛起函數(shù)的使用場(chǎng)景
(1)情況一:在主線程啟動(dòng),并在主線程調(diào)用掛起函數(shù)
viewModelScope.launch(Dispatchers.Main) { // 1. 在主線程啟動(dòng)協(xié)程
// 2. 當(dāng)前上下文是 Dispatchers.Main
doSomeWork() // 3. 調(diào)用掛起函數(shù)
}
// 這個(gè)掛起函數(shù)沒(méi)有使用 withContext 切換線程
// 因此它將繼承調(diào)用者的上下文,即在主線程運(yùn)行
suspend fun doSomeWork() {
// 這里的代碼會(huì)在 Dispatchers.Main 上執(zhí)行
// 如果在這里執(zhí)行耗時(shí)操作,會(huì)阻塞主線程!
heavyOperation() // ? 危險(xiǎn)!會(huì)阻塞UI!
}
fun heavyOperation() {
Thread.sleep(2000) // 模擬耗時(shí)阻塞操作
}這是最常見的Android場(chǎng)景。協(xié)程在主線程啟動(dòng),掛起函數(shù)內(nèi)部沒(méi)有切換上下文,那么它就會(huì)在主線程運(yùn)行。在這個(gè)例子中,掛起函數(shù) doSomeWork()的作用域是主線程。
(2)情況二:正確的“主線程安全”掛起函數(shù)
viewModelScope.launch(Dispatchers.Main) { // 1. 在主線程啟動(dòng)協(xié)程
// 2. 當(dāng)前上下文是 Dispatchers.Main
val result = doSomeSafeWork() // 3. 調(diào)用掛起函數(shù)(掛起點(diǎn))
updateUI(result) // 6. 恢復(fù)后,仍在主線程,安全更新UI
}
// 這是一個(gè)主線程安全的掛起函數(shù)
suspend fun doSomeSafeWork(): String {
// 4. 函數(shù)開始執(zhí)行時(shí),仍在主線程
// 但 withContext 會(huì)將協(xié)程的執(zhí)行掛起,并將代碼塊交給 IO 調(diào)度器
return withContext(Dispatchers.IO) {
// 5. 這個(gè)代碼塊現(xiàn)在在IO線程池中的某個(gè)線程執(zhí)行
// 模擬網(wǎng)絡(luò)請(qǐng)求或數(shù)據(jù)庫(kù)操作,不會(huì)阻塞主線程
Thread.sleep(2000)
"Result from network"
}
// withContext 完成后,協(xié)程會(huì)自動(dòng)切回原來(lái)的上下文(Dispatchers.Main)
// 所以返回值是在主線程被接收的
}一個(gè)良好的掛起函數(shù)應(yīng)該內(nèi)部處理線程切換,保證無(wú)論從哪個(gè)線程調(diào)用它,其耗時(shí)操作都在后臺(tái)進(jìn)行,并最終將結(jié)果返回給調(diào)用方線程。
doSomeSafeWork() 函數(shù)??內(nèi)部??的 withContext 代碼塊的作用域是子線程(IO線程)。但從外部看,這個(gè)函數(shù)??被調(diào)用和返回??的上下文(viewModelScope通常是 Dispatchers.Main)是主線程。
(3)情況三:在子線程啟動(dòng)協(xié)程
viewModelScope.launch(Dispatchers.Default) { // 1. 在Default線程池啟動(dòng)
// 2. 當(dāng)前上下文是 Dispatchers.Default
doSomeWork() // 3. 這個(gè)掛起函數(shù)將在 Default 線程執(zhí)行
}
suspend fun doSomeWork() {
// 在 Dispatchers.Default 上執(zhí)行
}如果明確指定一個(gè)后臺(tái)調(diào)度器啟動(dòng)協(xié)程,那么掛起函數(shù)(如果不內(nèi)部切換)就會(huì)在那個(gè)后臺(tái)線程運(yùn)行。
(4)總結(jié)
場(chǎng)景 | 掛起函數(shù)所在線程 | 說(shuō)明 |
|---|---|---|
??默認(rèn)情況?? | ??繼承調(diào)用方協(xié)程的上下文?? | 掛起函數(shù)不自動(dòng)切換線程,它在哪個(gè)線程被調(diào)用,就在哪個(gè)線程運(yùn)行。 |
??使用 | ??由 | 這是??主動(dòng)控制??掛起函數(shù)內(nèi)部代碼執(zhí)行線程的標(biāo)準(zhǔn)方式。 |
??設(shè)計(jì)目標(biāo)?? | ??實(shí)現(xiàn)主線程安全?? | 一個(gè)好的掛起函數(shù)應(yīng)該內(nèi)部使用 |
??掛起函數(shù)的作用域不一定在子線程中。它的線程環(huán)境是??動(dòng)態(tài)的??和??可預(yù)測(cè)的??。
??動(dòng)態(tài)的??:取決于調(diào)用它的協(xié)程上下文和它內(nèi)部使用的調(diào)度器。
可預(yù)測(cè)的??:開發(fā)者可以通過(guò) withContext精確地控制其內(nèi)部代碼應(yīng)該在哪個(gè)線程上執(zhí)行。
??注意:??
1、永遠(yuǎn)不要假設(shè)一個(gè)掛起函數(shù)會(huì)在后臺(tái)線程運(yùn)行。如果要執(zhí)行耗時(shí)操作,??必須??在掛起函數(shù)內(nèi)部使用 withContext(Dispatchers.IO)或 withContext(Dispatchers.Default)來(lái)明確切換到合適的線程。這才是編寫“主線程安全”掛起函數(shù)的關(guān)鍵。
2、結(jié)構(gòu)化并發(fā)(Structured Concurrency),這是協(xié)程設(shè)計(jì)的核心哲學(xué),要求協(xié)程的生命周期與它的啟動(dòng)作用域(如 ViewModel的 viewModelScope或 Activity的 lifecycleScope)綁定。
到此這篇關(guān)于kotlin中關(guān)于協(xié)程的使用的文章就介紹到這了,更多相關(guān)kotlin協(xié)程使用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Android開發(fā)中Activity的四種launchMode
這篇文章主要介紹了Android開發(fā)中Activity的四種launchMode,launchMode主要用于控制多個(gè)Activity間的跳轉(zhuǎn),需要的朋友可以參考下2016-03-03
Android Activity與Intent詳解及示例代碼
本文主要講解Android Activity與Intent的知識(shí),這里整理了相關(guān)資料并附有示例代碼,有興趣的小伙伴可以參考下2016-08-08
Android使用surfaceView自定義抽獎(jiǎng)大轉(zhuǎn)盤
這篇文章主要為大家詳細(xì)介紹了Android使用surfaceView自定義抽獎(jiǎng)大轉(zhuǎn)盤,熟練掌握SurfaceVie實(shí)現(xiàn)抽獎(jiǎng)大轉(zhuǎn)盤,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12
Android自定義viewgroup可滾動(dòng)布局 GestureDetector手勢(shì)監(jiān)聽(5)
這篇文章主要為大家詳細(xì)介紹了Android自定義viewgroup可滾動(dòng)布局,GestureDetector手勢(shì)監(jiān)聽,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12
Android EditText默認(rèn)不彈出輸入法的實(shí)現(xiàn)方法
下面小編就為大家分享一篇Android EditText默認(rèn)不彈出輸入法的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-01-01
Android中快速便捷的實(shí)現(xiàn)圓角按鈕方法詳解
圓角按鈕在我們現(xiàn)在的界面中常常會(huì)用到,最近在開發(fā)中就又遇到了,所以想著有沒(méi)有更快速更便捷的實(shí)現(xiàn)方法呢,所以就有了這篇文章,本文主要給大家介紹了關(guān)于Android中如何快速便捷的實(shí)現(xiàn)圓角按鈕的相關(guān)資料,需要的朋友可以參考下。2017-05-05
Android IPC機(jī)制利用Messenger實(shí)現(xiàn)跨進(jìn)程通信
這篇文章主要介紹了Android IPC機(jī)制中 Messager 實(shí)現(xiàn)跨進(jìn)程通信的知識(shí),對(duì)Android學(xué)習(xí)通信知識(shí)非常重要,需要的同學(xué)可以參考下2016-07-07
Android實(shí)現(xiàn)獲取驗(yàn)證碼倒計(jì)時(shí)功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)獲取驗(yàn)證碼倒計(jì)時(shí)功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12

