android kotlin 協程(二) 基本入門2

語言: CN / TW / HK

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-始終和父協程使用同一執行緒

官方文件介紹

先來看一個簡單的例子:

image-20230210100808380

這行程式碼的意思是開啟一個協程,他的作用域在子執行緒上

可以看出,只要設定DIspatchers.IO 就可以切換執行緒

tips: 這裡我使用的是協程除錯才可以打印出協程編號

1.image-20230210100955680

  1. -Dkotlinx.coroutines.debug

image-20230210101023349

使用協程DIspatcher切換執行緒的時候,需要注意的是,子協程如果排程了,就使用排程後的執行緒,如果沒有排程,始終保持和父協程相同的執行緒

這裡的排程就是指的是否有DIspatcher.XXX

例如這樣:

image-20230210101633250

對於coroutine#4,他會跟隨 coroutine#3 的執行緒

coroutine#3 會 跟隨 coroutine#2 的執行緒

coroutine#2 有自身的排程器IO,所以全部都是IO執行緒

再來看一段程式碼:

image-20230210103107799

withContext() 是用來切換執行緒,這裡切換到主執行緒,但是輸出的結果並沒有切換到主執行緒

withContext{} 與launch{} 排程的區別:

  • withContext 在原有協程上切換執行緒
  • launch 建立一個新的協程來切換執行緒

這裡我感覺是kotlin對JVM支援還不夠

因為本身JVM平臺就沒有Main執行緒,Main執行緒是對與Android平臺的

所以我們將這段程式碼拿到android平臺試一下

image-20230210102725891

可以看出,可以切換,我們以android平臺為主!

這裡需要注意的是:

JVM平臺上沒有Dispatcher.Main, 因為Main只是針對android的,所以如果想要在JVM平臺上切換Main執行緒,

需要新增:

implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")

並且在dispatcher.Main之前呼叫 Dispatchers.setMain(Dispatchers.Unconfined)

gitHub issues

現在我們知道了通過Dispatcher.XXX 就可以切換執行緒, 那麼Dispatcher.XXX是什麼呢? 這裡以Dispatcher.IO為例

image-20230210105911786

可以看出,繼承關係為:

Dispatcher.IO = DefaultIoScheduler => ExecutorCoroutineDispatcher => CoroutineDispatcher => AbstractCoroutineContextElement => Element => CoroutineContext

最終都是 CoroutineContext 的子類!

完整程式碼

CoroutineName 協程名字

定義:協程名字, 子協程會繼承父協程的名字, 如果協程種有自己的名字,那麼就優先使用自己的

image-20230210152847116

這塊程式碼比較簡單,就不廢話了

image-20230210162156439

可以看出,CoroutineName也是CoroutineContext的子類, 如果說

現在我們現在想要切換到子執行緒上我們該怎麼做?

通過剛才的程式碼,我們知道DIspatcher.XXX 其本質就是CoroutineContext, 那麼我們就可以通過內建的操作符過載來實現兩個功能的同時操作

image-20230210162509633

完整程式碼

CoroutineStart 協程啟動模式

定義: coroutineStart 用來控制協程排程器,以及協程的執行時機等

  • CoroutineStart.DEFAULT: 立即根據其上下文安排協程執行;
  • CoroutineStart.LAZY: 懶載入,不會立即執行,只有呼叫的時候才會執行
  • CoroutineStart.ATOMIC: 常配合Job#cancel()來使用, 如果協程體中有新的掛起點,呼叫Job#cancel()時 取消掛起點之後的程式碼,否則全部取消
  • CoroutineStart.UnDISPATCHED: 不進行任何排程,包括執行緒切換等, 執行緒狀態會跟隨父協程保持一致

官方參考

CoroutineStart.DEFAULT 我相信不用過多贅述, 預設就是這個,直接從 CoroutineStart.LAZY開始

CoroutineStart.LAZY

首先來看一段程式碼:

CoroutineStart-Lazy

可以通過這段程式碼發現, 其餘的協程都執行了,只有採用CoroutineStart.LAZY的協程沒有執行,並且runBlocking 會一直等待他執行

那麼只需要呼叫Job#start() 或者 job#join() 即可

image-20230210175105779

CoroutineStart.ATOMIC

tips:該屬性目前還在試驗階段

先來看正常效果:

image-20230210194428793

在這段程式碼中,我們開啟了一個協程,然後立即cancel了,協程中的程式碼沒有執行

如果改成 CoroutineStart.ATOMIC 會發生什麼情況呢?

image-20230210194547704

可以驚奇的發現,居然取消協程沒有作用!

那麼這個CoroutineStart.ATOMIC到底有什麼用呢?

再來看一段程式碼:

image-20230213104724981

可以看出, CoroutineStart.ATOMIC 會將掛起點之後的程式碼給cancel掉,

即使這裡delay很久,也會立即cancel

再換一種掛起點方式

image-20230213104918005

也還是同樣的結果.

Coroutine.UNDISPATCHED

