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() 區別
原創不易,您的點贊就是對我最大的支持!