Kotlin協程它不香嗎?

本博客的目的:java

  1. 知道Kotlin協程是什麼,爲何要用Kotlin協程
  2. 快速上手Kotlin協程
  3. 抓住核心,避免被誤導

Kotlin協程是什麼

Kotlin的協程簡單說就是線程的框架,詳細點說它就是一套基於線程而實現的一套更上層的工具APIandroid

協程這個術語早在 1958 年就被髮明並用於構建彙編程序,說明協程是一種編程思想,並不侷限於特定的語言。好比Go 語言也有協程,叫 Goroutines數據庫

那爲何要用Kotlin協程呢?編程

咱們常常會寫出異步操做的代碼,那麼這時候就免不了要處理線程間的通訊及切換。你可能會想到Android已經有一些很優秀的框架來幫咱們作這些事情,好比AsyncTask。但它有一些缺點:json

  • 它須要處理不少回調,一旦業務過多則容易陷入「回調地獄」。
  • 強行把業務拆分紅了前臺、中間更新、後臺三個函數。

回調地獄指多個回調嵌套在一塊兒bash

在寫業務代碼的時候,有好幾個接口須要你使用,接口A須要接口B的回調結果做爲參數去請求數據。這樣就會造成回調函數的嵌套。若是有三四層回掉嵌套,最終就會長下面這個樣子:網絡

asyncFunc1(opt, (...args1)  {
    asyncFunc2(opt, (...args2) {
        asyncFunc3(opt, (...args3) {
            asyncFunc4(opt, (...args4)  {
                // some operation
            });
        });
    });
});
複製代碼

看起來是否是有點噁心,若是你沒有感受到噁心,筆者以爲你可能常常寫這樣的代碼因此習慣了,有一句「名人」名言:吐着吐着就習慣了。多線程

到這裏優秀的你確定又想到了Rxjava這把利器,咱們能夠經過它提供的「Observable」的編程範式進行鏈式調用,能夠很好地消除回調。閉包

那麼這裏介紹的協程到底能夠作什麼呢?上面的問題它天然是能夠解決了,那它相較於RxJava的優點是什麼呢?併發

筆者以爲最主要的是它能夠用看起來同步的方式寫出異步的代碼。這樣寫代碼的人寫起來很舒服,讀代碼的人讀起來很暢快。

快速上手

下面筆者利用Retrofit配合協程實現一個登陸功能

首先須要添加如下依賴庫

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
//爲 Retrofit 添加對 Deferred 的支持
implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
複製代碼

根據 wanandroid 的登陸API接口,經過retrofit框架敲出客戶端的登陸接口

接口API連接: www.wanandroid.com/blog/show/2

interface ApiService {
    companion object {
        const val BASE_URL = "https://www.wanandroid.com"
    }

    @FormUrlEncoded
    @POST("/user/login")
    fun login(@Field("username") username: String,
              @Field("password") password: String): Deferred<WanResponse<User>>
}
複製代碼

Deferred是什麼?它是Job的子接口。那,,,Job又是什麼呢?能夠簡單理解,整個登陸請求的過程就是會被封裝成Job,而後交給協程調度器處理。但Job在完成的時候是沒有返回值的,因此就有了Deferred,它的意思就是延遲,結果稍後才能拿到,它能夠爲任務完成時提供返回值。

根據請求後返回的json,寫出返回值的數據類

data class WanResponse<out T>(val errorCode: Int,val errorMsg: String,val data: T)
複製代碼
data class User(val collectIds: List<Int>,val email: String,
                val icon: String,val id: Int,
                val password: String, val type: Int, val username: String)
複製代碼

以後構建一個retrofit實例,用它來進行請求登陸

