Kotlin-簡約之美-進階篇(十三):協(xié)程

@[toc]

什么是協(xié)程秕铛?

官方描述:協(xié)程通過將復(fù)雜性放入庫來簡化異步編程。程序的邏輯可以在協(xié)程中順序地表達煞肾,而底層庫會為我們解決其異步性梢莽。該庫可以將用戶代碼的相關(guān)部分包裝為回調(diào)、訂閱相關(guān)事件臼朗、在不同線程(甚至不同機器)上調(diào)度執(zhí)行邻寿,而代碼則保持如同順序執(zhí)行一樣簡單蝎土。

協(xié)程就像非常輕量級的線程。線程是由系統(tǒng)調(diào)度的绣否,線程切換或線程阻塞的開銷都比較大誊涯。而協(xié)程依賴于線程,但是協(xié)程掛起時不需要阻塞線程蒜撮,幾乎是無代價的暴构,協(xié)程是由開發(fā)者控制的。所以協(xié)程也像用戶態(tài)的線程段磨,非常輕量級取逾,一個線程中可以創(chuàng)建任意個協(xié)程。

協(xié)程很重要的一點就是當(dāng)它掛起的時候苹支,它不會阻塞其他線程砾隅。協(xié)程底層庫也是異步處理阻塞任務(wù),但是這些復(fù)雜的操作被底層庫封裝起來债蜜,協(xié)程代碼的程序流是順序的晴埂,不再需要一堆的回調(diào)函數(shù),就像同步代碼一樣策幼,也便于理解邑时、調(diào)試和開發(fā)。它是可控的特姐,線程的執(zhí)行和結(jié)束是由操作系統(tǒng)調(diào)度的,而協(xié)程可以手動控制它的執(zhí)行和結(jié)束黍氮。

使用

首先需要添加依賴:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3"

1.runBlocking:T

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    Log.e(TAG, "主線程id:${mainLooper.thread.id}")
    test()
    Log.e(TAG, "協(xié)程執(zhí)行結(jié)束")
}

private fun test() = runBlocking {
    repeat(8) {
        Log.e(TAG, "協(xié)程執(zhí)行$it 線程id:${Thread.currentThread().id}")
        delay(1000)
    }
}

[圖片上傳失敗...(image-26061b-1582802832277)]

runBlocking啟動的協(xié)程任務(wù)會阻斷當(dāng)前線程唐含,直到該協(xié)程執(zhí)行結(jié)束。當(dāng)協(xié)程執(zhí)行結(jié)束之后沫浆,頁面才會被顯示出來捷枯。

2.launch:Job

這是最常用的用于啟動協(xié)程的方式,它最終返回一個Job類型的對象专执,這個Job類型的對象實際上是一個接口淮捆,它包涵了許多我們常用的方法。下面先看一下簡單的使用:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    Log.e(TAG, "主線程id:${mainLooper.thread.id}")
    val job = GlobalScope.launch {
        delay(6000)
        Log.e(TAG, "協(xié)程執(zhí)行結(jié)束 -- 線程id:${Thread.currentThread().id}")
    }
    Log.e(TAG, "主線程執(zhí)行結(jié)束")
}

//Job中的方法
job.isActive
job.isCancelled
job.isCompleted
job.cancel()
jon.join()

[圖片上傳失敗...(image-acaffd-1582802832277)]

從執(zhí)行結(jié)果看出本股,launch不會阻斷主線程攀痊。

launch方法源碼解析

我們看一下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
}

從方法定義中可以看出,launch() 是CoroutineScope的一個擴展函數(shù)拄显,CoroutineScope簡單來說就是協(xié)程的作用范圍苟径。launch方法有三個參數(shù):1.協(xié)程下上文;2.協(xié)程啟動模式躬审;3.協(xié)程體:block是一個帶接收者的函數(shù)字面量棘街,接收者是CoroutineScope

1.協(xié)程下上文

