【Kotlin】協程(一)——入門

介紹

這裏一開始不打算介紹什麼是協程,雖然標題叫介紹~~html

爲了方便理解,這邊先作個比喻: 從使用的角度來看,Kotlin的協程像是「另外一種RxJava」,可是比RxJava要高效。java

這裏先有個大體的印象,先了解下協程在實際中的做用,回頭再去看它的原理,或許會更容易些。android

一開始查了好多關於協程資料(包括官方完檔),發現不一樣的人說的不大同樣,最後越看越亂。因而我決定一開始先不說什麼是協程。後端

做用

上面說到,協程用起來「像是另外一種RxJava」。api

那麼是否是能夠用協程來開啓一個異步操做?切換線程? 答案是確定的,不只能夠作到,並且寫起來也很簡單。下面看個栗子網絡

栗子

舉個例子,這裏有個登陸操做,須要用兩個接口才能完成。 一、使用帳號密碼去獲取token 二、經過token獲取用戶信息異步

很明顯,這是個嵌套的請求。代碼立刻就浮如今腦海中,因而咱們埋頭「papapa」,很快就寫出了這樣的一段:async

reqToken(new CallBack<String>() { //請求token
    @Override
    public void onSuccess(String token) {
        reqUserInfo(token, new CallBack<UserInfo>() { //經過token,獲取用戶信息
            @Override
            public void onSuccess(UserInfo userInfo) {
                Logger.Companion.i("login success");
            }
        });
    }
});
複製代碼

是的,確實沒什麼問題。不過沒以爲這要的代碼很長嗎?ide

因而咱們改用lambda簡寫,或是kotlin:函數

reqToken{ //請求token
    reqUserInfo(it){  //經過token,獲取用戶信息
        Logger.i("login success")
    }
}
複製代碼

nice,瞬間簡潔了好多

確實簡潔了不少。不過仍是難逃嵌套結構,若是多來幾層,最後可能成了這樣:

蜜汁嵌套

看得頭皮發麻~~

可是!!!,若果用協程就不同了(劃重點)

coroutineScope.launch {
    val token = getToken()
    val userInfo = getUserInfo(token)
    Logger.i("login success")
}
複製代碼

不只代碼少,並且能夠用同步的方式來寫異步!!!

往下再瞭解一點?

使用

知道到了他的優(niu)秀(bi)之處,下面來看看是怎麼用的

由於是Kotlin的協程,因此項目須要支持Kotlin。怎麼支持就不用我說了吧? (不要問我,我不會,由於那是另外一個同事作的。hahaha~~~)

倒入依賴

gradle倒入協程依賴

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
複製代碼

建立協程做用域CoroutineScope

能夠直接new一個MainScope

val mainScop = MainScope()
複製代碼

注意記得在銷燬的時候調用cancel(),調用cancel()後用mainScope啓動的協程都會取消掉。

override fun onDestroy() {
    super.onDestroy()
    mainScope.cancel()
}
複製代碼

建立一個協程

經常使用的方式有兩種:launch()async(),下面分別來講明他們的用途。

固然還有其餘的建立方式,這裏就不說了。

launch

使用launch()建立一個協程,返回一個Job對象。

val job = mainScope.launch {
    //在協程中的操做
    Logger.i("launch end")
}
複製代碼

很簡單,mainScope.launch{}就能建立一個協程,大括號中的代碼是在協程中執行的。

看下打印的日誌,發現這個協程時在主線程中運行的。

"這有什麼用?在主線程中運行的協程?那我再裏面作耗時操做,是否是會卡住?"

確實,若是直接這樣用時會阻塞主線程的。因此這時候,就須要用到withContext()

這裏用的是上面的mainScope,這個做用域內的調度器是基於主線程調度器的。也就是說,mainScope.launch()獲得的協程默認都是在主線程中。因此println("in main scope")是在主線程中運行的

withContext

withContext():**用給定的協程上下文調用指定的暫停塊,暫停直到完成,而後返回結果。**也就是說,能夠用來切換線程,並返回執行後的結果。

經常使用的有 Dispatchers.Main:工做在主線程中 Dispatchers.Default:將會獲取默認調度器(子線程) Dispatchers.IO:IO線程 Dispatchers.Unconfined:是一個特殊的調度器(說實話,我沒搞懂他的用法~~)

這裏子線程中請求一個token,而後回到主線程中:

mainScope.launch {
    val token = withContext(Dispatchers.Default) {
        Logger.i("get token")
        val token =  api.getToken()//注意!!!這是個同步的網絡請求
        token
    }
    Logger.i("token $token")
}
複製代碼

再來看下日誌

有withContext()後,線程的切換顯得是那麼簡單。只要你開心,能夠切來切去。

mainScope.launch {
    withContext(Dispatchers.Default) {
        Logger.i("切到子線程")
    }
    withContext(Dispatchers.Main) {
        Logger.i("切到主線程")
    }
    withContext(Dispatchers.IO) {
        Logger.i("切到IO線程")
    }
    Logger.i("launch end")
}
複製代碼