定義: 不進行任何排程,包括執行緒切換等, 執行緒狀態會跟隨父協程保持一致

首先還是看預設狀態

image-20230213133925157

注意:這裡程式碼會首先執行:1.main start2. main end

這裡有一個排程的概念,比較抽象:

image-20230213134104808

協程始終都是非同步執行的,kotlin協程的底層也是執行緒, kotlin協程說白了就是一個執行緒框架,

所以建立協程的時候,其實就是建立了一個執行緒, 使用執行緒的時候,我們會通過Thread#start() 告訴JVM我們有一個任務需要執行,

然後JVM去分配,最後JVM去執行

這裡排程的大致邏輯和執行緒類似

只不過協程可以輕易的實現2個執行緒之前切換,切換回來的過程在協程中我們叫它恢復

這裡扯的有點遠,先來看本篇的內容 :)

我們來看看 Coroutine.UNDISPATCHED有什麼作用

image-20230213143444851

可以看出,一旦使用了這種啟動模式, 就沒有了排程的概念,即使是切換執行緒(withContext)也無濟於事

跟隨父協程執行緒狀態而變化

image-20230213145126614

說實話,這種啟動模式我認為比較雞肋,和不寫這個協程好像也沒有很大的區別

完整程式碼

CoroutineException 協程異常捕獲

重點: 協程異常捕獲必須放在最頂層的協程作用域上

最簡單的我們通過try catch 來捕獲,這種辦法就不說了,

首先我們來看看 coroutineException的繼承關係

image-20230213164458469

CoroutineExceptionHandler => AbstractCoroutineContextElement => Element => CoroutineContext

最終繼承自 CoroutineContext

到目前為止,我們知道了 coroutineContext有4個有用的子類

  • Job 用來控制協程生命週期
  • CoroutineDispatcher 協程排程器,用來切換執行緒
  • CoroutineName 寫成名字
  • CoroutineException 協程異常捕獲

首先我們來分析 CoroutineScope#launch 異常捕獲

捕獲異常之前先說一個祕密: Job不僅可以用來控制協程生命週期,還可以用不同的Job 來控制協程的異常捕獲

Job配合CoroutineHandler 異常捕獲

先來看一段簡單的程式碼:

tip: 如果不寫Job 預設就是Job()

image-20230213165529549

可以看出,目前的狀態是協程1出現錯誤之後,就會反饋給CoroutineExcetionHandler

然後協程2就不會執行了

SupervisorJob()

假如有一個場景,我們需要某個子協程出現問題就出現問題,不應該影響到其他的子協程執行,那麼我們就可以用SupervisorJob()

SupervisorJob() 的特點就是:如果某個子協程出現問題不會影響兄弟協程

image-20230213165914619

Job與 SupervisorJob 的區別也很明顯

  • Job 某個協程出現問題,會直接影響兄弟協程,兄弟協程不會執行
  • SupervisorJob 某個協程出現問題,不會影響兄弟協程.

如果現在場景變一下,現在換成了子協程中出現問題,來看看效果

image-20230213170604284

可以看出, 子協程2並沒有執行 這是預設效果,若在子協程中開啟多個子協程,其實建議寫法是這樣的

coroutineScope{}

image-20230213171536117

為什麼要這麼寫呢? 明明我不寫效果就一樣,還得寫這玩意,不是閒的沒事麼

我感覺,作用主要就是統一程式碼,傳遞CoroutineScope 例如這樣

image-20230213172133040

正常在實際開發中如果吧程式碼全寫到一坨,應該會遭到同行鄙視 :]

現在場景又調整了, 剛才是子協程出現問題立即終止子協程的兄弟協程

現在調整成了: 某個子協程出現問題,不影響子協程的兄弟協程,就想 SupervisorJob() 型別

superiverScope{}

那就請出了我們的superiverScope{} 作用域

image-20230213172559111

效果很簡單

這裡主要要分清楚

SuperiverScope() 和 superiverScope{} 是不一樣的

  • SuperiverScope() 是用來控制兄弟協程異常的,並且他是一個
  • superiverScope{} 是用來控制子協程的兄弟協程的,他是一個函式

async捕獲異常

重點: async使用 CoroutineExceptionHandler 是捕獲不到異常的

例如這樣:

image-20230213173928389

async 的異常在Deferred#await()中, 還記得上一篇中我們聊過 Deferred#await()這個方法會獲取到async{} 中的返回結果

如果我們想要捕獲async{} 中的異常,我們只需要try{} catch{} await即可,例如這樣寫

image-20230213174309411

async 也可以配合 SupervisorJob() 達到子協程出現問題,不影響兄弟協程執行,例如這樣:

image-20230213191750604

如何讓 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的話,肯定知道這一步是在做什麼

image-20230213194624115

完整程式碼

下一篇預告:

  • 協程執行流程 [入門理解掛起與恢復]
  • delay() 與 Thread#sleep() 區別

原創不易,您的點贊就是對我最大的支援!