上下文可以有很多作用蟆盐,包括攜帶參數(shù),攔截協(xié)程執(zhí)行等等遭殉,多數(shù)情況下我們不需要自己去實現(xiàn)上下文石挂,只需要使用現(xiàn)成的就好。上下文有一個重要的作用就是線程切換险污,Kotlin協(xié)程使用調(diào)度器來確定哪些線程用于協(xié)程執(zhí)行痹愚,Kotlin提供了調(diào)度器給我們使用:

  • Dispatchers.Main:使用這個調(diào)度器在 Android 主線程上運行一個協(xié)程÷扌模可以用來更新UI 里伯。在UI線程中執(zhí)行
  • Dispatchers.IO:這個調(diào)度器被優(yōu)化在主線程之外執(zhí)行磁盤或網(wǎng)絡(luò) I/O。在線程池中執(zhí)行
  • Dispatchers.Default:這個調(diào)度器經(jīng)過優(yōu)化渤闷,可以在主線程之外執(zhí)行 cpu 密集型的工作疾瓮。例如對列表進行排序和解析 JSON。在線程池中執(zhí)行飒箭。
  • Dispatchers.Unconfined:在調(diào)用的線程直接執(zhí)行狼电。
    調(diào)度器實現(xiàn)了CoroutineContext接口。
2.啟動模式

在Kotlin協(xié)程當(dāng)中弦蹂,啟動模式定義在一個枚舉類中:

public enum class CoroutineStart {
    DEFAULT,
    LAZY,
    @ExperimentalCoroutinesApi
    ATOMIC,
    @ExperimentalCoroutinesApi
    UNDISPATCHED;
}

一共定義了4種啟動模式肩碟,下表是含義介紹:

啟動模式 作用
DEFAULT 默認的模式,立即執(zhí)行協(xié)程體
LAZY 只有在需要的情況下運行
ATOMIC 立即執(zhí)行協(xié)程體凸椿,但在開始運行之前無法取消
UNDISPATCHED 立即在當(dāng)前線程執(zhí)行協(xié)程體削祈,直到第一個 suspend 調(diào)用
2.協(xié)程體

協(xié)程體是一個用suspend關(guān)鍵字修飾的一個無參,無返回值的函數(shù)類型脑漫。被suspend修飾的函數(shù)稱為掛起函數(shù),與之對應(yīng)的是關(guān)鍵字resume(恢復(fù))髓抑,注意:掛起函數(shù)只能在協(xié)程中和其他掛起函數(shù)中調(diào)用,不能在其他地方使用优幸。
suspend函數(shù)會將整個協(xié)程掛起吨拍,而不僅僅是這個suspend函數(shù),也就是說一個協(xié)程中有多個掛起函數(shù)時网杆,它們是順序執(zhí)行的羹饰。看下面的代碼示例:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    GlobalScope.launch {
        val token = getToken()
        val userInfo = getUserInfo(token)
        setUserInfo(userInfo)
    }
    repeat(8){
        Log.e(TAG,"主線程執(zhí)行$it")
    }
}
private fun setUserInfo(userInfo: String) {
    Log.e(TAG, userInfo)
}

private suspend fun getToken(): String {
    delay(2000)
    return "token"
}

private suspend fun getUserInfo(token: String): String {
    delay(2000)
    return "$token - userInfo"
}

[圖片上傳失敗...(image-d05b20-1582802832277)]

getToken方法將協(xié)程掛起碳却,協(xié)程中其后面的代碼永遠不會執(zhí)行队秩,只有等到getToken掛起結(jié)束恢復(fù)后才會執(zhí)行。同時協(xié)程掛起后不會阻塞其他線程的執(zhí)行追城。

3.async

async跟launch的用法基本一樣刹碾,區(qū)別在于:async的返回值是Deferred,將最后一個封裝成了該對象座柱。async可以支持并發(fā)迷帜,此時一般都跟await一起使用物舒,看下面的例子。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    GlobalScope.launch {
        val result1 = GlobalScope.async {
            getResult1()
        }
        val result2 = GlobalScope.async {
            getResult2()
        }
        val result = result1.await() + result2.await()
        Log.e(TAG,"result = $result")
    }
}

private suspend fun getResult1(): Int {
    delay(3000)
    return 1
}

private suspend fun getResult2(): Int {
    delay(4000)
    return 2
}

