Kotlin協(xié)程

什么是協(xié)程航徙?

官方描述:協(xié)程通過將復(fù)雜性放入庫來簡化異步編程谈况。程序的邏輯可以在協(xié)程中順序地表達(dá)嫩海,而底層庫會(huì)為我們解決其異步性捂刺。該庫可以將用戶代碼的相關(guān)部分包裝為回調(diào)谣拣、訂閱相關(guān)事件、在不同線程(甚至不同機(jī)器)上調(diào)度執(zhí)行族展,而代碼則保持如同順序執(zhí)行一樣簡單森缠。

協(xié)程就像非常輕量級(jí)的線程。線程是由系統(tǒng)調(diào)度的仪缸,線程切換或線程阻塞的開銷都比較大贵涵。而協(xié)程依賴于線程,但是協(xié)程掛起時(shí)不需要阻塞線程恰画,幾乎是無代價(jià)的宾茂,協(xié)程是由開發(fā)者控制的。所以協(xié)程也像用戶態(tài)的線程拴还,非常輕量級(jí)跨晴,一個(gè)線程中可以創(chuàng)建任意個(gè)協(xié)程。

協(xié)程很重要的一點(diǎn)就是當(dāng)它掛起的時(shí)候自沧,它不會(huì)阻塞其他線程坟奥。協(xié)程底層庫也是異步處理阻塞任務(wù)树瞭,但是這些復(fù)雜的操作被底層庫封裝起來拇厢,協(xié)程代碼的程序流是順序的,不再需要一堆的回調(diào)函數(shù)晒喷,就像同步代碼一樣孝偎,也便于理解、調(diào)試和開發(fā)凉敲。它是可控的衣盾,線程的執(zhí)行和結(jié)束是由操作系統(tǒng)調(diào)度的,而協(xié)程可以手動(dòng)控制它的執(zhí)行和結(jié)束爷抓。

使用

首先需要添加依賴:

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

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)
    }
}

runBlocking啟動(dòng)的協(xié)程任務(wù)會(huì)阻斷當(dāng)前線程势决,直到該協(xié)程執(zhí)行結(jié)束。當(dāng)協(xié)程執(zhí)行結(jié)束之后蓝撇,頁面才會(huì)被顯示出來果复。

2.launch:Job

這是最常用的用于啟動(dòng)協(xié)程的方式,它最終返回一個(gè)Job類型的對(duì)象渤昌,這個(gè)Job類型的對(duì)象實(shí)際上是一個(gè)接口虽抄,它包涵了許多我們常用的方法走搁。下面先看一下簡單的使用:

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()

從執(zhí)行結(jié)果看出,launch不會(huì)阻斷主線程迈窟。

我們看一下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的一個(gè)擴(kuò)展函數(shù),CoroutineScope簡單來說就是協(xié)程的作用范圍车酣。launch方法有三個(gè)參數(shù):1.協(xié)程下上文曲稼;2.協(xié)程啟動(dòng)模式;3.協(xié)程體:block是一個(gè)帶接收者的函數(shù)字面量湖员,接收者是CoroutineScope

1.協(xié)程下上文

上下文可以有很多作用躯肌,包括攜帶參數(shù),攔截協(xié)程執(zhí)行等等破衔,多數(shù)情況下我們不需要自己去實(shí)現(xiàn)上下文清女,只需要使用現(xiàn)成的就好。上下文有一個(gè)重要的作用就是線程切換晰筛,Kotlin協(xié)程使用調(diào)度器來確定哪些線程用于協(xié)程執(zhí)行嫡丙,Kotlin提供了調(diào)度器給我們使用:

  • Dispatchers.Main:使用這個(gè)調(diào)度器在 Android 主線程上運(yùn)行一個(gè)協(xié)程《恋冢可以用來更新UI 曙博。在UI線程中執(zhí)行

  • Dispatchers.IO:這個(gè)調(diào)度器被優(yōu)化在主線程之外執(zhí)行磁盤或網(wǎng)絡(luò) I/O。在線程池中執(zhí)行

  • Dispatchers.Default:這個(gè)調(diào)度器經(jīng)過優(yōu)化怜瞒,可以在主線程之外執(zhí)行 cpu 密集型的工作父泳。例如對(duì)列表進(jìn)行排序和解析 JSON。在線程池中執(zhí)行吴汪。

  • Dispatchers.Unconfined:在調(diào)用的線程直接執(zhí)行惠窄。

調(diào)度器實(shí)現(xiàn)了CoroutineContext接口

2.啟動(dòng)模式

Kotlin協(xié)程當(dāng)中漾橙,啟動(dòng)模式定義在一個(gè)枚舉類中:

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

一共定義了4種啟動(dòng)模式杆融,下表是含義介紹:

啟動(dòng)模式 作用
DEFAULT 默認(rèn)的模式,立即執(zhí)行協(xié)程體
LAZY 只有在需要的情況下運(yùn)行
ATOMIC 立即執(zhí)行協(xié)程體霜运,但在開始運(yùn)行之前無法取消
UNDISPATCHED 立即在當(dāng)前線程執(zhí)行協(xié)程體脾歇,直到第一個(gè) suspend 調(diào)用

2.協(xié)程體

協(xié)程體是一個(gè)用suspend關(guān)鍵字修飾的一個(gè)無參,無返回值的函數(shù)類型淘捡。被suspend修飾的函數(shù)稱為掛起函數(shù),與之對(duì)應(yīng)的是關(guān)鍵字resume(恢復(fù))藕各,注意:掛起函數(shù)只能在協(xié)程中和其他掛起函數(shù)中調(diào)用,不能在其他地方使用焦除。

