android kotlin 協程(一) 基礎入門

語言: 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

前言:最近系統的學習了一遍協程, 計劃通過10篇左右blog來記錄一下我對協程的理解, 從最簡單的 runBlocking開始; 到最後 suspend和continuation的關係等等

tips:前面幾篇全都是協程的基本使用,沒有源碼,等後面對協程有個基本理解之後,才會簡單的分析一下源碼!

學習我這個系列的協程, 只需要記住一點, suspend函數 永遠不會阻塞main線程執行! 永遠是異步的!

看完本篇你將會學到哪些知識:

  • runBlocking()
  • CoroutineScope#launch()
  • CoroutineScope#async()
  • Job的常用方法
  • 協程狀態[isActive,isCancelled,isCompleted]

runBlocking

定義: runBlocking 會阻塞線程來等待自己子協程執行完, 並且對於不是子協程的作用域,也會盡量的去執行,

首先來了解一下什麼是自己的子協程

image-20230209134504138

通常我們通過

  • CoroutineScope.launch{}
  • CoroutineScope.async{}

來開啟一個協程,因為當前是在CoroutineScope作用域中,所以直接launch / async 即可

這段代碼可以看出,runBlocking 會等待子協程全部執行完,然後在結束任務,因為協程都是異步的,

所以會先執行協程之外的代碼,然後再執行協程中的代碼

可以在協程中添加一些睡眠操作再來測試一下

image-20230209135225296

可以看出,還是可以正常的執行完所有代碼

現在解釋完了定義中的前半句話: runBlocking 會阻塞線程來等待自己子協程執行完, 並且對於不是子協程的作用域,也會盡量的去執行,

再來看一下後半句話:

image-20230209135639421

可以看出,通過自定義coroutine 和 GlobalScope,來創建的協程照樣可以執行出來

那麼在他們之中稍微添加一點邏輯會怎麼樣?

image-20230209140524725

可以看出,一旦添加了一點邏輯, runBlocking是不會等待非子協程的作用域

如果想讓runBlocking等待非子協程執行完,那麼只需要調用Job.#join() 即可

例如這樣:

image-20230209141429449

join()方法目前可以理解為: 等待當前協程執行完 在往下執行其他代碼,

一旦調用了join()方法,那麼協程就變成了同步的,那麼這塊代碼一共執行需要4s

因為協程1並沒有join, 所以協程1還是異步的,

協程2調用了join,所以在執行協程2的過程中,協程1也在執行.

所以協程1,與協程2的執行時間為2s

image-20230209142033645

tips: 在開發中不建議使用runBlocking,因為會阻塞主線程,阻塞主線程的時間,用來子協程的執行..

開啟協程兩種不同的方式

在上面代碼中我們提到了,開啟協程有2種方式

  • CoroutineScope#launch{}
  • CoroutineScope#async{}

先來看相同點:

image-20230209143252217

相同點就是無論是哪種方式,都會執行裏面的代碼

那麼這兩種方式有什麼區別呢?

  • launch無法返回數據, async可以返回結果

image-20230209143141023

返回的結果通過 Deferred#await()來獲取,並且調用Deferred#await()的時候,會等待async{} 執行完成之後在往下執行,就和Job#join一樣,不過await()有返回結果

使用await的時候有一個注意點:

image-20230209143832540

那麼也可以看到,launch{} 與 async{} 的返回值也有所不同:

  • launch{} 返回 Job
  • async{} 返回Deferred

其實本質上來説,async 返回的也是Job,不過只是Job的子類Deferred而已,Deferred只是對返回值等一些操作的封裝

image-20230209144036904

那麼Job是用來幹什麼的呢?

Job是用來管理協程的生命週期的, 例如剛才提到的 Job.join() 就可以讓協程 “立即執行”

launch{} 和 async{} 捕獲異常的方式也不同,這個等下一篇專門聊異常的時候在詳細講解

Job.cancel 取消協程

協程比線程好用的一點就是協程可以自己管理生命週期, 而線程則不可以

image-20230209145025542