async是不阻塞線程的,也就是說getResult1和getResult2是同時進行的戏锹,所以獲取到result的時間是4s冠胯,而不是7s。

應(yīng)用

項目中的網(wǎng)絡(luò)請求框架大部分都是基于RxJava + Retrofit + Okhttp封裝的锦针,RxJava可是很好的實現(xiàn)線程之間的切換荠察,如果只是網(wǎng)絡(luò)框架中用到了RxJava,那就是“大材小用”了奈搜,畢竟RxJava的功能還是很強大的悉盆。Retrofit從2.6.0開始已經(jīng)支持協(xié)程了:可以定義成一個掛起函數(shù)。

interface Api {
    @POST("user/login")
    suspend fun login(): Call<User>
}

下面的例子是使用協(xié)程來代替RxJava實現(xiàn)線程切換馋吗。

1.首先定義一個請求相關(guān)的支持DSL語法的接收者焕盟。
class RetrofitCoroutineDSL<T> {

    var api: (Call<Result<T>>)? = null
    internal var onSuccess: ((T) -> Unit)? = null
        private set
    internal var onFail: ((msg: String, errorCode: Int) -> Unit)? = null
        private set
    internal var onComplete: (() -> Unit)? = null
        private set

    /**
     * 獲取數(shù)據(jù)成功
     * @param block (T) -> Unit
     */
    fun onSuccess(block: (T) -> Unit) {
        this.onSuccess = block
    }

    /**
     * 獲取數(shù)據(jù)失敗
     * @param block (msg: String, errorCode: Int) -> Unit
     */
    fun onFail(block: (msg: String, errorCode: Int) -> Unit) {
        this.onFail = block
    }

    /**
     * 訪問完成
     * @param block () -> Unit
     */
    fun onComplete(block: () -> Unit) {
        this.onComplete = block
    }

    internal fun clean() {
        onSuccess = null
        onComplete = null
        onFail = null
    }
}
2.然后給協(xié)程定義一個擴展方法,用于Retrofit網(wǎng)絡(luò)請求宏粤。
fun <T> CoroutineScope.retrofit(dsl: RetrofitCoroutineDSL<T>.() -> Unit) {
    //在主線程中開啟協(xié)程
    this.launch(Dispatchers.Main) {
        val coroutine = RetrofitCoroutineDSL<T>().apply(dsl)
        coroutine.api?.let { call ->
            //async 并發(fā)執(zhí)行 在IO線程中
            val deferred = async(Dispatchers.IO) {
                try {
                    call.execute() //已經(jīng)在io線程中了脚翘,所以調(diào)用Retrofit的同步方法
                } catch (e: ConnectException) {
                    coroutine.onFail?.invoke("網(wǎng)絡(luò)連接出錯", -1)
                    null
                } catch (e: IOException) {
                    coroutine.onFail?.invoke("未知網(wǎng)絡(luò)錯誤", -1)
                    null
                }
            }
            //當(dāng)協(xié)程取消的時候,取消網(wǎng)絡(luò)請求
            deferred.invokeOnCompletion {
                if (deferred.isCancelled) {
                    call.cancel()
                    coroutine.clean()
                }
            }
            //await 等待異步執(zhí)行的結(jié)果
            val response = deferred.await()
            if (response == null) {
                coroutine.onFail?.invoke("返回為空", -1)
            } else {
                response.let {
                    if (response.isSuccessful) {
                        //訪問接口成功
                        if (response.body()?.status == 1) {
                            //判斷status 為1 表示獲取數(shù)據(jù)成功
                            coroutine.onSuccess?.invoke(response.body()!!.data)
                        } else {
                            coroutine.onFail?.invoke(response.body()?.msg ?: "返回數(shù)據(jù)為空", response.code())
                        }
                    } else {
                        coroutine.onFail?.invoke(response.errorBody().toString(), response.code())
                    }
                }
            }
            coroutine.onComplete?.invoke()
        }
    }
}

在上面的代碼中绍哎,比較難理解的是下面的代碼:

val coroutine = RetrofitCoroutineDSL<T>().apply(dsl)

