Kotlin 協程基礎使用學習

原文: Kotlin 協程基礎使用學習-Stars-One的雜貨小窩

本篇閱讀可能需要以下知識,否則可能閱讀會有些困難

  • 客戶端開發基礎(Android開發或JavaFx開發)
  • Java多線程基礎
  • kotlin基礎

本文儘量以使用爲主,以代碼爲輔講解,不提及過深協程底層代碼邏輯,僅做一個基礎入門來快速上手學習(斷斷續續寫了好幾個周,若是有錯誤之處也請在評論區提出 😂)

協程優點

首先,先說下爲什麼使用協程吧

協程得和線程進行比較

  • 可在單個線程運行多個協程,其支持掛起,不會使運行協程的線程阻塞。
  • 協程可以取消
  • 協程可以讓異步代碼同步化,其本質是輕量級線程,進而可以降低異步程序的設計複雜度。

對於客戶端的網絡請求數據,以往寫法都是在回調操作裏進行更新UI操作,一旦業務複雜,且需要調用多個接口

如接口A調用完後得到的數據A需要進行拼接,從而構造成接口B的參數,去請求接口B得到數據,那麼就得在裏面瘋狂套娃,難管理且閱讀很難受

而採用協程,則將異步操作變爲同步操作,如下圖:

需要注意的是,並不是說協程比線程池進行併發任務性能更好,實際上協程內部還是使用線程調度的那一套,只不過對於開發者來說,更是黑箱操作

只是對於客戶端開發來說,可以從那種回調處理更新UI的解放出來

從性能上去看:

協程的性能並不優於線程池或者其他異步框架,主要是其做了更多語言級別步驟,但通常情況下,與其他框架的性能幾乎一致,因爲相比IO的耗時,語言級別的損耗可以幾乎忽略不計;

從設計模式去看:

協程使得開發者可以自行管理異步任務,而不同於線程的搶佔式任務,並且寫成還支持子協程的嵌套關閉、更簡便的異常處理機制等,故相比其他異步框架,協程的理念更加先進;

入門使用

依賴說明

kotlin的協程是一個單獨的庫,需要我們進行依賴後才能使用

這裏需要說明一下,協程分爲了幾個Module,需要根據情況引用(我這裏只介紹其中幾個常用的模塊,需要了解更多可以去看官方文檔說明)

  • kotlinx-coroutines-core
  • kotlinx-coroutines-core-jvm
  • kotlinx-coroutines-android
  • kotlinx-coroutines-javafx

kotlinx-coroutines-core模塊是針對多平臺項目一個公共庫,Kotlin/Native、Kotlin/JVM 和 Kotlin/JS 上使用。

kotlinx-coroutines-core-jvm是專門爲在 JVM 平臺上運行的項目設計,並提供了一些額外的功能,比如提供針對 JVM 的調度器和擴展函數。

kotlinx-coroutines-androidkotlinx-coroutines-javafx則是針對的特定的UI平臺,提供了對應的調度器,Android是Dispatcher.Main,JavaFx則是Dispatch.JavaFx(實際上也能用Dispatch.Main,與Dispatch.JavaFx等同的)

PS: 這裏如果不懂Dispatchers,沒有關係,只需要記住這個就是方便我們切換到UI線程(主線程)操作即可

像我一般是在Android平臺或者是JavaFx平臺,沒有JS和Native的需求

所以一般引用kotlinx-coroutines-core-jvm即可,會自動將kotlinx-coroutines-core也引入

之後根據平臺選擇kotlinx-coroutines-androidkotlinx-coroutines-javafx依賴

引入依賴(示例):

<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-core-jvm</artifactId>
    <version>1.8.0-RC2</version>
</dependency>

//這裏省略了對應平臺的版本依賴,參考下面gradle依賴即可

gradle引入:

implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0-RC2")


//對應平臺版本依賴,版本是一致的,如果想要切換到主線程來更新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"
}

不過需要注意的是,上面的版本僅供參考

因爲協程依賴與kotlin版本有關聯關係,如果你使用協程庫的高版本,可能kotlin也要使用較高版本,不然可能編譯會報錯

