Kotlin系列五:協程總結

目錄

一 協程基本用法

1.1 GlobalScope

1.2 runBlocking

1.3 launch

1.4 suspend

1.5 coroutineScope

1.6 小結

二 更多的作用域構建器

三 協程簡化回調


一 協程基本用法

協程:協程允許我們在單線程模式下模擬多線程編程效果,代碼執行時的掛起與恢復完全由編程語言控制,和操作系統無關。這種特性使得高併發程序的運行效率得到極大的提升,可以用看起來同步的代碼寫出實際上異步的操作。

Kotlin沒有將協程納入標準庫的API中,而是以依賴庫的形式提供的。以安卓爲例:


implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'

1.1 GlobalScope

GlobalScope.launch 每次創建的都是一個頂層協程,當前應用程序結束協程跟着結束

//此時只會打印codes run is coroutine scope,
//不會打印codes run is coroutine scope finished,
//因爲main結束了會強制結束當前協程
fun main(){
    GlobalScope.launch {
        println("codes run is coroutine scope")
        delay(1500)
        println("codes run is coroutine scope finished")
    }
    Thread.sleep(1000)
}
 

1.2 runBlocking

runBlocking函數也會創建一個協程的作用域,與GlobalScope.launch 不同的是,它可以保證在協程作用域內的所有代碼和子協程沒有執行完成之前一直阻塞當前線程。

因爲它可能會阻塞當前線程,而你又恰好在主線程中調用它,就有可能卡死界面,因此該函數只推薦在測試環境下使用,否則會影響性能。

//此時只會打印codes run is coroutine scope和codes run is coroutine scope finished,
//因爲runBlocking 會在協程內的代碼和子協程未完成的情況下阻塞線程
fun main(){
    runBlocking {
        println("codes run is coroutine scope")
        delay(1500)
        println("codes run is coroutine scope finished")
    }
    Thread.sleep(1000)
}

1.3 launch

launch函數只能在協程作用域中才能被調用,且它會在當前協程作用域下創建子協程。子協程會隨外層作用域協程的結束而一同結束。

//打印順序launch1 launch2 launch1 finished launch2 finished
//子作用域可以併發執行,並且根據編程語言先後執行順序
fun main(){
    runBlocking {
        launch{
            println("launch1")
            delay(1000)
            println("launch1 finished")
        }
        launch{
            println("launch2")
            delay(1000)
            println("launch2 finished")
        }
    }
    Thread.sleep(1000)
}

1.4 suspend

如果在launch函數中調用一個其他函數會產生一個問題:launch函數中的代碼是有協程作用域的,但是抽取到一個單獨函數中的代碼就沒有協程作用域了,無法調用類似於delay()這種掛起函數。

解決辦法:加suspend關鍵字,它可以將任意函數聲明成掛起函數,而掛起函數是可以相互調用的。

//聲明成掛起函數,掛起函數可以相互調用的
suspend fun printDot(){
    println(".")
    delay(1000)
}

1.5 coroutineScope

suspend關鍵字只能聲明爲掛起函數,無法提供作用域,因此1.4中的問題只解決了掛起函數相互調用的問題,被調用的函數中還是沒有協程作用域,無法調用類似於launch()函數。此時就用到了coroutineScope 函數。

coroutineScope 是掛起函數,它可以繼承外部作用域創建子作用域。coroutineScope 只會阻塞當前的協程,不會阻塞線程,runBlocking 會阻塞當前線程

//由於suspend 只能聲明爲掛起函數,無法提供作用域
//coroutineScope 是掛起函數,並且可以繼承外部作用域創建子作用域
//coroutineScope 只會阻塞當前的協程,不會阻塞線程,runBlocking 會阻塞當前線程
suspend fun printDot() = coroutineScope {
    launch{
        println(".")
        delay(1000)
    }
}

1.6 小結

GlobalScope.launch和runBlocking函數可以再任意地方調用;

coroutineScope函數可以在協程作用域或掛起函數中調用;

launch函數只能在協程作用域中調用;

可創建新協程作用域的函數:

  • GlobalScope.launch 可在任何地方調用
  • runBlocking 可在任何地方調用
  • lanuch
  • coruotineScope

二 更多的作用域構建器

1.6中已經介紹了四種可創建新協程作用域的函數,但是比如像GlobalScope.launch()函數,它每次都是創建的頂層協程,管理成本高(要一個個去job.cancel),所以除非明確要簡歷頂層協程,否則不建議使用GlobalScope.launch()。

//取消當前協程
val job = GlobalScope.launch {
    //處理具體邏輯
}
job.cancel()
 