async

除了launch(),還有個經常使用的方法——async()async()launch()類似。不一樣的是他能夠返回協程執行結束後值。

async()返回的是一個Deferred對象,須要經過Deferred#await()獲得返回值。

仍是上面的例子:子線程中請求一個token,而後回到主線程中:

mainScope.launch {
    val tokenDeferred = async(Dispatchers.Default) {
        Logger.i("get token")
        val token =  api.getToken()//注意!!!這是個同步的網絡請求
        token //返回token
    }
    val token = tokenDeferred.await()
    Logger.i("token : $token")
}
複製代碼

打印的結果上面同樣,就不貼圖了。

async()launch()同樣,都能指定執行的線程。

因爲Deferred#await()須要在協程中調用,因此上面在launch()中使用async()

「這有什麼用?跟launch()差很少啊?」

額~~ 用處大了,往下看

suspend

若是切換線程中的代碼不少,想把(withContext(){...})的代碼抽出來。因而寫成這樣

fun getToken(): String {
    return withContext(Dispatchers.Default) {
        //同步請求獲得token
        val token = api.getToken()
        token
    }
}
複製代碼

BUT,並不能這樣用,發現編譯器報錯了:

發現 withContext()只能在 協程或** suspend**方法中使用。因此,在方法前加上 suspend就不會報錯了。

suspend fun getToken(): String { ... }
複製代碼

suspend:申明這是個可掛起的函數,裏面能夠用協程的一下方法(launch()、async()、withContext()等)。

實際應用

有了協程,寫異步的代碼將會方便不少。

串行的請求

回到一開始的栗子,請求token,而後用token請求UserInfo

mainScope.launch {
    //獲取token
    val token = withContext(Dispatchers.Default) {
        val token = api.getToken()
        token 
    }
    //經過token,獲取userInfo
    val userInfo = withContext(Dispatchers.Default) {
        val userInfo = api.getUserInfo(token)
        userInfo 
    }
    //登陸成功
    Logger.i("login success, token: $token, userInfo is null: ${userInfo == null}")
}
複製代碼

看到這裏,你可能會:「不對啊,一開始的栗子沒這麼複雜~~~」

由於上面的例子中,把請求那部分的代碼抽到suspend方法去了。

mainScope.launch {
    //獲取token
    val token = getToken()
    //經過token,獲取userInfo
    val userInfo = getUserInfo(token)
    //登陸成功
    Logger.i("login success, token: $token, userInfo is null: ${userInfo == null}")
}
---------------------------------------
suspend fun getUserInfo(token: String): UserInfo {
    return withContext(Dispatchers.Default) {
        Logger.i("get userInfo, token: $token")
        val userInfo = api.getUserInfo(token)
        userInfo
    }
}
suspend fun getToken(): String {
    return withContext(Dispatchers.Default) {
        Logger.i("get token")
        val token = api.getToken()
        token
    }
複製代碼

稍微調整下,就會發現和上面是栗子是同樣的

並行的請求

有時候,遇到「優秀」的後端同窗。一個頁面須要請求兩個接口,用兩個接口返回的數據才能渲染出頁面。

這裏發起兩個連續的請求也能夠作到,可是若是能夠變成兩個並行的請求,豈不美哉?

那麼,async()就能夠排上用場了。

mainScope.launch {
    val timeMillis = measureTimeMillis { //記錄耗時
        val deferred1 = async { getData1() }
        val deferred2 = async { getData2() }
        val data1 = deferred1.await()
        val data2 = deferred2.await()
        Logger.i("data1: $data1, data1: $data2")
    }
    Logger.i("timeMillis : $timeMillis")
}
--------------------------------------------------
suspend fun getData1(): String {
    return withContext(Dispatchers.Default) {
        Thread.sleep(1000)
        "value1"
    }
}
suspend fun getData2(): String {
    return withContext(Dispatchers.Default) {
        Thread.sleep(1000)
        "value2"
    }
}
複製代碼

查看日誌

會發現,getData2()getData1()都是延遲1000ms的請求,若是用串行的方式來寫,耗時確定超過2000ms。使用async()耗時也才1051ms。

這裏用measureTimeMillis()來計算代碼耗時。

總結

協程基本的使用到這裏就能夠告一段落了,主要介紹了協程給我帶來了什麼,能夠在什麼場景下用,怎麼用。相信這樣同步的方式來寫異步,這樣寫出來的代碼必定是很是直觀、清晰的。

然而,有關什麼是協程?有哪些詳細的用法和細節?進程、線程和協程又有什麼關係?留着後面後面再說。

參考

Kotlin 官網 Kotlin Coroutines(協程) 徹底解析 Kotlin Primer·第七章·協程庫 探究高級的Kotlin Coroutines知識 破解 Kotlin 協程

以上有錯誤之處,感謝指出

相關文章
相關標籤/搜索