android kotlin 協程(二) 基本入門2
theme: github
android kotlin 協程(二)
config:
-
system: macOS
-
android studio: 2022.1.1 Electric Eel
- gradle: gradle-7.5-bin.zip
- android build gradle: 7.1.0
- Kotlin coroutine core: 1.6.4
tips:前面幾篇全都是協程的基本使用,沒有原始碼,等後面對協程有個基本理解之後,才會簡單的分析一下原始碼!
上一篇(android kotlin coroutine 基本入門)
看完本篇你能學會什麼:
- CoroutineDispatcher // 協程排程器 用來切換執行緒
-
CoroutineName // 協程名字
-
CoroutineStart // 協程啟動模式
- CoroutineException // launch / async 捕獲異常
- GlobalCoroutineException // 全域性捕獲異常
CoroutineDispatcher 協程排程器
定義: 根據名字也可以看出來, 協程排程器, 主要用來切換執行緒,主要有4種
- Dispatchers.Main - 使用此排程程式可在 Android 主執行緒上執行協程。
- Dispatchers.IO - 此排程程式經過了專門優化,適合在主執行緒之外執行磁碟或網路 I/O。示例包括使用 Room 元件、從檔案中讀取資料或向檔案中寫入資料,以及執行任何網路操作。
- Dispatchers.Default - 此排程程式經過了專門優化,適合在主執行緒之外執行佔用大量 CPU 資源的工作。用例示例包括對列表排序和解析 JSON。
- Dispatchers.Unconfined-始終和父協程使用同一執行緒
先來看一個簡單的例子:
這行程式碼的意思是開啟一個協程,他的作用域在子執行緒上
可以看出,只要設定DIspatchers.IO 就可以切換執行緒
tips: 這裡我使用的是協程除錯才可以打印出協程編號
1.
- -Dkotlinx.coroutines.debug
使用協程DIspatcher切換執行緒的時候,需要注意的是,子協程如果排程了,就使用排程後的執行緒,如果沒有排程,始終保持和父協程相同的執行緒
這裡的排程就是指的是否有DIspatcher.XXX
例如這樣:
對於coroutine#4,他會跟隨 coroutine#3 的執行緒
coroutine#3 會 跟隨 coroutine#2 的執行緒
coroutine#2 有自身的排程器IO,所以全部都是IO執行緒
再來看一段程式碼:
withContext() 是用來切換執行緒,這裡切換到主執行緒,但是輸出的結果並沒有切換到主執行緒
withContext{} 與launch{} 排程的區別:
- withContext 在原有協程上切換執行緒
- launch 建立一個新的協程來切換執行緒
這裡我感覺是kotlin對JVM支援還不夠
因為本身JVM平臺就沒有Main執行緒,Main執行緒是對與Android平臺的
所以我們將這段程式碼拿到android平臺試一下
可以看出,可以切換,我們以android平臺為主!
這裡需要注意的是:
JVM平臺上沒有Dispatcher.Main, 因為Main只是針對android的,所以如果想要在JVM平臺上切換Main執行緒,
需要新增:
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
並且在dispatcher.Main之前呼叫 Dispatchers.setMain(Dispatchers.Unconfined)
現在我們知道了通過Dispatcher.XXX 就可以切換執行緒, 那麼Dispatcher.XXX是什麼呢? 這裡以Dispatcher.IO為例
可以看出,繼承關係為:
Dispatcher.IO = DefaultIoScheduler => ExecutorCoroutineDispatcher => CoroutineDispatcher => AbstractCoroutineContextElement => Element => CoroutineContext
最終都是 CoroutineContext 的子類!
CoroutineName 協程名字
定義:協程名字, 子協程會繼承父協程的名字, 如果協程種有自己的名字,那麼就優先使用自己的
這塊程式碼比較簡單,就不廢話了
可以看出,CoroutineName也是CoroutineContext的子類, 如果說
現在我們現在想要切換到子執行緒上我們該怎麼做?
通過剛才的程式碼,我們知道DIspatcher.XXX 其本質就是CoroutineContext, 那麼我們就可以通過內建的操作符過載來實現兩個功能的同時操作
CoroutineStart 協程啟動模式
定義: coroutineStart 用來控制協程排程器,以及協程的執行時機等
- CoroutineStart.DEFAULT: 立即根據其上下文安排協程執行;
- CoroutineStart.LAZY: 懶載入,不會立即執行,只有呼叫的時候才會執行
- CoroutineStart.ATOMIC: 常配合Job#cancel()來使用, 如果協程體中有新的掛起點,呼叫Job#cancel()時 取消掛起點之後的程式碼,否則全部取消
- CoroutineStart.UnDISPATCHED: 不進行任何排程,包括執行緒切換等, 執行緒狀態會跟隨父協程保持一致
CoroutineStart.DEFAULT 我相信不用過多贅述, 預設就是這個,直接從 CoroutineStart.LAZY開始
CoroutineStart.LAZY
首先來看一段程式碼:
可以通過這段程式碼發現, 其餘的協程都執行了,只有採用CoroutineStart.LAZY的協程沒有執行,並且runBlocking 會一直等待他執行
那麼只需要呼叫Job#start() 或者 job#join() 即可
CoroutineStart.ATOMIC
tips:該屬性目前還在試驗階段
先來看正常效果:
在這段程式碼中,我們開啟了一個協程,然後立即cancel了,協程中的程式碼沒有執行
如果改成 CoroutineStart.ATOMIC 會發生什麼情況呢?
可以驚奇的發現,居然取消協程沒有作用!
那麼這個CoroutineStart.ATOMIC到底有什麼用呢?
再來看一段程式碼:
可以看出, CoroutineStart.ATOMIC 會將掛起點之後的程式碼給cancel掉,
即使這裡delay很久,也會立即cancel
再換一種掛起點方式
也還是同樣的結果.
Coroutine.UNDISPATCHED
定義: 不進行任何排程,包括執行緒切換等, 執行緒狀態會跟隨父協程保持一致
首先還是看預設狀態
注意:這裡程式碼會首先執行:1.main start 和 2. main end
這裡有一個排程的概念,比較抽象:
協程始終都是非同步執行的,kotlin協程的底層也是執行緒, kotlin協程說白了就是一個執行緒框架,
所以建立協程的時候,其實就是建立了一個執行緒, 使用執行緒的時候,我們會通過Thread#start() 告訴JVM我們有一個任務需要執行,
然後JVM去分配,最後JVM去執行
這裡排程的大致邏輯和執行緒類似
只不過協程可以輕易的實現2個執行緒之前切換,切換回來的過程在協程中我們叫它恢復
這裡扯的有點遠,先來看本篇的內容 :)
我們來看看 Coroutine.UNDISPATCHED有什麼作用
可以看出,一旦使用了這種啟動模式, 就沒有了排程的概念,即使是切換執行緒(withContext)也無濟於事
跟隨父協程執行緒狀態而變化
說實話,這種啟動模式我認為比較雞肋,和不寫這個協程好像也沒有很大的區別
CoroutineException 協程異常捕獲
重點: 協程異常捕獲必須放在最頂層的協程作用域上
最簡單的我們通過try catch 來捕獲,這種辦法就不說了,
首先我們來看看 coroutineException的繼承關係
CoroutineExceptionHandler => AbstractCoroutineContextElement => Element => CoroutineContext
最終繼承自 CoroutineContext
到目前為止,我們知道了 coroutineContext有4個有用的子類
- Job 用來控制協程生命週期
- CoroutineDispatcher 協程排程器,用來切換執行緒
- CoroutineName 寫成名字
- CoroutineException 協程異常捕獲
首先我們來分析 CoroutineScope#launch 異常捕獲
捕獲異常之前先說一個祕密: Job不僅可以用來控制協程生命週期,還可以用不同的Job 來控制協程的異常捕獲
Job配合CoroutineHandler 異常捕獲
先來看一段簡單的程式碼:
tip: 如果不寫Job 預設就是Job()
可以看出,目前的狀態是協程1
出現錯誤之後,就會反饋給CoroutineExcetionHandler
然後協程2
就不會執行了
SupervisorJob()
假如有一個場景,我們需要某個子協程出現問題就出現問題,不應該影響到其他的子協程執行,那麼我們就可以用SupervisorJob()
SupervisorJob() 的特點就是:如果某個子協程出現問題不會影響兄弟協程
Job與 SupervisorJob 的區別也很明顯
- Job 某個協程出現問題,會直接影響兄弟協程,兄弟協程不會執行
- SupervisorJob 某個協程出現問題,不會影響兄弟協程.
如果現在場景變一下,現在換成了子協程中出現問題,來看看效果
可以看出, 子協程2
並沒有執行 這是預設效果,若在子協程中開啟多個子協程,其實建議寫法是這樣的
coroutineScope{}
為什麼要這麼寫呢? 明明我不寫效果就一樣,還得寫這玩意,不是閒的沒事麼
我感覺,作用主要就是統一程式碼,傳遞CoroutineScope 例如這樣
正常在實際開發中如果吧程式碼全寫到一坨,應該會遭到同行鄙視 :]
現在場景又調整了, 剛才是子協程出現問題立即終止子協程的兄弟協程
現在調整成了: 某個子協程出現問題,不影響子協程的兄弟協程,就想 SupervisorJob() 型別
superiverScope{}
那就請出了我們的superiverScope{}
作用域
效果很簡單
這裡主要要分清楚
SuperiverScope() 和 superiverScope{} 是不一樣的
- SuperiverScope() 是用來控制兄弟協程異常的,並且他是一個類
- superiverScope{} 是用來控制子協程的兄弟協程的,他是一個函式
async捕獲異常
重點: async使用 CoroutineExceptionHandler 是捕獲不到異常的
例如這樣:
async 的異常在Deferred#await()
中, 還記得上一篇中我們聊過 Deferred#await()
這個方法會獲取到async{} 中的返回結果
如果我們想要捕獲async{} 中的異常,我們只需要try{} catch{} await即可,例如這樣寫
async 也可以配合 SupervisorJob() 達到子協程出現問題,不影響兄弟協程執行,例如這樣:
如何讓 CoroutineExceptionHandler 監聽到async的異常,本質是監聽不到的,
但是,我們知道了deferred#await()
會丟擲異常,那麼我們可以套一層 launch{} 這樣一來就可以達到我們想要的效果
```kotlin suspend fun main() { val exceptionHandler = CoroutineExceptionHandler { _, throwable -> printlnThread("catch 到了 $throwable") } val customScope = CoroutineScope(SupervisorJob() + CoroutineName("自定義協程") + Dispatchers.IO + exceptionHandler)
val deferred1 = customScope.async {
printlnThread("子協程 1 start")
throw KotlinNullPointerException(" ============= 出錯拉 1")
"協程1執行完成"
}
val deferred2 = customScope.async {
printlnThread("子協程 2 start")
"協程2執行完成"
}
val deferred3 = customScope.async {
printlnThread("子協程 3 start")
throw KotlinNullPointerException(" ============= 出錯拉 3")
"協程3執行完成"
}
customScope.launch {
supervisorScope {
launch {
val result = deferred1.await()
println("協程1 result:$result")
}
launch {
val result = deferred2.await()
println("協程2 result:$result")
}
launch {
val result = deferred3.await()
println("協程3 result:$result")
}
}
}.join()
} ```
結果為:
子協程 3 start: thread:DefaultDispatcher-worker-2 @自定義協程#3
子協程 2 start: thread:DefaultDispatcher-worker-3 @自定義協程#2
子協程 1 start: thread:DefaultDispatcher-worker-1 @自定義協程#1
協程2 result:協程2執行完成
catch 到了 kotlin.KotlinNullPointerException: ============= 出錯拉 3: thread:DefaultDispatcher-worker-2 @自定義協程#7
catch 到了 kotlin.KotlinNullPointerException: ============= 出錯拉 1: thread:DefaultDispatcher-worker-1 @自定義協程#5
協程捕獲異常,最終要的一點就是,協程中的異常會一直向上傳遞,如果想要 使用 CoroutineExceptionHandler,監聽到異常,那麼就必須將 CoroutineExceptionHandler 配置到最頂級的coroutineScope中
GlobalCoroutineException 全域性異常捕獲
需要在本地配置一個捕獲監聽:
resources/META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler
就和APT類似,如果你玩過APT的話,肯定知道這一步是在做什麼
下一篇預告:
- 協程執行流程 [入門理解掛起與恢復]
- delay() 與 Thread#sleep() 區別
原創不易,您的點贊就是對我最大的支援!