//GlobalScope頂層協議,多個協議需要一個一個取消不方便
//CoroutineScope可以實現一個一個取消
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
 
}
job.cancel()

launch函數返回值永遠是一個Job對象,利用async函數可以創建一個協程並獲取它的執行結果。async函數必須在協程作用域中才能調用,它會創建一個新子協程並返回一個Deferred對象,調用deferred.await()方法就可獲得async函數代碼塊的結果。

 withContext()函數可以理解成async函數的簡化版寫法,但是它強制要求指定一個線程參數。線程參數主要有3種可選:

  1. Dispatchers.Default:默認的低併發線程策略,適用於計算密集型任務。
  2. Dispatchers.IO:較高併發線程策略,適用於主要在阻塞和等待中的代碼。
  3. Dispatchers.Main:只能在安卓項目中使用,在Android主線程中執行。
//獲取協程內容返回值
fun main(){
    runBlocking {
        val result = async {
            5+5
        }.await()//後面的代碼會在有返回值後才執行,所以有多個async請在最後執行await,不然會串行執行,效率很低
        println(result)
    }
}
 
//相當於上面代碼
fun main(){
    runBlocking {
        val result = withContext(Dispatchers.Default){
            5+5
        }
        println(result)
    }
}

三 協程簡化回調

suspendCoroutine函數幾乎可以用於簡化任何回調寫法。

suspendCoroutine函數必須在協程作用域或掛起函數中才能調用,它接收一個Lambda表達式,掛起當前協程,在一個普通協程中執行Lambda表達式代碼。Lambda的參數裏會傳上一個Continuation參數,調用它的resume()方法或者resumeWithException()可以讓協程恢復執行。

以常見的網絡請求回調爲例:

suspend fun request(address: String): String {
    return suspendCoroutine { continuation ->
        HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
            override fun onFinish(response: String) {
                continuation.resume(response)
            }

            override fun onError(e: Exception) {
                continuation.resumeWithException(e)
            }
        })
    }
}

GlobalScope.launch {
    val response = request("https://www.baidu.com/")
    Log.d("xfhy", "網絡請求結果 : $response")
}

將網絡請求的代碼使用suspendCoroutine包裝一下,即可免去每次去手動生成一個匿名類。調用continuation的resume方法將結果返回,可以在外面以看上去同步的代碼拿到請求結果。上面爲了簡單,沒有加try…catch,實際中需要加上捕獲異常。

Retrofit網絡請求也可以簡化:

val appService = ServiceCreator.create<AppService>()
appService.getAppData().enqueue(object:Callback<List<App>>) {
    
    override fun onResponse(call:Callback<List<App>>,response:Response<List<App>>) {
        //得到服務器返回的數據
    }
 
    override fun onFailure(call:Callback<List<App>>,t:Throwble) {
        //對異常進行處理
    }
 
}
 
//suspendCoroutine函數可以簡化上面的回調
//suspendCoroutine必須在協程作用域或者掛起函數中調用,他接受lambda表達式
suspend fun <T> Call<T>.await():T{
    return suspendCoroutine { continuation ->
        enqueue(object:Callback<List<App>>) {
    
            override fun onResponse(call:Callback<List<App>>,response:Response<List<App>>) {
                val body = response.body()
                if(body!=null)
                    continuation.resume(body)
                else
                    continuation.resumeWithException(RuntimeExceotion("respnse body is null")) 
            }
 
            override fun onFailure(call:Callback<List<App>>,t:Throwble) {
                //對異常進行處理
                continuation.resumeWithException(t) 
            }
    }
}
 
suspend fun getAppData(){
    try {
        val appList = ServiceCreator.creat<AppService>().getAppData().await()
        //對服務器響應數據進行處理
    } catch (e:Exception) {
        //對異常做處理
    }
}
 
//上述掛起函數可以在LiveData中調用,因爲LiveData的代碼塊中提供一個掛起函數的上下文
object Respository {
 
    fun searchPlaces(query:String) = liveData(Dispatchers.IO){
        val result = try{
            val placeResponse = SunnyWeatherNetwork.searchPlaces(query)
            if(placeResponse.status == "ok"){
                val places = placeResponse.places
                Result,success(places)
            }else{
                Result.failure(RuntimeExceotion("response status is ${placeResponse.status}"))
            }
        }catch(e:Exception){
            Result.failure<List<Place>>(e)
        }
        emit(result)
    }    
 
}

 

注:本文主要例子和參考

郭霖《第一行代碼》 Kotlin部分

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章