dsl是帶接收者的函數(shù)字面量来农,接收者是RetrofitCoroutineDSL,所有先創(chuàng)建一個接受者對象崇堰,然后將傳入的實參dsl賦值給該對象沃于。還可以寫成下面的樣子:

val coroutine = RetrofitCoroutineDsl<T>()
coroutine.dsl() 

上面的寫法是直接調(diào)用函數(shù)字面量。為了方便里面海诲,把上述代碼翻譯成對應(yīng)的Java代碼:

RetrofitCoroutineDsl<T> coroutine = new RetrofitCoroutineDsl<T>();
dsl.invoke(coroutine);

調(diào)用函數(shù)dsl并傳入coroutine,其實就是把dsl賦值給coroutine揽涮。

3.最后一步,讓BaseActivity實現(xiàn)接口CoroutineScope饿肺,這樣在頁面中的上下文就是協(xié)程下上文
open class BaseActivity : AppCompatActivity(), CoroutineScope {

    private lateinit var job: Job

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        job = Job()
    }

    override fun onDestroy() {
        super.onDestroy()
        // 關(guān)閉頁面后,結(jié)束所有協(xié)程任務(wù)
        job.cancel() 
    }
}

是CoroutineContext中的運算符重載盾似,包含兩者的上下文:

//Returns a context containing elements from this context and elements from  other [context].
//The elements from this context with the same key as in the other one are dropped.
public operator fun plus(context: CoroutineContext): CoroutineContext

在Activity中可以直接調(diào)用擴展函數(shù)retrofit來調(diào)用網(wǎng)絡(luò)請求:

retrofit<User> {
    api = RetrofitCreater.create(Api::class.java).login()
    onSuccess {
        Log.e(TAG, "result = ${it?.avatar}")
    }
    onFailed { msg, _ ->
        Log.e(TAG, "onFailed = $msg")
    }
}

如果不需要處理訪問失敗的情況敬辣,可以寫成下面的樣子:

retrofit<User> {
    api = RetrofitCreater.create(Api::class.java).login()
    onSuccess {
        Log.e(TAG, "result = ${it?.avatar}")
    }
}

使用協(xié)程可以更好的控制任務(wù)的執(zhí)行,并且比線程更加的節(jié)省資源零院,更加的高效溉跃。結(jié)合DSL的代碼風(fēng)格,可以讓我們的程序更加直觀易懂告抄、簡潔優(yōu)雅撰茎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市打洼,隨后出現(xiàn)的幾起案子龄糊,更是在濱河造成了極大的恐慌逆粹,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件炫惩,死亡現(xiàn)場離奇詭異僻弹,居然都是意外死亡,警方通過查閱死者的電腦和手機他嚷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門蹋绽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人筋蓖,你說我怎么就攤上這事卸耘。” “怎么了粘咖?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵蚣抗,是天一觀的道長。 經(jīng)常有香客問我涂炎,道長忠聚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任唱捣,我火速辦了婚禮两蟀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘震缭。我一直安慰自己赂毯,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布拣宰。 她就那樣靜靜地躺著党涕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪巡社。 梳的紋絲不亂的頭發(fā)上膛堤,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音晌该,去河邊找鬼肥荔。 笑死,一個胖子當(dāng)著我的面吹牛朝群,可吹牛的內(nèi)容都是我干的燕耿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼姜胖,長吁一口氣:“原來是場噩夢啊……” “哼誉帅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤蚜锨,失蹤者是張志新(化名)和其女友劉穎档插,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體踏志,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡阀捅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了针余。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饲鄙。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖圆雁,靈堂內(nèi)的尸體忽然破棺而出忍级,到底是詐尸還是另有隱情,我是刑警寧澤伪朽,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布轴咱,位于F島的核電站,受9級特大地震影響烈涮,放射性物質(zhì)發(fā)生泄漏朴肺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一坚洽、第九天 我趴在偏房一處隱蔽的房頂上張望戈稿。 院中可真熱鬧,春花似錦讶舰、人聲如沸鞍盗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽般甲。三九已至,卻和暖如春鹅颊,著一層夾襖步出監(jiān)牢的瞬間敷存,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工堪伍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留历帚,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓杠娱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親谱煤。 傳聞我的和親對象是個殘疾皇子摊求,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容