Kotlin協程系列(三)

2023-12-01 12:00:26

1.前言

  前面兩節,我們運用了kotlin提供的簡單協程去實現了一套更易用的複合協程,這些基本上是以官方協程框架為範本進行設計和實現的。雖然我們還沒有直接接觸kotlin官方協程框架,但對它的絕大多數功能已經瞭如指掌了。本節,我們來探討一下官方協程框架的更多功能,並將其運用到實際的生產當中,在這裡,我以在Android中使用kotlin官方協程框架為例進行講述。

2.launch函數啟動一個協程

  在Android開發中,我們一般將協程的作用域和Android元件的lifeCycle繫結在一起,這樣,當元件銷燬的時候,協程的作用域就會取消,協程也就銷燬了,這樣不會造成記憶體漏失。在ViewModel中,我們可以直接使用viewModelScope這個作用域去建立協程,在Activity/Fragment這些擁有生命週期的元件中,我們可以使用lifecycleScope去建立協程,這裡我們使用lifecycleScope進行講述。

  這裡我們先給出launch函數的官方實現:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

  我們先補充一個知識點,協程的啟動模式,也就是start引數所設定的,總共有四種啟動模式,如下所示:

  1. DEFAULT:建立協程之後,立即開始排程,在排程前如果協程被取消,其將直接進入取消響應狀態
  2. ATOMIC:協程建立後,立即開始排程,協程執行到第一個掛起點之前不響應取消
  3. LAZY:只有協程被需要時,包括主動呼叫start,join,await等函數時才會開始排程,如果排程前被取消協程就會進入異常結束狀態
  4. UNDISPATCHED:協程建立之後立即在當前函數的呼叫棧中執行,直到遇到第一個真正掛起的點

  這裡我們要搞清楚立即排程和立即執行的區別,立即排程表示協程的排程器會立即接收排程指令,但具體執行的時機以及在哪個執行緒上執行還需要根據排程器的情況而定,也就是說立即排程到立即執行前通常會隔一段時間。這裡,我們給出一段程式碼,每隔一段時間列印一個數位:

lifecycleScope.launch{
            for(a in 1..10){
                Log.i("lifecycleScope",a.toString())
                delay(1000)
            }
        }

  這裡需要注意的是,如果不指定排程器,那麼該協程預設執行在UI執行緒上,指定排程器可以通過context引數指定,和上一節我們實現的一樣,這裡不再贅述。

  lauch函數的返回值是Job物件,Job物件常用的屬性和函數如下:

  1. isActive:判斷Job是否處於活動狀態
  2. isCompleted:判斷Job是否屬於完成狀態
  3. isCancelled:判斷Job是否被取消
  4. start():開始Job
  5. cancel():取消Job
  6. join():將當前協程掛起,直到該協程完成
  7. cancelAndJoin():前兩個函數的結合體

3.async函數啟動一個協程

  async和launch函數的不同點在於launch函數啟動的協程是沒有返回值的,而async函數啟動的協程是有返回值的。async函數返回一個Deferred物件,它繼承自Job物件,我們可以通過Deferred物件中的await函數獲取協程的執行結果,程式碼如下:

lifecycleScope.launch{
            val deferred=async{
                "計算結果"
            }
            val result=deferred.await()
            Log.i("lifecycleScope",result)
        }

  async函數和launch函數的共同點是他們不會等待協程執行結束,會立馬往下執行,測試如下:

lifecycleScope.launch {
            lastTime = System.currentTimeMillis()
            async {
                delay(1000)
            }
            async {
                delay(1000)
            }
            Log.i("耗時", (System.currentTimeMillis() - lastTime).toString())
        }

  列印的結果是1ms,並不是2000毫秒,也就是說多個async函數是並行執行的,當然,這裡換成launch結果也是一樣的。當然,如果你在後面加一個await函數,那麼結果就是2000ms左右了,也就是這樣:

lifecycleScope.launch {
            lastTime = System.currentTimeMillis()
            async {
                delay(1000)
            }.await()
            async {
                delay(1000)
            }.await()
            Log.i("耗時", (System.currentTimeMillis() - lastTime).toString())
        }

  使用launch加上join函數的結果也是一樣的。如果我再換一種寫法:

lifecycleScope.launch {
            lastTime = System.currentTimeMillis()
            val job1=launch {
                delay(1000)
            }
            val job2=launch {
                delay(1000)
            }
            job1.join()
            job2.join()
            Log.i("耗時", (System.currentTimeMillis() - lastTime).toString())
        }

  這裡的執行結果是1000毫秒左右,可以自行嘗試,換成async函數加await也是一樣的。

  通過上面的測試,我們可以得出結論,launch函數和async函數啟動的協程是並行執行的,並且啟動協程之後會立馬往下執行,不會等待協程完成,除非呼叫join或await函數。launch函數和async函數的唯一區別就是async函數啟動的協程有返回值,如果不需要獲取協程的執行結果,那麼沒必要用async函數。

4.withContext函數的作用

  官方框架中還為我們提供了一個好用的api,withContext(),它的定義如下:

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T 

  withContext會將引數中的lambda表示式排程到由context指定的排程器上執行,並且它會返回協程體當中的返回值,它的作用幾乎和async{}.await()等價,但和async{}.await()相比,它的記憶體開銷更低,因此對於使用async後立即要呼叫await的情況,應當優先使用withContext函數。而且有了withContext之後,在Android開發的時候,就可以不再使用Handler了,我們可以在需要進行耗時操作(網路請求,資料庫讀寫,檔案讀寫)時,使用withContext切換到IO執行緒上,在得到想要的結果後要更新UI時又可以切換到UI執行緒上,非常的方便。測試如下:

lifecycleScope.launch(Dispatchers.IO) {
            delay(1000)
            val result="JJLin"
           withContext(Dispatchers.Main){
               tv_display.text=result
           }
        }

  這段程式碼模擬了在IO執行緒上進行耗時操作,可以是資料庫存取,網路請求之類的;拿到結果後,用withContext切換到主執行緒,進行UI的更新。

5.協程的超時取消

  kotlin官方協程框架為我們提供了一個withTimeout()函數用於執行超時取消設定,這個api的定義如下:

public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T

  這個函數可以設定一個超時時間,超過這個時間後就會通過丟擲異常來取消這個協程,如果不想丟擲異常,可以使用withTimeoutOrNull,這個函數在超時之後會返回null,而不會丟擲異常。