對於maven項目,修改項目使用的kotlin版本即可

對於gradle項目,除了修改kotlin版本,還得修改上面的那個plugin插件版本

不過穩妥的做法,還是根據kotlin版本選擇對應的協程版本,畢竟沒準kotlin版本一升級,整個項目就跑不起來,尤其是Android項目(kotlin版本依賴比較嚴重)

協程與kotlin版本對應關係見下表(點擊展開)
發佈時間 kotlin版本 官方推薦的協程庫版本 標準庫更新版本簡述
2020-04-15 1.3.72 1.3.8 Kotlin 1.3.70 的錯誤修復版本。
2020-08-17 1.4.0 1.3.9 具有許多功能和改進的功能版本,主要關注質量和性能。
2020-09-07 1.4.10 1.3.9 Kotlin 1.4.0 的錯誤修復版本。
2020-11-23 1.4.20 1.4.1 支持新的 JVM 功能,例如通過調用動態進行字符串連接,改進了 KMM 項目的性能和異常處理,DK 路徑的擴展:Path(“dir”) / “file.txt”
2020-12-07 1.4.21 1.4.1 Kotlin 1.4.20 的錯誤修復版本
2021-02-03 1.4.30 1.4.2 新的 JVM 後端,現在處於 Beta 版;新語言功能預覽;改進的 Kotlin/Native 性能;標準庫 API 改進
2021-02-25 1.4.31 1.4.2 Kotlin 1.4.30 的錯誤修復版本
2021-03-22 1.4.32 1.4.3 Kotlin 1.4.30 的錯誤修復版本
2021-05-05 1.5.0 1.5.0-RC 具有新語言功能、性能改進和進化性更改(例如穩定實驗性 API)的功能版本。
2021-05-24 1.5.10 1.5.0 Kotlin 1.5.0 的錯誤修復版本。
2021-06-24 1.5.20 1.5.0 默認情況下,通過 JVM 上的調用動態進行字符串連接;改進了對 Lombok 的支持和對 JSpecify 的支持;Kotlin/Native:KDoc 導出到 Objective-C 頭文件和更快的 Array.copyInto() 在一個數組中;Gradle:緩存註解處理器的類加載器並支持 --parallel Gradle 屬性;跨平臺的 stdlib 函數的對齊行爲
2021-07-13 1.5.21 1.5.0 Kotlin 1.5.20 的錯誤修復版本。
2021-08-23 1.5.30 1.5.1 JVM上註解類的實例化;改進的選擇加入要求機制和類型推斷;測試版中的 Kotlin/JS IR 後端;支持 Apple Silicon 目標;改進的 CocoaPods 支持;Gradle:Java 工具鏈支持和改進的守護程序配置;
2021-09-20 1.5.31 1.5.2 Kotlin 1.5.30 的錯誤修復版本。
2021-11-29 1.5.32 1.5.2 Kotlin 1.5.31 的錯誤修復版本。
2021-11-16 1.6.0 1.6.0 具有新語言功能、性能改進和進化性更改(例如穩定實驗性 API)的功能版本。
2021-12-14 1.6.10 1.6.0 Kotlin 1.6.0 的錯誤修復版本。
2022-04-04 1.6.20 1.6.0 具有各種改進的增量版本
2022-04-20 1.6.21 1.6.0 Kotlin 1.6.20 的錯誤修復版本。
2022-06-09 1.7.0 1.7.0 在 Alpha for JVM 中發佈了 Kotlin K2 編譯器的功能、穩定的語言功能、性能改進和演進性變化,例如穩定實驗性 API。
2022-07-07 1.7.10 1.7.0 Kotlin 1.7.0 的錯誤修復版本。
2022-09-29 1.7.20 1.7.0 具有新語言功能的增量版本,支持 Kotlin K2 編譯器中的多個編譯器插件,默認啓用新的 Kotlin/Native 內存管理器,以及對 Gradle 7.1 的支持。
2022-11-09 1.7.21 1.7.0 Kotlin 1.7.20 的錯誤修復版本。
2022-12-28 1.8.0 1.7.0 一個功能版本,改進了 kotlin-reflect 性能、新的 JVM 遞歸複製或刪除目錄內容實驗功能、改進了 Objective-C/Swift 互操作性。
2023-02-02 1.8.10 1.7.0 Kotlin 1.8.0 的錯誤修復版本。
2023-04-03 1.8.20 1.7.0 功能發佈,包括 Kotlin K2 編譯器更新、AutoCloseable 接口和 stdlib 中的 Base64 編碼、默認啓用的新 JVM 增量編譯、新的 Kotlin/Wasm 編譯器後端。
2023-04-25 1.8.21 1.7.0 Kotlin 1.8.20 的錯誤修復版本。
2023-06-08 1.8.22 1.7.0 Kotlin 1.8.20 的錯誤修復版本。
2023-07-06 1.9.0 1.7.0 包含 Kotlin K2 編譯器更新的功能版本、新的枚舉類值函數、開放式範圍的新運算符、Kotlin Multiplatform 中的 Gradle 配置緩存預覽、Kotlin Multiplatform 中的 Android 目標支持更改、Kotlin/Native 中的自定義內存分配器預覽 。