這裏需要注意的是,如果協程體中還在執行,但是外部已經取消了,那麼則會throw異常出來

JobCancellationException

```kotlin fun main() = runBlocking { println("main start")

val job = launch {
    try {
        (0..100).forEachIndexed { index, _ ->
            delay(1000)
            println("launch $index")
        }
    } catch (e: Exception) {
        println("協程被取消了 $e")
    }
}

// 協程執行完成監聽
job.invokeOnCompletion {
    println("協程執行完畢${it}")
}

delay(5500)

// 取消協程
job.cancel()

println("main end")

} ```

Job#invokeOnCompletion: 協程完成回調

運行結果:

main start launch 0 launch 1 launch 2 launch 3 launch 4 main end 協程被取消了 kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@76a4d6c 協程執行完畢kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelled}@76a4d6c

coroutine的3種狀態

coroutine的三種狀態都是通過Job來管理的:

  • isActive 是否是活躍狀態
  • isCancelled 是否取消
  • isCompleted 是否完成

先來看看正常流程執行的代碼:

image-20230209151017683

我們知道協程始終是異步執行的,在執行printlnJob的時候,協程體中的代碼還沒有真正的執行

所以此時處於活躍狀態,並且協程沒有被執行完

如果我們在協程執行完成的回調中調用

image-20230209151208403

那麼此時,協程體中的代碼已經執行完了,那麼此時就是非活躍狀態

還剩一個Job#isCancelled 這個方法比較簡單,簡單的説就是是否調用了Job.cancel()

image-20230209151653026

但是這裏有一個特別奇怪的點,明明已經調用Job#cancel() 來取消協程,並且協程體中的代碼也沒執行,但是為什麼還顯示協程沒有執行完呢?

因為Job#cancel() 並不是suspend函數,不是suspend函數就沒有恢復功能,這行文字可能看的有一點迷惑,先不用管什麼掛起於恢復,現在只需要知道

我們調用cancel() 的時候會緊跟着一個,Job#join() 即可

或者直接調用Job.cancelAndJoin() 即可

image-20230209152145627

掛起恢復,這4個字我理解了10天左右,不可能通過本篇就講清楚,現在只需要會調用這些api,即可!!

那麼問題就來了,這個狀態有什麼用呢?

先來看一段代碼:

Feb-09-2023 15-40-30

可以驚奇的發現,這段代碼無論如何都cancel不掉.好像是失效了一樣

那麼解決這個問題,就可以檢測協程是否是活躍狀態,例如這樣

Feb-09-2023 15-43-37

Job也提供了一個方法: Job#ensureActive()

ensureActive() 本質也是通過isActive判斷,不同的是,當取消的時候可以捕獲到取消的異常,然後來處理對應的事件

圖片地址: http://gitee.com/lanyangyangzzz/picture/blob/master/coroutine/coroutine1/Feb-09-2023%2015-53-05.gif

回顧一下本篇:

本篇我們講解了runBlocking, 這個函數會幫我們阻塞主線程, 阻塞住線程的時候會等待內部的子協程全部執行完

還聊了最基礎的如何開啟一個協程, launch / async 以及他們的相同點和不同點

最後引出了協程生命週期管理者Job, 講解了Job常用的方法,以及job的3種狀態

| 方法名 | 作用 | 補充 | | -------------------- | -------------------- | ------------------------------------------------------------ | | join() | 立即恢復協程體執行 | 等待協程體執行完成,在執行後續代碼 | | cancel() | 取消協程 | ,如果取消時,協程體還在執行,這throw JobCancellationException,這個異常不會上報,會自行處理 | | invokeOnCompletion() | 協程體執行完成回調 | | | isActive | 協程體是否是活躍狀態 | | | isCancelled | 協程體是否被取消 | | | isCompleted | 協程體是否執行完成 | |

完整代碼

下一篇預告:

  • CoroutineDispatcher // 協程調度器 用來切換線程
  • CoroutineName // 協程名字

  • CoroutineStart // 協程啟動模式

  • CoroutineException // launch / async 捕獲異常
  • GlobalCoroutineException // 全局捕獲異常

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