class ApiRepository {
    val retrofit = Retrofit.Builder()
        .baseUrl(ApiService.BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        //添加對Deffered的支持
        .addCallAdapterFactory(CoroutineCallAdapterFactory.invoke())
        .build()
        .create(ApiService::class.java)

    fun login(name: String,password: String): Deferred<WanResponse<User>>{
        return retrofit.login(name,password)
    }
}
複製代碼

接下來主角協程要出場了。咱們能夠經過launch函數開啓一個協程

GlobalScope.launch(Dispatchers.IO) {
  var result: WanResponse<User>?=null
  result = repository.login(userName,userPassword).await()
  launch(Dispatchers.Main) {
        btnLogin.text = result.data.username
    }
}
複製代碼

這段代碼出現了Dispatchers 調度器,它能夠將協程限制在一個特定的線程執行,或者將它分派到一個線程池,或者讓它不受限制地運行,關於 Dispatchers 這裏先不展開了。

經常使用的 Dispatchers ,有如下三種:

  • Dispatchers.Main:Android 中的主線程
  • Dispatchers.IO:針對磁盤和網絡 IO 進行了優化,適合 IO 密集型的任務,好比:讀寫文件,操做數據庫以及網絡請求
  • Dispatchers.Default:適合 CPU 密集型的任務,好比計算

但上面的栗子只是一次網絡請求,若是有屢次請求可能就變成這個樣子:

GlobalScope.launch(Dispachers.IO) {
    //io操做
    launch(Dispachers.Main){
        //ui操做
        launch(Dispachers.IO) {
            //io操做
            launch(Dispacher.Main) {
              //ui操做
            }
        }
    }
}
複製代碼

這個嵌套???不是說協程能夠不用寫嵌套代碼的嗎

因而協程中有一個很實用的函數:withContext這個函數能夠切換到指定的線程,並在閉包內的邏輯執行結束以後,自動把線程切回去繼續執行,

用 withContext 改寫一下,它的結構大體就長這個亞子:

launch(Dispachers.Main) {
    ...
    withContext(Dispachers.IO) {
        ...
    }
    ...
    withContext(Dispachers.IO) {
        ...
    }
    ...
}
複製代碼

好比上面的登陸的栗子就能夠改寫成這樣:

GlobalScope.launch(Dispatchers.Main) {
    var result: WanResponse<User>?=null
    withContext(Dispatchers.IO){
        //請求登陸
       result = repository.login(userName,userPassword).await()
     }
      //更新ui
    btnLogin.text = result?.data?.username
   }
複製代碼

好像的確變得簡潔了許多,但離咱們的目標:看起來同步的方式寫出異步的代碼還差那麼一點。

既然不須要嵌套了,那就能夠把io線程的操做,拿出來單獨做爲函數,就能夠寫成這樣:

suspend fun login(name: String,password: String): WanResponse<User> {
       return withContext(Dispatchers.IO) {
            val repository = ApiRepository()
            repository.login(name, password).await()
        }
    }
複製代碼

這個函數和普通函數不同,多出來一個關鍵字suspend,直譯過來是掛起的意思,那這個關鍵字真正的做用到底什麼呢?這個下面會詳細解釋,這裏先跳過。

掛起函數寫好了,那開啓協程部分的代碼就能夠改寫一下

GlobalScope.launch (Dispatchers.Main){
                val result =login(userName,userPassword)
                btnLogin.text = result.data.username
    }
複製代碼

這樣看起來就和同步方式的代碼同樣了

supspend 關鍵字的做用

上面提到了掛起函數中的suspend,那它的做用是什麼呢?是掛起做用?

若是是掛起做用,那它掛起的對象是什麼?是當前線程仍是所在的函數?

答案是都不是,協程中的掛起,本質上掛起的對象是協程。協程是啥?就是launch函數包起來的代碼塊。

GlobalScope.launch (Dispatchers.Main){
        //login是個suspend函數
        val result = login(userName,userPassword)
        btnLogin.text = result.data.username
 }
      //Next
      ..... 
複製代碼
suspend fun login(name: String,password: String): WanResponse<User> {
       return withContext(Dispatchers.IO) {
            val repository = ApiRepository()
            repository.login(name, password).await()
        }
    }
複製代碼

當執行到suspend函數的時候,該協程就會在當前線程中被掛起,通俗一點理解,當前線程暫時無論這個協程了。

那當前線程它去作什麼呢?它該去作啥就作啥,好比仍是上面的例子,因爲該協程是在主線程中的,在請求登陸時協程就會被掛起,主線程就從這個協程中脫離出來,繼續走NEXT以後的代碼。當登陸請求成功以後,掛起函數又會將其切換到主線程中。

敲黑板,敲黑板啦

掛起其實作的就是稍後會將線程自動切換回來的操做,切換回來的動做就叫恢復(resume),它是協程裏的功能,因此咱們要在協程裏面(這個協程固然也能夠是一個掛起函數),去調用自帶的掛起函數,好比經常使用的withContext()

到這裏你極可能就認爲suspend關鍵字的做用就是掛起協程的做用了,那就過高估它,它並無這麼神奇的功能。

suspend它本質上只是一個提醒,那麼是誰對誰的提醒呢?

它是函數建立者對函數調用者的提醒,告訴函數調用者我是個掛起函數,是個耗時函數,請你在協程裏面調用我。表面上它是一個要求,實際上它是一個提醒。

supend它並無作掛起操做的功能,真正作掛起的是這個函數裏面掛起函數,好比咱們這裏用的withContext這個自帶的掛起函數。

因此值得注意的是:若是咱們沒有在supsend這個函數裏面去使用掛起函數,那這個掛起函數就沒有意義。由於一旦你使用了suspended關鍵字,就意味着它只能在協程中被調用。其實很容易理解:你又不須要掛起,還加個suspend讓調用者只能在協程裏面被調用,這不就至關於佔着茅坑不拉shi同樣[手動狗頭]

簡單總結一下:supsend的關鍵字它存在的意義就是提醒,在某種程度來講它能夠限制調用者不在主線程作耗時操做。

若是建立一個 suspend 函數但它內部不包含真正的掛起邏輯,編譯器會給一個提醒:redundant suspend modifier,告訴你這個 suspend 是多餘的。

避開誤區

1. 協程的掛起是非阻塞式,而線程是阻塞式的?

首先什麼是非阻塞式?

簡單點說非阻塞式就是不卡當前線程。這樣看協程的確是非阻塞式,好比你在主線程遇到一個掛起函數,就被切到另外一個線程去作操做了,那麼你的主線程固然就不會被卡了。

那麼問題來了,用Thread去切換線程是阻塞式的嗎?

線程是阻塞式的這句話只在單線程狀況下是對的,單線程的耗時操做確定會卡線程,因此是非阻塞式的。在多線程下,多線程的切換線程是不會卡線程的,因此確定是非阻塞式的。

那單協程的掛起是阻塞式的嗎?

它也是非阻塞式,由於它能夠利用掛起函數來切線程。

因此,協程的掛起和線程的切換並無什麼區別,它們都是非阻塞方式。協程的掛起並不比線程的切換高級到哪裏去,Kotlin的掛起就是切線程而已,和java的切線程本質上並無區別的,除了它的這種「寫法上看起來是阻塞式,實際非阻塞式的」神奇之處以外,並無其餘的神奇之處。

2.Kotlin協程的這種非阻塞式比線程更加高效?

若是你看懂了上面的內容,這句話也就不攻自破了,Kotlin協程並無比線程高級,更不存在協程比線程高效一說。

小結

協程就是 Kotlin 提供的一套線程封裝的 API,但並非說協程就是爲線程而生的。 協程設計的初衷是爲了解決併發問題,讓「協做式多任務」 實現起來更加方便。但初學Kotlin協程能夠從線程控制切入,也就是本文介紹的內容,至於更高級的應用和實現原理,待筆者以爲若是有必要深刻學習以後,再寫博客分享~~

若是以爲筆者寫的不容易理解,建議去看看扔物線大佬的Kotlin協程三連,確定會讓你有醍醐灌頂之感。

Kotlin的協程用力瞥一眼

本博客的內容能夠算是看完協程三連後的學習筆記了~

相關文章
相關標籤/搜索