摘自:kotlin標準庫與kotlin協程相關支持庫對應關係(持續更新。。。)_kotlinx-coroutines-core和kotlin-gradle-plugin版本對應-CSDN博客

協程啓動

先來一段協程啓動的代碼

fun main() {
    runBlocking {
        val scope = this
		//啓動協程
        val job = scope.launch(Dispatchers.IO) {
            delay(1000)
            println("延遲1s後打印")
        }
        println("已啓動協程了")
    }
}

運行結果:

已啓動協程了
延遲1s後打印

協程取消

fun main() {
    runBlocking {
        val scope = this
        val job = scope.launch(Dispatchers.IO) {
            delay(1000)
            println("延遲1s後打印")
        }
        println("已啓動協程了")
		job.cancel()
		println("已取消協程")
    }
}

基礎概念

協程主要包含以下部分:

  • 協程Job:協程Job是協程的執行單元,它表示了一個協程的任務。我們可以通過Job來控制協程的啓動、取消、等待和異常處理等操作。
  • 協程構建器(Coroutine Builders):協程構建器是創建協程的入口點。在Kotlin中,常見的協程構建器有launchasyncrunBlocking等。
  • 協程作用域(Coroutine Scope):協程作用域是協程的生命週期範圍。它定義了協程的生命週期和取消操作。通常,我們會使用GlobalScopeCoroutineScope等來創建協程作用域。
  • 協程執行器(Dispatcher):協程執行器(也稱爲調度器)是協程的執行線程(或線程池)。它決定了協程在哪個線程上執行,可以通過指定不同的調度器來實現協程的併發和異步操作。
  • 協程掛起函數(Suspending Function):協程掛起函數是在協程中使用的特殊函數,它可以暫時掛起協程的執行而不阻塞線程。掛起函數使用suspend修飾符,並可調用其他掛起函數、阻塞函數、異步函數等。
  • 協程上下文(Coroutine Context):協程上下文是協程的運行環境,包含了協程的調度器(Dispatcher)和其他上下文元素,如異常處理器等。協程上下文可以由調度器、Job、異常處理器等元素組成。

其實入門的簡單使用,用的比較頻繁的還是前5個概念,最後一個協程上下文概念我可能不會花太多筆墨寫

協程上下文CoroutineContext實際是一個接口,而Job,Dispatcher都是實現了協程上下文此接口

首先,要有個概念,只要在協程作用域中才能啓動協程,而協程作用域,需要通過協程構建器來進行創建

我們來看上面的代碼

fun main() {
	//runBlocking方法實際上就是協程構建器
    runBlocking {
		//這裏的作用域實際就是協程作用域
        val scope = this
		//通過launch方法來啓動一個協程,得到一個Job對象
		//實際上,把Job對象說成協程應該就好理解了
		//注意這裏,出現了一個Dispatchers.IO,這個就是我們的協程執行器,可以看做爲一個協程提供的線程池(之後會細講)
        val job = scope.launch(Dispatchers.IO) {
			//delay是延遲執行,是協程作用域提供的一個方法
            delay(1000)
            println("延遲1s後打印")
        }
        println("已啓動協程了")
    }
}

