原文: Kotlin 協(xié)程基礎(chǔ)使用學(xué)習(xí)-Stars-One的雜貨小窩
本篇閱讀可能需要以下知識(shí),否則可能閱讀會(huì)有些困難
- 客戶端開發(fā)基礎(chǔ)(Android開發(fā)或JavaFx開發(fā))
- Java多線程基礎(chǔ)
- kotlin基礎(chǔ)
本文盡量以使用為主,以代碼為輔講解,不提及過深協(xié)程底層代碼邏輯,僅做一個(gè)基礎(chǔ)入門來快速上手學(xué)習(xí)(斷斷續(xù)續(xù)寫了好幾個(gè)周,若是有錯(cuò)誤之處也請(qǐng)?jiān)谠u(píng)論區(qū)提出 ??)
協(xié)程優(yōu)點(diǎn)
首先,先說下為什么使用協(xié)程吧
協(xié)程得和線程進(jìn)行比較
- 可在單個(gè)線程運(yùn)行多個(gè)協(xié)程,其支持掛起,不會(huì)使運(yùn)行協(xié)程的線程阻塞。
- 協(xié)程可以取消
- 協(xié)程可以讓異步代碼同步化,其本質(zhì)是輕量級(jí)線程,進(jìn)而可以降低異步程序的設(shè)計(jì)復(fù)雜度。
對(duì)于客戶端的網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù),以往寫法都是在回調(diào)操作里進(jìn)行更新UI操作,一旦業(yè)務(wù)復(fù)雜,且需要調(diào)用多個(gè)接口
如接口A調(diào)用完后得到的數(shù)據(jù)A需要進(jìn)行拼接,從而構(gòu)造成接口B的參數(shù),去請(qǐng)求接口B得到數(shù)據(jù),那么就得在里面瘋狂套娃,難管理且閱讀很難受
而采用協(xié)程,則將異步操作變?yōu)橥讲僮?如下圖:
需要注意的是,并不是說協(xié)程比線程池進(jìn)行并發(fā)任務(wù)性能更好,實(shí)際上協(xié)程內(nèi)部還是使用線程調(diào)度的那一套,只不過對(duì)于開發(fā)者來說,更是黑箱操作
只是對(duì)于客戶端開發(fā)來說,可以從那種回調(diào)處理更新UI的解放出來
從性能上去看:
協(xié)程的性能并不優(yōu)于線程池或者其他異步框架,主要是其做了更多語言級(jí)別步驟,但通常情況下,與其他框架的性能幾乎一致,因?yàn)橄啾菼O的耗時(shí),語言級(jí)別的損耗可以幾乎忽略不計(jì);
從設(shè)計(jì)模式去看:
協(xié)程使得開發(fā)者可以自行管理異步任務(wù),而不同于線程的搶占式任務(wù),并且寫成還支持子協(xié)程的嵌套關(guān)閉、更簡(jiǎn)便的異常處理機(jī)制等,故相比其他異步框架,協(xié)程的理念更加先進(jìn);
入門使用
依賴說明
kotlin的協(xié)程是一個(gè)單獨(dú)的庫,需要我們進(jìn)行依賴后才能使用
這里需要說明一下,協(xié)程分為了幾個(gè)Module,需要根據(jù)情況引用(我這里只介紹其中幾個(gè)常用的模塊,需要了解更多可以去看官方文檔說明)
- kotlinx-coroutines-core
- kotlinx-coroutines-core-jvm
- kotlinx-coroutines-android
- kotlinx-coroutines-javafx
kotlinx-coroutines-core
模塊是針對(duì)多平臺(tái)項(xiàng)目一個(gè)公共庫,Kotlin/Native、Kotlin/JVM 和 Kotlin/JS 上使用。
kotlinx-coroutines-core-jvm
是專門為在 JVM 平臺(tái)上運(yùn)行的項(xiàng)目設(shè)計(jì),并提供了一些額外的功能,比如提供針對(duì) JVM 的調(diào)度器和擴(kuò)展函數(shù)。
而kotlinx-coroutines-android
和kotlinx-coroutines-javafx
則是針對(duì)的特定的UI平臺(tái),提供了對(duì)應(yīng)的調(diào)度器,Android是Dispatcher.Main
,JavaFx則是Dispatch.JavaFx
(實(shí)際上也能用Dispatch.Main
,與Dispatch.JavaFx
等同的)
PS: 這里如果不懂Dispatchers,沒有關(guān)系,只需要記住這個(gè)就是方便我們切換到UI線程(主線程)操作即可
像我一般是在Android平臺(tái)或者是JavaFx平臺(tái),沒有JS和Native的需求
所以一般引用kotlinx-coroutines-core-jvm
即可,會(huì)自動(dòng)將kotlinx-coroutines-core
也引入
之后根據(jù)平臺(tái)選擇kotlinx-coroutines-android
或kotlinx-coroutines-javafx
依賴
引入依賴(示例):
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core-jvm</artifactId>
<version>1.8.0-RC2</version>
</dependency>
//這里省略了對(duì)應(yīng)平臺(tái)的版本依賴,參考下面gradle依賴即可
gradle引入:
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0-RC2")
//對(duì)應(yīng)平臺(tái)版本依賴,版本是一致的,如果想要切換到主線程來更新UI操作,就需要下面的依賴
//android
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0-RC2")
//javafx
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.8.0-RC2")
//注意還得加入插件
plugins {
// For build.gradle.kts (Kotlin DSL)
kotlin("jvm") version "1.9.21"
// For build.gradle (Groovy DSL)
id "org.jetbrains.kotlin.jvm" version "1.9.21"
}
不過需要注意的是,上面的版本僅供參考
因?yàn)閰f(xié)程依賴與kotlin版本有關(guān)聯(lián)關(guān)系,如果你使用協(xié)程庫的高版本,可能kotlin也要使用較高版本,不然可能編譯會(huì)報(bào)錯(cuò)
對(duì)于maven項(xiàng)目,修改項(xiàng)目使用的kotlin版本即可
對(duì)于gradle項(xiàng)目,除了修改kotlin版本,還得修改上面的那個(gè)plugin插件版本
不過穩(wěn)妥的做法,還是根據(jù)kotlin版本選擇對(duì)應(yīng)的協(xié)程版本,畢竟沒準(zhǔn)kotlin版本一升級(jí),整個(gè)項(xiàng)目就跑不起來,尤其是Android項(xiàng)目(kotlin版本依賴比較嚴(yán)重)
協(xié)程與kotlin版本對(duì)應(yīng)關(guān)系見下表(點(diǎn)擊展開)
發(fā)布時(shí)間 | kotlin版本 | 官方推薦的協(xié)程庫版本 | 標(biāo)準(zhǔn)庫更新版本簡(jiǎn)述 |
---|---|---|---|
2020-04-15 | 1.3.72 | 1.3.8 | Kotlin 1.3.70 的錯(cuò)誤修復(fù)版本。 |
2020-08-17 | 1.4.0 | 1.3.9 | 具有許多功能和改進(jìn)的功能版本,主要關(guān)注質(zhì)量和性能。 |
2020-09-07 | 1.4.10 | 1.3.9 | Kotlin 1.4.0 的錯(cuò)誤修復(fù)版本。 |
2020-11-23 | 1.4.20 | 1.4.1 | 支持新的 JVM 功能,例如通過調(diào)用動(dòng)態(tài)進(jìn)行字符串連接,改進(jìn)了 KMM 項(xiàng)目的性能和異常處理,DK 路徑的擴(kuò)展:Path(“dir”) / “file.txt” |
2020-12-07 | 1.4.21 | 1.4.1 | Kotlin 1.4.20 的錯(cuò)誤修復(fù)版本 |
2021-02-03 | 1.4.30 | 1.4.2 | 新的 JVM 后端,現(xiàn)在處于 Beta 版;新語言功能預(yù)覽;改進(jìn)的 Kotlin/Native 性能;標(biāo)準(zhǔn)庫 API 改進(jìn) |
2021-02-25 | 1.4.31 | 1.4.2 | Kotlin 1.4.30 的錯(cuò)誤修復(fù)版本 |
2021-03-22 | 1.4.32 | 1.4.3 | Kotlin 1.4.30 的錯(cuò)誤修復(fù)版本 |
2021-05-05 | 1.5.0 | 1.5.0-RC | 具有新語言功能、性能改進(jìn)和進(jìn)化性更改(例如穩(wěn)定實(shí)驗(yàn)性 API)的功能版本。 |
2021-05-24 | 1.5.10 | 1.5.0 | Kotlin 1.5.0 的錯(cuò)誤修復(fù)版本。 |
2021-06-24 | 1.5.20 | 1.5.0 | 默認(rèn)情況下,通過 JVM 上的調(diào)用動(dòng)態(tài)進(jìn)行字符串連接;改進(jìn)了對(duì) Lombok 的支持和對(duì) JSpecify 的支持;Kotlin/Native:KDoc 導(dǎo)出到 Objective-C 頭文件和更快的 Array.copyInto() 在一個(gè)數(shù)組中;Gradle:緩存注解處理器的類加載器并支持 --parallel Gradle 屬性;跨平臺(tái)的 stdlib 函數(shù)的對(duì)齊行為 |
2021-07-13 | 1.5.21 | 1.5.0 | Kotlin 1.5.20 的錯(cuò)誤修復(fù)版本。 |
2021-08-23 | 1.5.30 | 1.5.1 | JVM上注解類的實(shí)例化;改進(jìn)的選擇加入要求機(jī)制和類型推斷;測(cè)試版中的 Kotlin/JS IR 后端;支持 Apple Silicon 目標(biāo);改進(jìn)的 CocoaPods 支持;Gradle:Java 工具鏈支持和改進(jìn)的守護(hù)程序配置; |
2021-09-20 | 1.5.31 | 1.5.2 | Kotlin 1.5.30 的錯(cuò)誤修復(fù)版本。 |
2021-11-29 | 1.5.32 | 1.5.2 | Kotlin 1.5.31 的錯(cuò)誤修復(fù)版本。 |
2021-11-16 | 1.6.0 | 1.6.0 | 具有新語言功能、性能改進(jìn)和進(jìn)化性更改(例如穩(wěn)定實(shí)驗(yàn)性 API)的功能版本。 |
2021-12-14 | 1.6.10 | 1.6.0 | Kotlin 1.6.0 的錯(cuò)誤修復(fù)版本。 |
2022-04-04 | 1.6.20 | 1.6.0 | 具有各種改進(jìn)的增量版本 |
2022-04-20 | 1.6.21 | 1.6.0 | Kotlin 1.6.20 的錯(cuò)誤修復(fù)版本。 |
2022-06-09 | 1.7.0 | 1.7.0 | 在 Alpha for JVM 中發(fā)布了 Kotlin K2 編譯器的功能、穩(wěn)定的語言功能、性能改進(jìn)和演進(jìn)性變化,例如穩(wěn)定實(shí)驗(yàn)性 API。 |
2022-07-07 | 1.7.10 | 1.7.0 | Kotlin 1.7.0 的錯(cuò)誤修復(fù)版本。 |
2022-09-29 | 1.7.20 | 1.7.0 | 具有新語言功能的增量版本,支持 Kotlin K2 編譯器中的多個(gè)編譯器插件,默認(rèn)啟用新的 Kotlin/Native 內(nèi)存管理器,以及對(duì) Gradle 7.1 的支持。 |
2022-11-09 | 1.7.21 | 1.7.0 | Kotlin 1.7.20 的錯(cuò)誤修復(fù)版本。 |
2022-12-28 | 1.8.0 | 1.7.0 | 一個(gè)功能版本,改進(jìn)了 kotlin-reflect 性能、新的 JVM 遞歸復(fù)制或刪除目錄內(nèi)容實(shí)驗(yàn)功能、改進(jìn)了 Objective-C/Swift 互操作性。 |
2023-02-02 | 1.8.10 | 1.7.0 | Kotlin 1.8.0 的錯(cuò)誤修復(fù)版本。 |
2023-04-03 | 1.8.20 | 1.7.0 | 功能發(fā)布,包括 Kotlin K2 編譯器更新、AutoCloseable 接口和 stdlib 中的 Base64 編碼、默認(rèn)啟用的新 JVM 增量編譯、新的 Kotlin/Wasm 編譯器后端。 |
2023-04-25 | 1.8.21 | 1.7.0 | Kotlin 1.8.20 的錯(cuò)誤修復(fù)版本。 |
2023-06-08 | 1.8.22 | 1.7.0 | Kotlin 1.8.20 的錯(cuò)誤修復(fù)版本。 |
2023-07-06 | 1.9.0 | 1.7.0 | 包含 Kotlin K2 編譯器更新的功能版本、新的枚舉類值函數(shù)、開放式范圍的新運(yùn)算符、Kotlin Multiplatform 中的 Gradle 配置緩存預(yù)覽、Kotlin Multiplatform 中的 Android 目標(biāo)支持更改、Kotlin/Native 中的自定義內(nèi)存分配器預(yù)覽 。 |
摘自:kotlin標(biāo)準(zhǔn)庫與kotlin協(xié)程相關(guān)支持庫對(duì)應(yīng)關(guān)系(持續(xù)更新。。。)_kotlinx-coroutines-core和kotlin-gradle-plugin版本對(duì)應(yīng)-CSDN博客
協(xié)程啟動(dòng)
先來一段協(xié)程啟動(dòng)的代碼
fun main() {
runBlocking {
val scope = this
//啟動(dòng)協(xié)程
val job = scope.launch(Dispatchers.IO) {
delay(1000)
println("延遲1s后打印")
}
println("已啟動(dòng)協(xié)程了")
}
}
運(yùn)行結(jié)果:
已啟動(dòng)協(xié)程了
延遲1s后打印
協(xié)程取消
fun main() {
runBlocking {
val scope = this
val job = scope.launch(Dispatchers.IO) {
delay(1000)
println("延遲1s后打印")
}
println("已啟動(dòng)協(xié)程了")
job.cancel()
println("已取消協(xié)程")
}
}
基礎(chǔ)概念
協(xié)程主要包含以下部分:
- 協(xié)程Job:協(xié)程Job是協(xié)程的執(zhí)行單元,它表示了一個(gè)協(xié)程的任務(wù)。我們可以通過Job來控制協(xié)程的啟動(dòng)、取消、等待和異常處理等操作。
- 協(xié)程構(gòu)建器(Coroutine Builders):協(xié)程構(gòu)建器是創(chuàng)建協(xié)程的入口點(diǎn)。在Kotlin中,常見的協(xié)程構(gòu)建器有
launch
、async
、runBlocking
等。 - 協(xié)程作用域(Coroutine Scope):協(xié)程作用域是協(xié)程的生命周期范圍。它定義了協(xié)程的生命周期和取消操作。通常,我們會(huì)使用
GlobalScope
、CoroutineScope
等來創(chuàng)建協(xié)程作用域。 - 協(xié)程執(zhí)行器(Dispatcher):協(xié)程執(zhí)行器(也稱為調(diào)度器)是協(xié)程的執(zhí)行線程(或線程池)。它決定了協(xié)程在哪個(gè)線程上執(zhí)行,可以通過指定不同的調(diào)度器來實(shí)現(xiàn)協(xié)程的并發(fā)和異步操作。
- 協(xié)程掛起函數(shù)(Suspending Function):協(xié)程掛起函數(shù)是在協(xié)程中使用的特殊函數(shù),它可以暫時(shí)掛起協(xié)程的執(zhí)行而不阻塞線程。掛起函數(shù)使用
suspend
修飾符,并可調(diào)用其他掛起函數(shù)、阻塞函數(shù)、異步函數(shù)等。 - 協(xié)程上下文(Coroutine Context):協(xié)程上下文是協(xié)程的運(yùn)行環(huán)境,包含了協(xié)程的調(diào)度器(Dispatcher)和其他上下文元素,如異常處理器等。協(xié)程上下文可以由調(diào)度器、Job、異常處理器等元素組成。
其實(shí)入門的簡(jiǎn)單使用,用的比較頻繁的還是前5個(gè)概念,最后一個(gè)協(xié)程上下文概念我可能不會(huì)花太多筆墨寫
協(xié)程上下文CoroutineContext實(shí)際是一個(gè)接口,而Job,Dispatcher都是實(shí)現(xiàn)了協(xié)程上下文此接口
首先,要有個(gè)概念,只要在協(xié)程作用域中才能啟動(dòng)協(xié)程,而協(xié)程作用域,需要通過協(xié)程構(gòu)建器來進(jìn)行創(chuàng)建
我們來看上面的代碼
fun main() {
//runBlocking方法實(shí)際上就是協(xié)程構(gòu)建器
runBlocking {
//這里的作用域?qū)嶋H就是協(xié)程作用域
val scope = this
//通過launch方法來啟動(dòng)一個(gè)協(xié)程,得到一個(gè)Job對(duì)象
//實(shí)際上,把Job對(duì)象說成協(xié)程應(yīng)該就好理解了
//注意這里,出現(xiàn)了一個(gè)Dispatchers.IO,這個(gè)就是我們的協(xié)程執(zhí)行器,可以看做為一個(gè)協(xié)程提供的線程池(之后會(huì)細(xì)講)
val job = scope.launch(Dispatchers.IO) {
//delay是延遲執(zhí)行,是協(xié)程作用域提供的一個(gè)方法
delay(1000)
println("延遲1s后打印")
}
println("已啟動(dòng)協(xié)程了")
}
}
通過上面的代碼,應(yīng)該對(duì)前4個(gè)概念有些基本了解了,再來說說掛起函數(shù)
以上面代碼為例,協(xié)程里的方法太多了,想要封裝成一個(gè)方法,可以這樣改造:
fun main() {
runBlocking {
val scope = this
val job = scope.launch(Dispatchers.IO) {
test()
}
println("已啟動(dòng)協(xié)程了")
}
}
suspend fun test() {
delay(1000)
println("延遲1s后打印")
}
由于我們因?yàn)橛玫搅薲elay這個(gè)方法,所以我們得將當(dāng)前方法加上一個(gè)suspend
關(guān)鍵字,聲明當(dāng)前函數(shù)是掛起函數(shù)
只有聲明了我們才能在函數(shù)里使用delay這個(gè)方法,不加關(guān)鍵字,IDE會(huì)提示行代碼標(biāo)紅,無法通過編譯
同時(shí),還有一個(gè)概念,只有在協(xié)程作用域上,才能調(diào)用掛起函數(shù)
當(dāng)然,如果你的方法里沒有delay此類方法,可以不加suspend關(guān)鍵字聲明
協(xié)程作用域提供了不止delay這個(gè)方法,還有些其他方法,下文會(huì)進(jìn)行補(bǔ)充
至于最后一個(gè)協(xié)程上下文,我們可以runBlocking和launch方法參數(shù)見到它的身影如下圖:
由于本文偏向使用為主,所以不打算對(duì)協(xié)程上下文進(jìn)行展開細(xì)說了
協(xié)程構(gòu)建器
前面也說到了,runBlocking()可以看做為一個(gè)協(xié)程構(gòu)建器,但這個(gè)只是方便我們?cè)趍ain方法或者測(cè)試使用,為什么呢?
因?yàn)樗鼘?shí)際上會(huì)阻塞當(dāng)前線程,如下代碼:
fun main() {
runBlocking() {
val scope = this
val job = scope.launch(Dispatchers.IO) {
delay(1000)
println("延遲1s后打印")
}
println("已啟動(dòng)協(xié)程了")
}
println("任務(wù)結(jié)束")
}
輸出結(jié)果:
已啟動(dòng)協(xié)程了
延遲1s后打印
任務(wù)結(jié)束
由輸出結(jié)果可以看出,當(dāng)前main方法需要等待runBlocking()
方法及里面協(xié)程執(zhí)行完畢才會(huì)執(zhí)行完畢
但是像Android開發(fā)和Javafx開發(fā),如果想上述這樣寫法,在runBlocking()
進(jìn)行耗時(shí)長的任務(wù),那么估計(jì)UI線程直接卡死,Android直接出現(xiàn)ANR異常了
那么問題來了,協(xié)程提供了哪些協(xié)程構(gòu)造器?
答案如下:
runBlocking
launch
async
runBlocking
: 會(huì)創(chuàng)建一個(gè)新的協(xié)程同時(shí)阻塞當(dāng)前線程,直到協(xié)程結(jié)束。適用于main函數(shù)和單元測(cè)試
需要注意的是,runBlocking會(huì)根據(jù)最后一行從而返回?cái)?shù)值,類似kotlin對(duì)象的run函數(shù),如
fun main() {
val str = runBlocking() {
//省略協(xié)程啟動(dòng)等操作
"hello"
}
//返回字符串
println(str)
}
launch
: 創(chuàng)建一個(gè)新的協(xié)程,不會(huì)阻塞當(dāng)前線程,必須在協(xié)程作用域中才可以調(diào)用。它返回的是一個(gè)該協(xié)程任務(wù)的引用,即Job對(duì)象。這是最常用的啟動(dòng)協(xié)程的方式。
async
: 創(chuàng)建一個(gè)新的協(xié)程,不會(huì)阻塞當(dāng)前線程,必須在協(xié)程作用域中才可以調(diào)用,并返回Deffer對(duì)象。可通過調(diào)用Deffer.await()方法等待該子協(xié)程執(zhí)行完成并獲取結(jié)果。常用于并發(fā)執(zhí)行-同步等待和獲取返回值的情況。
由于launch
和async
2個(gè)構(gòu)造器得需要和協(xié)程作用域配合使用,所以決定在下面和協(xié)程作用域一起講解了
協(xié)程和協(xié)程作用域
協(xié)程作用域
如果在一段普通代碼想要開啟協(xié)程,除了上面說到的runBlocking方法,我們還可以通過協(xié)程作用域來調(diào)用launch
和async
來進(jìn)行協(xié)程的啟動(dòng)
可用的協(xié)程作用域有:
GlobalScope
CoroutineScope
-
supervisorScope{}
好像低版本只有方法,而高版本的協(xié)程庫則可以使用類SupervisorScope
-
MainScope
主線程協(xié)程作用域(需要引用對(duì)應(yīng)平臺(tái)的依賴,如android或javafx才會(huì)有此作用域)
其中GlobalScope是一個(gè)全局的協(xié)程作用域?qū)ο?使用的話,直接使用靜態(tài)方法來進(jìn)行,如下代碼:
GlobalScope.launch {
//你的邏輯
}
不過這種啟動(dòng)的協(xié)程存在組件被銷毀但協(xié)程還存在的情況,一般不推薦
而一般推薦使用新建一個(gè)CoroutineScope對(duì)象來啟動(dòng)協(xié)程,之后在組件銷毀的生命周期手動(dòng)調(diào)用cancel()
方法,會(huì)將當(dāng)前所有的協(xié)程任務(wù)都取消,如下代碼:
//在當(dāng)前類聲明此對(duì)應(yīng)(如Activity)
val scope = CoroutineScope(Dispatchers.Main)
//這里在按鈕點(diǎn)擊事件里執(zhí)行
//這里使用的協(xié)程調(diào)度器指定當(dāng)前協(xié)程作用域是在主線程(UI線程)
scope.launch{
}
//在組件銷毀的生命周期(如Activity的onDestroy方法里)
scope.cancel()
SupervisorScope這個(gè)協(xié)程作用域主要是針對(duì)異常的,如果子協(xié)程發(fā)生異常,則不影響父協(xié)程的運(yùn)行(具體可見下文的"協(xié)程里的異常"一章),這里先不介紹
MainScope主要是UI主線程的協(xié)程作用域,在此作用域,相當(dāng)于在主線程操作,一般我們將耗時(shí)操作切換到Dispatchers.IO去做,如下代碼:
MainScope().launch{
withContext(Dispatchers.IO){
//網(wǎng)絡(luò)請(qǐng)求等耗時(shí)操作
}
//更新UI操作
}
上面的withContext()
方法也是在協(xié)程作用域才能使用的方法,目的就是切換到其他協(xié)程執(zhí)行耗時(shí)操作,執(zhí)行完畢后再切換回當(dāng)前的協(xié)程(主線程),是個(gè)阻塞操作
如果需要根據(jù)網(wǎng)絡(luò)請(qǐng)求的結(jié)果從而來進(jìn)行更新UI,可以利用withContext()的返回值,如將上述代碼改造如下:
MainScope().launch{
val str = withContext(Dispatchers.IO){
//網(wǎng)絡(luò)請(qǐng)求等耗時(shí)操作
//假設(shè)得到一個(gè)字符串返回值
"hello"
}
//更新UI操作
tv.text = str
}
PS:如果對(duì)于Android平臺(tái),還可以使用下面的2個(gè)作用域:
lifecycleScope
:生命周期范圍,用于activity等有生命周期的組件,在DESTROYED的時(shí)候會(huì)自動(dòng)結(jié)束,需要導(dǎo)入依賴implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
viewModelScope
:viewModel范圍,用于ViewModel中,在ViewModel被回收時(shí)會(huì)自動(dòng)結(jié)束,需要導(dǎo)入依賴implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
介紹完上面幾個(gè)協(xié)程作用域后,接下來對(duì)launch方法和async方法進(jìn)行講解
調(diào)度器
在講解launch方法之前,先講解下調(diào)度器的種類和概念
首先,我們知道此launch方法返回job對(duì)象,之后我們可以對(duì)job對(duì)象操作,調(diào)用job.cancel()
取消任務(wù)
而launch方法里的傳參,可以傳遞一個(gè)調(diào)度器,那么協(xié)程中有哪幾個(gè)調(diào)度器?
主要有以下幾個(gè):
Dispatchers.DEFAULT
Dispatchers.IO
Dispatchers.MAIN
簡(jiǎn)單理解調(diào)度器視為線程就比較好理解了,比如說我們需要執(zhí)行長時(shí)間的任務(wù),就使用Dispatchers.IO
調(diào)度器,而需要更改UI,則切換回主線程,如下面代碼示例:
btn.setOnClicker{
//按鈕點(diǎn)擊觸發(fā)協(xié)程
val job = CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main){
val result = withContext(Dispatchers.IO){
//模擬請(qǐng)求數(shù)據(jù),最終得到數(shù)據(jù)
"resp"
}
//根據(jù)result來進(jìn)行更改UI操作(這里已經(jīng)在主線程了)
textview.text = "result"
}
}
就像之前所說,我們?cè)谄胀ùa中使用launch,就得先創(chuàng)建一個(gè)協(xié)程作用域CoroutineScope,之后再啟動(dòng)一個(gè)協(xié)程
CoroutineScope的構(gòu)造方法需要傳一個(gè)協(xié)程調(diào)度器,這里我們就是傳了Dispatchers.MAIN
,標(biāo)示此協(xié)程作用域默認(rèn)是在主線程
之后我們也可以通過launch方法來切換不同的線程執(zhí)行,上面代碼中,CoroutineScope和launch都有設(shè)置一個(gè)調(diào)度器
實(shí)際上,Dispatchers.MAIN是一個(gè)對(duì)象,上面代碼我們可以省略launch方法里參數(shù),如下代碼
btn.setOnClicker{
//按鈕點(diǎn)擊觸發(fā)協(xié)程
val job = CoroutineScope(Dispatchers.Main).launch{
val result = withContext(Dispatchers.IO){
//模擬請(qǐng)求數(shù)據(jù),最終得到數(shù)據(jù)
"resp"
}
//根據(jù)result來進(jìn)行更改UI操作(這里已經(jīng)在主線程了)
textview.text = "result"
}
}
如果引用了Javafx的依賴,那么這個(gè)Dispatchers.MAIN
和Dispatchers.JAVAFX
是一個(gè)對(duì)象,兩者可互用
再來說說Dispatchers.IO
,實(shí)際上這個(gè)是類似線程池的東西,創(chuàng)建的協(xié)程任務(wù)可能會(huì)被分配到不同的協(xié)程上去執(zhí)行
協(xié)程實(shí)際也有有個(gè)線程池的,只不過我們使用可以不太關(guān)心,當(dāng)然,如果你需要自己構(gòu)建一個(gè)線程池給協(xié)程使用,也有對(duì)應(yīng)方法可以設(shè)置,如下方法
// 創(chuàng)建一個(gè)包含多個(gè)線程的線程池
val customThreadPool = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
runBlocking {
//啟動(dòng)的設(shè)置
launch(customThreadPool) {
}
//或者
withContext(customThreadPool) {
repeat(10) {
println("Coroutine is running on thread: ${Thread.currentThread().name}")
}
}
}
// 關(guān)閉線程池
(customThreadPool.executor as? Executors)?.shutdown()
asCoroutineDispatcher()
方法,是協(xié)程為傳統(tǒng)線程池提供的一個(gè)擴(kuò)展方法,可以將線程池轉(zhuǎn)為我們的Dispatcher進(jìn)行使用(用法方面,和Dispatchers.Main
這種對(duì)象使用一樣)
launch方法
其實(shí)關(guān)于launch()的使用方法,上面的例子已經(jīng)介紹的七七八八了,主要是對(duì)關(guān)于launch()返回的Job對(duì)象進(jìn)行講解
Job對(duì)象有以下常用屬性和方法:
-
isActive
當(dāng)前協(xié)程是否已經(jīng)激活 -
isCompleted
當(dāng)前協(xié)程是否已經(jīng)完成 -
isCancelled
當(dāng)前協(xié)程是否已經(jīng)取消 -
cancel()
用于Job的取消,取消協(xié)程 -
start()
用于啟動(dòng)一個(gè)協(xié)程,讓其到達(dá)Active狀態(tài) -
invokeOnCompletion()
當(dāng)其完成或者異常時(shí)會(huì)調(diào)用 -
join()
阻塞并等候當(dāng)前協(xié)程完成
前3個(gè)屬性很好理解,這里直接跳過;
注意到有一個(gè)start()方法,什么意思呢?因?yàn)閰f(xié)程可以設(shè)置為懶啟動(dòng),具體代碼如下:
val job = launch(start = CoroutineStart.LAZY) { }
job.start()
而關(guān)于CoroutineStart
類,有以下幾種選中
-
DEFAULT
:默認(rèn)啟動(dòng)模式,表示協(xié)程會(huì)立即開始執(zhí)行。(之前省略不寫,就是使用的這個(gè)選項(xiàng)) -
LAZY
:懶啟動(dòng)模式,表示協(xié)程只有在首次被使用時(shí)才會(huì)啟動(dòng)執(zhí)行。 -
ATOMIC
:原子啟動(dòng)模式,表示協(xié)程會(huì)盡快被執(zhí)行,但可以被取消。 -
UNDISPATCHED
:未調(diào)度啟動(dòng)模式,表示協(xié)程會(huì)在當(dāng)前調(diào)用者線程中立即執(zhí)行,而不進(jìn)行調(diào)度。
至于后2種,目前我沒有在具體情景使用,只是做個(gè)了解,不擴(kuò)展進(jìn)行說明了
invokeOnCompletion方法則是方便我們監(jiān)聽協(xié)程完成后的操作,具體示例代碼如下:
val job = launch() { }
job.invokeOnCompletion{
//相關(guān)邏輯
}
這里通過IDE的代碼提示,可以看見invokeOnCompletion方法還可以接受2個(gè)參數(shù)
-
onCancelling
job被取消是否觸發(fā)當(dāng)前回調(diào),默認(rèn)為false -
invokeImmediately
指示指定的代碼塊是否應(yīng)立即調(diào)用,而不管作業(yè)狀態(tài)如何,默認(rèn)為true
上面列的幾個(gè)方法只是常用的,還有些不常用的方法,由于自己不怎么常用,這里就不一一來列出來了
協(xié)程并發(fā)
async方法
如果說,我們想要實(shí)現(xiàn)幾個(gè)協(xié)程并發(fā)進(jìn)行,就可以使用此方法來開啟多個(gè)協(xié)程,如下例子
runBlocking {
async() {
//邏輯1
}
async() {
//邏輯2
}
}
async方法參數(shù)和launch方法是一樣的,用法方面我這里就不多說什么了,唯一需要注意的是,async方法返回的是一個(gè)Deffer對(duì)象(雖然它也是繼承于Job對(duì)象)
如果我們需要等待某個(gè)方法的結(jié)果的話,可以使用Deffer.await()方法來實(shí)現(xiàn),如下面例子:
runBlocking {
val deffer = async {
delay(200)
5 //這里語法上是kotlin的作用域方法,返回一個(gè)int類型,如果不明白的可以自行去了解下
}
val result = deffer.await() // result為Int類型,數(shù)據(jù)為5
}
await()
調(diào)用后,會(huì)使當(dāng)前協(xié)程作用域進(jìn)行等待,直到協(xié)程執(zhí)行完畢
由于Deffer對(duì)象是繼承于Job對(duì)象,所有Job的相關(guān)方法,它也可以用,這里參考上面說到的Job的相關(guān)方法即可
最后補(bǔ)充下:
如果我們需要協(xié)程并發(fā)比較多的話,可以使用一個(gè)list來裝Deffer對(duì)象,最后統(tǒng)一調(diào)用await()
方法,代碼如下:
runBlocking {
val list = (0..10).map {
async {
delay(200)
5
}
}
list.forEach {
//每個(gè)協(xié)程執(zhí)行結(jié)果,做對(duì)應(yīng)邏輯操作
val result = it.await()
}
}
不過看到某大佬的文章,提到:協(xié)程并發(fā)并不是指線程并發(fā),
上面代碼實(shí)際也可以使用launch方法來實(shí)現(xiàn)并發(fā),詳見此文Kotlin協(xié)程-協(xié)程的日常進(jìn)階使用 - 掘金
父協(xié)程和子協(xié)程
還記得上面提到的協(xié)程取消方法嗎?協(xié)程取消,會(huì)同時(shí)將其有關(guān)聯(lián)的子協(xié)程全部依次取消,具體代碼:
runBlocking {
val job1 = launch {
val deffer = async {
}
val job2 = launch { }
}
job1.cancel()
}
如上面示例,job1為父協(xié)程,deffer和job2為子協(xié)程,當(dāng)父協(xié)程取消,同時(shí)deffer和job2也會(huì)取消
這里還有一點(diǎn)要說明:
協(xié)程的異常是會(huì)傳遞的,比如當(dāng)一個(gè)子協(xié)程發(fā)生異常時(shí),它會(huì)影響它的兄弟協(xié)程與它的父協(xié)程。而使用了 SupervisorJob() 則意味著,其子協(xié)程的異常都將由其自己處理,而不會(huì)向外擴(kuò)散,影響其他協(xié)程。
詳情文章解釋可參考此文Kotlin | 關(guān)于協(xié)程異常處理,你想知道的都在這里 - 掘金,本文不擴(kuò)展說明了
一般這樣定義一個(gè)作用域即可解決問題,代碼如下:
private val exceHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e("tttt", "協(xié)程發(fā)生異常", throwable)
}
//調(diào)度器Dispatchers.IO根據(jù)你自己需要來即可
val gCo = CoroutineScope(SupervisorJob() + Dispatchers.IO + exceHandler)
擴(kuò)展補(bǔ)充
傳統(tǒng)java接口回調(diào)如何轉(zhuǎn)協(xié)程同步寫法
之前一直有個(gè)痛點(diǎn),就是用的是Java庫,里面提供的異步操作結(jié)果都是通過接口回調(diào)的方式來返回?cái)?shù)據(jù)的,如果我們kotlin中也是去這樣寫的話,根本就沒法體驗(yàn)到協(xié)程的優(yōu)勢(shì)
kotlin協(xié)程,則是提供了一個(gè)高級(jí)函數(shù)suspendCancellableCoroutine{}
供我們解決上述問題
這里以一個(gè)簡(jiǎn)單的網(wǎng)絡(luò)請(qǐng)求為例,有2個(gè)接口回調(diào),分別代表請(qǐng)求成功和請(qǐng)求失敗
interface RespInterface {
fun onSuccess(data:String)
fun onError()
}
Net.post(object :RespInterface{
override fun onSuccess(data: String) {
}
override fun onError() {
}
})
使用suspendCancellableCoroutine{}
改造,代碼:
suspend fun myJob() = suspendCancellableCoroutine<String> {
//下面的it代表CancellableContinuation<String>對(duì)象
Net.post(object :RespInterface{
override fun onSuccess(data: String) {
it.resume(data){}
}
override fun onError() {
it.resume(""){}
}
})
}
//在協(xié)程中調(diào)用
runBlocking {
//result為對(duì)應(yīng)的返回結(jié)果
val result = postNet()
}
suspendCancellableCoroutine{}
返回的是CancellableContinuationresume
方法返回的對(duì)象類型來定義
上面我只是一個(gè)簡(jiǎn)單的例子,如果請(qǐng)求失敗,則返回一個(gè)空白字符串,到時(shí)候邏輯在協(xié)程里判斷即可
對(duì)話框按順序彈出(Android)
這個(gè)同理,也是根據(jù)上面的suspendCancellableCoroutine{}
方法來實(shí)現(xiàn)的,就是有點(diǎn)麻煩,得每個(gè)對(duì)話框的方法都單獨(dú)寫
下面代碼是在Android平臺(tái)上使用的,使用DialogX庫的里的提示框作為示例:
suspend fun showDialog1() = suspendCancellableCoroutine<String> {
MessageDialog.show("提示1","提示1","確定")
.setOkButton { dialog, v ->
false
}
.setDialogLifecycleCallback(object :DialogLifecycleCallback<MessageDialog>(){
override fun onDismiss(dialog: MessageDialog?) {
it.resume(""){}
super.onDismiss(dialog)
}
})
}
suspend fun showDialog2() = suspendCancellableCoroutine<String> {
MessageDialog.show("提示2","提示2","確定")
.setOkButton { dialog, v ->
false
}
.setDialogLifecycleCallback(object :DialogLifecycleCallback<MessageDialog>(){
override fun onDismiss(dialog: MessageDialog?) {
it.resume(""){}
super.onDismiss(dialog)
}
})
}
//使用
lifecycleScope.launch {
showDialog1()
showDialog2()
}
如何自定義一個(gè)協(xié)程作用域
可以直接讓我們的類實(shí)現(xiàn) CoroutineScope 接口,但是我們需要指定協(xié)程的上下文,如下面代碼:
/**
* 自定義帶協(xié)程作用域的彈窗
*/
abstract class CoroutineScopeCenterPopup(activity: FragmentActivity) : CenterPopupView(activity), CoroutineScope {
private lateinit var job: Job
private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
YYLogUtils.e(throwable.message ?: "Unkown Error")
}
//此協(xié)程作用域的自定義 CoroutineContext
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job + CoroutineName("CenterPopupScope") + exceptionHandler
override fun onCreate() {
job = Job()
super.onCreate()
}
override fun onDismiss() {
job.cancel() // 關(guān)閉彈窗后,結(jié)束所有協(xié)程任務(wù)
YYLogUtils.w("關(guān)閉彈窗后,結(jié)束所有協(xié)程任務(wù)")
super.onDismiss()
}
}
上文代碼摘抄自Kotlin協(xié)程-協(xié)程的日常進(jìn)階使用 - 掘金,僅供記錄方便后來查閱參考文章來源:http://www.zghlxwxcb.cn/news/detail-841268.html
協(xié)程常用高階函數(shù)
協(xié)程里提供了一些函數(shù)使用,上面應(yīng)該已經(jīng)介紹的差不多了文章來源地址http://www.zghlxwxcb.cn/news/detail-841268.html
//創(chuàng)建一個(gè)普通的CoroutineScope
coroutineScope {}
//使用SupervisorJob()創(chuàng)建一個(gè)CoroutineScope
supervisorScope{}
//執(zhí)行一個(gè)掛起函數(shù),如果超時(shí),拋出TimeoutCancellationException異常!
withTimeout(time Millis: 1000){}
//執(zhí)行一個(gè)掛起函數(shù),如果超時(shí),返回null
withTimeoutorNull(time Millis: 1000) {}
//掛起當(dāng)前協(xié)程,直到協(xié)程執(zhí)行完成,如果傳遞的context與當(dāng)前context一致,則該函數(shù)不會(huì)掛起,相當(dāng)于阻塞執(zhí)行
withContext(Dispatchers.I0) {}
//一個(gè)方便的可取消的協(xié)程作用域
suspendCancellableCoroutine{}
參考
- kotlinx.coroutines/README.md at master · Kotlin/kotlinx.coroutines
- 協(xié)程基礎(chǔ) · Kotlin 官方文檔 中文版
- Kotlin | 關(guān)于協(xié)程異常處理,你想知道的都在這里 - 掘金
- 寫給Android工程師的協(xié)程指南 - 掘金
- Kotlin協(xié)程解析系列(上):協(xié)程調(diào)度與掛起 - 知乎
- 如何通過Kotlin協(xié)程, 簡(jiǎn)化"連續(xù)依次彈窗(Dialog隊(duì)列)"的需求 - 掘金
- Kotlin協(xié)程-CoroutineScope協(xié)程作用域 - 掘金
- Kotlin協(xié)程-協(xié)程的日常進(jìn)階使用 - 掘金
到了這里,關(guān)于Kotlin 協(xié)程基礎(chǔ)使用學(xué)習(xí)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!