suspend函數(shù)會(huì)將整個(gè)協(xié)程掛起激况,而不僅僅是這個(gè)suspend函數(shù),也就是說一個(gè)協(xié)程中有多個(gè)掛起函數(shù)時(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"
}

getToken方法將協(xié)程掛起,協(xié)程中其后面的代碼永遠(yuǎn)不會(huì)執(zhí)行黔帕,只有等到getToken掛起結(jié)束恢復(fù)后才會(huì)執(zhí)行代咸。同時(shí)協(xié)程掛起后不會(huì)阻塞其他線程的執(zhí)行。

3.async

asynclaunch的用法基本一樣成黄,區(qū)別在于:async的返回值是Deferred呐芥,將最后一個(gè)封裝成了該對(duì)象。async可以支持并發(fā)奋岁,此時(shí)一般都跟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是不阻塞線程的,也就是說getResult1getResult2是同時(shí)進(jìn)行的闻伶,所以獲取到result的時(shí)間是4s滨攻,而不是7s。

應(yīng)用

項(xiàng)目中的網(wǎng)絡(luò)請(qǐng)求框架大部分都是基于RxJava + Retrofit + Okhttp封裝的蓝翰,RxJava可是很好的實(shí)現(xiàn)線程之間的切換光绕,如果只是網(wǎng)絡(luò)框架中用到了RxJava,那就是“大材小用”了畜份,畢竟RxJava的功能還是很強(qiáng)大的诞帐。Retrofit2.6.0開始已經(jīng)支持協(xié)程了:可以定義成一個(gè)掛起函數(shù)。

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

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

1.首先定義一個(gè)請(qǐng)求相關(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é)程定義一個(gè)擴(kuò)展方法,用于Retrofit網(wǎng)絡(luò)請(qǐng)求钙态。
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ò)連接出錯(cuò)", -1)
                    null
                } catch (e: IOException) {
                    coroutine.onFail?.invoke("未知網(wǎng)絡(luò)錯(cuò)誤", -1)
                    null
                }
            }
            //當(dāng)協(xié)程取消的時(shí)候,取消網(wǎng)絡(luò)請(qǐng)求
            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 ?: "返回?cái)?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)建一個(gè)接受者對(duì)象剩失,然后將傳入的實(shí)參dsl賦值給該對(duì)象。還可以寫成下面的樣子:

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

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

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

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

3.最后一步甲捏,讓BaseActivity實(shí)現(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中的運(yùn)算符重載,包含兩者的上下文:

//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)用擴(kuò)展函數(shù)retrofit來調(diào)用網(wǎng)絡(luò)請(qǐng)求:

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)雅座云。

Kotlin協(xié)程原理詳解

Kotlin DSL

Kotlin實(shí)戰(zhàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末疙赠,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子朦拖,更是在濱河造成了極大的恐慌圃阳,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件璧帝,死亡現(xiàn)場離奇詭異捍岳,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)睬隶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門祟同,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人理疙,你說我怎么就攤上這事晕城。” “怎么了窖贤?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵砖顷,是天一觀的道長。 經(jīng)常有香客問我赃梧,道長滤蝠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任授嘀,我火速辦了婚禮物咳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蹄皱。我一直安慰自己览闰,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布巷折。 她就那樣靜靜地躺著压鉴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪锻拘。 梳的紋絲不亂的頭發(fā)上油吭,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天击蹲,我揣著相機(jī)與錄音,去河邊找鬼婉宰。 笑死歌豺,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的心包。 我是一名探鬼主播世曾,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼谴咸!你這毒婦竟也來了轮听?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤岭佳,失蹤者是張志新(化名)和其女友劉穎血巍,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體珊随,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡述寡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了叶洞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鲫凶。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖衩辟,靈堂內(nèi)的尸體忽然破棺而出螟炫,到底是詐尸還是另有隱情,我是刑警寧澤艺晴,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布昼钻,位于F島的核電站,受9級(jí)特大地震影響封寞,放射性物質(zhì)發(fā)生泄漏然评。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一狈究、第九天 我趴在偏房一處隱蔽的房頂上張望碗淌。 院中可真熱鬧,春花似錦抖锥、人聲如沸亿眠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缕探。三九已至,卻和暖如春还蹲,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工谜喊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留潭兽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓斗遏,卻偏偏與公主長得像山卦,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子诵次,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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

  • 輕量級(jí)線程:協(xié)程 在常用的并發(fā)模型中逾一,多進(jìn)程铸本、多線程、分布式是最普遍的遵堵,不過近些年來逐漸有一些語言以first-c...
    Tenderness4閱讀 6,351評(píng)論 2 10
  • 你的第一個(gè)協(xié)程 輸出結(jié)果 從本質(zhì)上講箱玷,協(xié)同程序是輕量級(jí)的線程。它們是與發(fā)布 協(xié)同程序構(gòu)建器一起啟動(dòng)的陌宿。您可以實(shí)現(xiàn)相...
    十方天儀君閱讀 2,498評(píng)論 0 2
  • 前言 ?今年的Google開發(fā)者大會(huì)已表明將Kotlin作為其正式的語言锡足,現(xiàn)Google大力主推Kotlin, 在...
    Vgecanshang閱讀 3,447評(píng)論 0 15
  • 協(xié)程是一種并發(fā)設(shè)計(jì)模式壳坪,你可以在 Android 上使用它來簡化異步代碼舶得。協(xié)程是在 Kotlin 1.3 時(shí)正式發(fā)...
    Android高級(jí)工程師閱讀 2,615評(píng)論 0 4
  • 今天分享這一本《小金魚逃走了》 這本書我在豆豆11個(gè)月大的時(shí)候開始講給他聽,他還不會(huì)說話爽蝴,閱讀中跟隨故事情節(jié)扩灯,當(dāng)我...
    郭薛玲閱讀 282評(píng)論 2 1