通過上面的代碼,應該對前4個概念有些基本瞭解了,再來說說掛起函數

以上面代碼爲例,協程裏的方法太多了,想要封裝成一個方法,可以這樣改造:

fun main() {
    runBlocking {
        val scope = this
        val job = scope.launch(Dispatchers.IO) {
           test()
        }
        println("已啓動協程了")
    }

}

suspend fun test() {
    delay(1000)
    println("延遲1s後打印")
}

由於我們因爲用到了delay這個方法,所以我們得將當前方法加上一個suspend關鍵字,聲明當前函數是掛起函數

只有聲明瞭我們才能在函數裏使用delay這個方法,不加關鍵字,IDE會提示行代碼標紅,無法通過編譯

同時,還有一個概念,只有在協程作用域上,才能調用掛起函數

當然,如果你的方法裏沒有delay此類方法,可以不加suspend關鍵字聲明

協程作用域提供了不止delay這個方法,還有些其他方法,下文會進行補充

至於最後一個協程上下文,我們可以runBlocking和launch方法參數見到它的身影如下圖:

由於本文偏向使用爲主,所以不打算對協程上下文進行展開細說了

協程構建器

前面也說到了,runBlocking()可以看做爲一個協程構建器,但這個只是方便我們在main方法或者測試使用,爲什麼呢?

因爲它實際上會阻塞當前線程,如下代碼:

fun main() {
    runBlocking() {
        val scope = this
        val job = scope.launch(Dispatchers.IO) {
           delay(1000)
           println("延遲1s後打印")
        }
        println("已啓動協程了")
    }
    println("任務結束")
}

輸出結果:

已啓動協程了
延遲1s後打印
任務結束

由輸出結果可以看出,當前main方法需要等待runBlocking()方法及裏面協程執行完畢纔會執行完畢

但是像Android開發和Javafx開發,如果想上述這樣寫法,在runBlocking()進行耗時長的任務,那麼估計UI線程直接卡死,Android直接出現ANR異常了

那麼問題來了,協程提供了哪些協程構造器?

答案如下:

  • runBlocking
  • launch
  • async

runBlocking: 會創建一個新的協程同時阻塞當前線程,直到協程結束。適用於main函數和單元測試

需要注意的是,runBlocking會根據最後一行從而返回數值,類似kotlin對象的run函數,如

fun main() {
    val str = runBlocking() {
		//省略協程啓動等操作
        "hello"
    }
	//返回字符串
    println(str)
}

launch : 創建一個新的協程,不會阻塞當前線程,必須在協程作用域中才可以調用。它返回的是一個該協程任務的引用,即Job對象。這是最常用的啓動協程的方式。

async: 創建一個新的協程,不會阻塞當前線程,必須在協程作用域中才可以調用,並返回Deffer對象。可通過調用Deffer.await()方法等待該子協程執行完成並獲取結果。常用於併發執行-同步等待和獲取返回值的情況。

由於launchasync2個構造器得需要和協程作用域配合使用,所以決定在下面和協程作用域一起講解了

協程和協程作用域

協程作用域

如果在一段普通代碼想要開啓協程,除了上面說到的runBlocking方法,我們還可以通過協程作用域來調用launchasync來進行協程的啓動

可用的協程作用域有:

  • GlobalScope
  • CoroutineScope
  • supervisorScope{} 好像低版本只有方法,而高版本的協程庫則可以使用類SupervisorScope
  • MainScope 主線程協程作用域(需要引用對應平臺的依賴,如android或javafx纔會有此作用域)

其中GlobalScope是一個全局的協程作用域對象,使用的話,直接使用靜態方法來進行,如下代碼:

GlobalScope.launch { 
	//你的邏輯
}

不過這種啓動的協程存在組件被銷燬但協程還存在的情況,一般不推薦

而一般推薦使用新建一個CoroutineScope對象來啓動協程,之後在組件銷燬的生命週期手動調用cancel()方法,會將當前所有的協程任務都取消,如下代碼:


