Kotlin協程學習之路【一】

檸檬茶就是力量發表於2020-03-31

協程介紹

本質上,協程像是輕量級的執行緒
在我們程式設計的過程中 難免會出現非同步程式設計和一些回撥函式,這就很容易出現callback hell 回撥地獄 ,也就是說可能出現大量巢狀程式碼,這種程式碼在視覺效果以及邏輯維護上都堪稱地獄級程式碼,很容易給程式設計師帶來困擾。
在這之前大家可能接觸比較多的是像Rxjava這種用於處理非同步程式設計的框架,有各種操作符以及流式呼叫等特點方便進行非同步程式設計,而協程在這方面和Rxjava這種框架不同,協程做到了讓程式碼看起來更直白更具有邏輯性,至於怎麼讓程式碼看起來更通俗易懂,可以看看接下來我在學習協程中個人的一些理解和總結

建立協程

舉兩個簡單的建立方法 runBlocking { } 以及 GlobalScope.launch { }

runBlocking { }

runBlocking建立了一個協程,並且會阻塞當前執行緒,等待作用域也就是{}內的程式碼以及所有子協程結束,並且runBlocking 是有返回值的,但是由於會阻塞執行緒,所以用的情況不多,在這裡做一個入門講解

GlobalScope.launch { }

通過這個方法建立出來的協程是一個頂層的協程,它的生命週期跟application是一樣的,沒有返回值,也不可以取消,並且不會阻塞當前執行緒,這一點和runBlocking { } 正好相反

通過下面一段程式碼讓大家瞭解一下用法

// 簡單的輸出一句Hello World
GlobalScope.launch {  
    print("Hello World")
}
複製程式碼

launchJob

launch是以不阻塞的方式去啟動一個新的協程,需要在協程的範圍內去呼叫,比如上面提到runBlocking couroutineScope,並且會返回一個Job用來控制任務的開始取消等操作

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
}
複製程式碼

上面是launch的構造方法,有三個引數

  • context:可以傳入一個排程器Dispatchers來控制協程啟動選項,預設的引數是CoroutineStart.DEFAULT也就是按當前的上下文環境去啟動,如果我們想在IO執行緒中啟動,可以傳入Dispatchers.IO 還有像Dispatchers.Main就是在主執行緒中去啟動
  • start:如果我們想去控制協程的啟動時機,可以通過start傳入一個CoroutineStart,預設的CoroutineStart是立即啟動,如果想要延時啟動可以傳入CoroutineStart.LAZY, 然後在需要啟動的時候呼叫Jobjoin方法
  • block:讓協程在傳入的協程範圍內去執行

構建launch的方法講完之後就講一下Job的作用,Job有幾個常用的方法,像是join cancel cancelAndJoin

  • join:啟動協程任務並且會以一個非阻塞的方式去等待這個Job完成

  • cancel:取消任務

  • cancelAndJoin:綜合上面兩個方法,實際上內部就是先呼叫了cancel再呼叫join,也就是說會等待協程內的任務完成之後,再繼續往下執行程式碼,如果是cancel的話,那麼呼叫的時候協程內的任務就會直接中斷

    以上兩種cancel,舉個在安卓中的實際場景,比如一個草稿箱功能 ,使用者在點選退出之後彈出一個loading框,這個時候可以用cancelAndJoin等待上傳完成之後再dissmiss,如果使用者選擇直接退出,那麼可以用cancel

    另外被取消的協程,會丟擲一個CancellationException異常,表示協程被正常取消,如果你沒有捕獲錯誤的話,不會列印到控制檯/日誌

  • isActive:這是一個判斷當前協程是否還存活的標記,可以在任務中根據這個屬性決定任務是否繼續

在協程中如果你想要在任務結束之後做一些操作,那麼你可以用try{}finally{}這樣的操作把任務邏輯寫在try中,在finally中釋放資源


作用域構建器

除了由不同的構建器提供協程作用域之外,還可以使用 coroutineScope 構建器宣告自己的作用域。它會建立一個協程作用域並且在所有已啟動子協程執行完畢之前不會結束。coroutineScope 是非阻塞式的,是掛起函式,而runBlocking 是阻塞式,屬於常規的函式,但是兩者都會等待各自作用域中的所有子協程結束

關於上面說的runBlocking { }是阻塞式,coroutineScope是非阻塞式,可以通過下面一段程式碼來體現
import kotlinx.coroutines.*

fun main() = runBlocking { 
    // 用launch啟動一個子協程
    launch { 
        delay(200L)
        println("Task from runBlocking")
    }
    
    // 建立一個協程作用域
    coroutineScope { 
        launch {
            delay(500L) 
            println("Task from nested launch")
        }
    
        delay(100L)
        println("Task from coroutine scope") // 這一行會在內嵌 launch 之前輸出
    }
    
    println("Coroutine scope is over") // 這一行在內嵌 launch 執行完畢後才輸出
}


最終的輸出結果:x
Task from coroutine scope 
Task from runBlocking 
Task from nested launch 
Coroutine scope is over

複製程式碼
 這一段程式碼取自kotlin官方文件,用來說明runBlocking { }coroutineScope的阻塞以及非阻塞式區別,有的人可能會覺得有點疑惑,程式碼上看起來coroutineScope後面的列印總是會在最後才輸出,看起來似乎coroutineScope才是阻塞式的
 然而並不是這樣,Task from runBlocking這一段先列印相信大家可以明白,接著程式碼執行到了coroutineScope作用域裡,作用域中有子協程,相信大家也能明白作用域中列印的順序,那麼問題來了,為什麼Coroutine scope is over總是在最後列印。
 其實是因為上面所提到的阻塞和非阻塞式特點,coroutineScope作用域會以非阻塞式的等待所有子協程完成,所以內部第一個launch在執行到delay的時候並沒有阻塞住執行緒,而是繼續執行下去,所以會先列印延時比較短的Task from coroutine scope,之後delay時間到了協程切回去列印了Task from nested launch,而由於coroutineScope是在runBlocking 當中,所以當couroutineScope沒有結束之前,runBlocking會阻塞當前執行緒等待,所以在coroutineScope內部delay沒有結束,沒有列印完成之前,最後一句println並不會被執行到。

這篇文章就暫時講這麼多知識點,接下來我也會根據自己在學習協程過程的一些疑惑以及總結陸續寫後續的相關文章

相關文章