//在當前類聲明此對應(如Activity)
val scope = CoroutineScope(Dispatchers.Main)

//這裏在按鈕點擊事件裏執行
//這裏使用的協程調度器指定當前協程作用域是在主線程(UI線程)
scope.launch{
	
}

//在組件銷燬的生命週期(如Activity的onDestroy方法裏)
scope.cancel()

SupervisorScope這個協程作用域主要是針對異常的,如果子協程發生異常,則不影響父協程的運行(具體可見下文的"協程裏的異常"一章),這裏先不介紹

MainScope主要是UI主線程的協程作用域,在此作用域,相當於在主線程操作,一般我們將耗時操作切換到Dispatchers.IO去做,如下代碼:

MainScope().launch{
	withContext(Dispatchers.IO){
		//網絡請求等耗時操作
	}
	//更新UI操作
}

上面的withContext()方法也是在協程作用域才能使用的方法,目的就是切換到其他協程執行耗時操作,執行完畢後再切換回當前的協程(主線程),是個阻塞操作

如果需要根據網絡請求的結果從而來進行更新UI,可以利用withContext()的返回值,如將上述代碼改造如下:

MainScope().launch{
	val str = withContext(Dispatchers.IO){
		//網絡請求等耗時操作
		//假設得到一個字符串返回值
		"hello"
	}
	//更新UI操作
	tv.text = str
}

PS:如果對於Android平臺,還可以使用下面的2個作用域:

  • lifecycleScope:生命週期範圍,用於activity等有生命週期的組件,在DESTROYED的時候會自動結束,需要導入依賴implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
  • viewModelScope:viewModel範圍,用於ViewModel中,在ViewModel被回收時會自動結束,需要導入依賴implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

介紹完上面幾個協程作用域後,接下來對launch方法和async方法進行講解

調度器

在講解launch方法之前,先講解下調度器的種類和概念

首先,我們知道此launch方法返回job對象,之後我們可以對job對象操作,調用job.cancel()取消任務

而launch方法裏的傳參,可以傳遞一個調度器,那麼協程中有哪幾個調度器?

主要有以下幾個:

  • Dispatchers.DEFAULT
  • Dispatchers.IO
  • Dispatchers.MAIN

簡單理解調度器視爲線程就比較好理解了,比如說我們需要執行長時間的任務,就使用Dispatchers.IO調度器,而需要更改UI,則切換回主線程,如下面代碼示例:

btn.setOnClicker{
	//按鈕點擊觸發協程
	val job = CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main){
		val result = withContext(Dispatchers.IO){
			//模擬請求數據,最終得到數據
			"resp"
		}
		//根據result來進行更改UI操作(這裏已經在主線程了)
		textview.text = "result"
	}
}

就像之前所說,我們在普通代碼中使用launch,就得先創建一個協程作用域CoroutineScope,之後再啓動一個協程

CoroutineScope的構造方法需要傳一個協程調度器,這裏我們就是傳了Dispatchers.MAIN,標示此協程作用域默認是在主線程

之後我們也可以通過launch方法來切換不同的線程執行,上面代碼中,CoroutineScope和launch都有設置一個調度器

實際上,Dispatchers.MAIN是一個對象,上面代碼我們可以省略launch方法裏參數,如下代碼

btn.setOnClicker{
	//按鈕點擊觸發協程
	val job = CoroutineScope(Dispatchers.Main).launch{
		val result = withContext(Dispatchers.IO){
			//模擬請求數據,最終得到數據
			"resp"
		}
		//根據result來進行更改UI操作(這裏已經在主線程了)
		textview.text = "result"
	}
}

如果引用了Javafx的依賴,那麼這個Dispatchers.MAINDispatchers.JAVAFX是一個對象,兩者可互用

再來說說Dispatchers.IO,實際上這個是類似線程池的東西,創建的協程任務可能會被分配到不同的協程上去執行

協程實際也有有個線程池的,只不過我們使用可以不太關心,當然,如果你需要自己構建一個線程池給協程使用,也有對應方法可以設置,如下方法

// 創建一個包含多個線程的線程池
val customThreadPool = Executors.newFixedThreadPool(4).asCoroutineDispatcher()

runBlocking {
	//啓動的設置
	launch(customThreadPool) { 
		 
	}
	//或者
	withContext(customThreadPool) {
		repeat(10) {
			println("Coroutine is running on thread: ${Thread.currentThread().name}")
		}
	}
}

// 關閉線程池
(customThreadPool.executor as? Executors)?.shutdown()

asCoroutineDispatcher()方法,是協程爲傳統線程池提供的一個擴展方法,可以將線程池轉爲我們的Dispatcher進行使用(用法方面,和Dispatchers.Main這種對象使用一樣)

launch方法

其實關於launch()的使用方法,上面的例子已經介紹的七七八八了,主要是對關於launch()返回的Job對象進行講解

Job對象有以下常用屬性和方法:

  • isActive 當前協程是否已經激活

  • isCompleted 當前協程是否已經完成

  • isCancelled 當前協程是否已經取消

  • cancel() 用於Job的取消,取消協程

  • start() 用於啓動一個協程,讓其到達Active狀態

  • invokeOnCompletion() 當其完成或者異常時會調用

  • join() 阻塞並等候當前協程完成

前3個屬性很好理解,這裏直接跳過;

注意到有一個start()方法,什麼意思呢?因爲協程可以設置爲懶啓動,具體代碼如下:

val job = launch(start = CoroutineStart.LAZY) {  }
job.start()

而關於CoroutineStart類,有以下幾種選中

  • DEFAULT:默認啓動模式,表示協程會立即開始執行。(之前省略不寫,就是使用的這個選項)
  • LAZY:懶啓動模式,表示協程只有在首次被使用時纔會啓動執行。
  • ATOMIC:原子啓動模式,表示協程會盡快被執行,但可以被取消。
  • UNDISPATCHED:未調度啓動模式,表示協程會在當前調用者線程中立即執行,而不進行調度。

至於後2種,目前我沒有在具體情景使用,只是做個瞭解,不擴展進行說明了

invokeOnCompletion方法則是方便我們監聽協程完成後的操作,具體示例代碼如下:

val job = launch() {  }
job.invokeOnCompletion{
	//相關邏輯
}

這裏通過IDE的代碼提示,可以看見invokeOnCompletion方法還可以接受2個參數

  • onCancelling job被取消是否觸發當前回調,默認爲false
  • invokeImmediately 指示指定的代碼塊是否應立即調用,而不管作業狀態如何,默認爲true

上面列的幾個方法只是常用的,還有些不常用的方法,由於自己不怎麼常用,這裏就不一一來列出來了

協程併發

async方法

如果說,我們想要實現幾個協程併發進行,就可以使用此方法來開啓多個協程,如下例子

runBlocking {
	async() {
		//邏輯1
	}
	async() {
		//邏輯2
	}
}

async方法參數和launch方法是一樣的,用法方面我這裏就不多說什麼了,唯一需要注意的是,async方法返回的是一個Deffer對象(雖然它也是繼承於Job對象)

如果我們需要等待某個方法的結果的話,可以使用Deffer.await()方法來實現,如下面例子:

runBlocking {
	val deffer = async {
		delay(200)
		5 //這裏語法上是kotlin的作用域方法,返回一個int類型,如果不明白的可以自行去了解下
	}
   val result = deffer.await() // result爲Int類型,數據爲5
}

await()調用後,會使當前協程作用域進行等待,直到協程執行完畢

由於Deffer對象是繼承於Job對象,所有Job的相關方法,它也可以用,這裏參考上面說到的Job的相關方法即可

最後補充下:

如果我們需要協程併發比較多的話,可以使用一個list來裝Deffer對象,最後統一調用await()方法,代碼如下:

runBlocking {

	val list = (0..10).map {
		async {
			delay(200)
			5
		}
	}
	
	list.forEach { 
		//每個協程執行結果,做對應邏輯操作
		val result = it.await()
	}
}

不過看到某大佬的文章,提到:協程併發並不是指線程併發,

上面代碼實際也可以使用launch方法來實現併發,詳見此文Kotlin協程-協程的日常進階使用 - 掘金

父協程和子協程

還記得上面提到的協程取消方法嗎?協程取消,會同時將其有關聯的子協程全部依次取消,具體代碼:

runBlocking {
	val job1 = launch {
		val deffer = async {
		}

		val job2 = launch {  }
	}
	job1.cancel()
}

如上面示例,job1爲父協程,deffer和job2爲子協程,當父協程取消,同時deffer和job2也會取消

這裏還有一點要說明:

協程的異常是會傳遞的,比如當一個子協程發生異常時,它會影響它的兄弟協程與它的父協程。而使用了 SupervisorJob() 則意味着,其子協程的異常都將由其自己處理,而不會向外擴散,影響其他協程。

詳情文章解釋可參考此文Kotlin | 關於協程異常處理,你想知道的都在這裏 - 掘金,本文不擴展說明了

一般這樣定義一個作用域即可解決問題,代碼如下:

private val exceHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
    Log.e("tttt", "協程發生異常", throwable)
}
//調度器Dispatchers.IO根據你自己需要來即可
val gCo = CoroutineScope(SupervisorJob() + Dispatchers.IO + exceHandler)

擴展補充

傳統java接口回調如何轉協程同步寫法

之前一直有個痛點,就是用的是Java庫,裏面提供的異步操作結果都是通過接口回調的方式來返回數據的,如果我們kotlin中也是去這樣寫的話,根本就沒法體驗到協程的優勢

kotlin協程,則是提供了一個高級函數suspendCancellableCoroutine{}供我們解決上述問題

這裏以一個簡單的網絡請求爲例,有2個接口回調,分別代表請求成功和請求失敗

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>對象
	Net.post(object :RespInterface{
		override fun onSuccess(data: String) {
			it.resume(data){}
		}

		override fun onError() {
			it.resume(""){}
		}
	})
}

//在協程中調用
runBlocking {
	//result爲對應的返回結果
	val result = postNet()
}

suspendCancellableCoroutine{}返回的是CancellableContinuation對象,這裏的T類型,就是看你最終調用resume方法返回的對象類型來定義

上面我只是一個簡單的例子,如果請求失敗,則返回一個空白字符串,到時候邏輯在協程裏判斷即可

對話框按順序彈出(Android)

這個同理,也是根據上面的suspendCancellableCoroutine{}方法來實現的,就是有點麻煩,得每個對話框的方法都單獨寫

下面代碼是在Android平臺上使用的,使用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()
}

如何自定義一個協程作用域

可以直接讓我們的類實現 CoroutineScope 接口,但是我們需要指定協程的上下文,如下面代碼:

/**
 * 自定義帶協程作用域的彈窗
 */
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")
    }

    //此協程作用域的自定義 CoroutineContext
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job + CoroutineName("CenterPopupScope") + exceptionHandler


    override fun onCreate() {
        job = Job()
        super.onCreate()
    }


    override fun onDismiss() {
        job.cancel()  // 關閉彈窗後,結束所有協程任務
        YYLogUtils.w("關閉彈窗後,結束所有協程任務")
        super.onDismiss()
    }
}

上文代碼摘抄自Kotlin協程-協程的日常進階使用 - 掘金,僅供記錄方便後來查閱參考

協程常用高階函數

協程裏提供了一些函數使用,上面應該已經介紹的差不多了

//創建一個普通的CoroutineScope
coroutineScope {}

//使用SupervisorJob()創建一個CoroutineScope
supervisorScope{}

//執行一個掛起函數,如果超時,拋出TimeoutCancellationException異常!
withTimeout(time Millis: 1000){}

//執行一個掛起函數,如果超時,返回null
withTimeoutorNull(time Millis: 1000) {}

//掛起當前協程,直到協程執行完成,如果傳遞的context與當前context一致,則該函數不會掛起,相當於阻塞執行
withContext(Dispatchers.I0) {}

//一個方便的可取消的協程作用域
suspendCancellableCoroutine{}

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章