Kotlin協(xié)程

什么是協(xié)程序仙?

官方描述:協(xié)程通過(guò)將復(fù)雜性放入庫(kù)來(lái)簡(jiǎn)化異步編程唉工。程序的邏輯可以在協(xié)程中順序地表達(dá)做入,而底層庫(kù)會(huì)為我們解決其異步性草描。該庫(kù)可以將用戶代碼的相關(guān)部分包裝為回調(diào)船惨、訂閱相關(guān)事件胸囱、在不同線程(甚至不同機(jī)器)上調(diào)度執(zhí)行茉盏,而代碼則保持如同順序執(zhí)行一樣簡(jiǎn)單儡循。

協(xié)程就像非常輕量級(jí)的線程混滔。線程是由系統(tǒng)調(diào)度的洒疚,線程切換或線程阻塞的開(kāi)銷都比較大歹颓。而協(xié)程依賴于線程,但是協(xié)程掛起時(shí)不需要阻塞線程油湖,幾乎是無(wú)代價(jià)的巍扛,協(xié)程是由開(kāi)發(fā)者控制的。所以協(xié)程也像用戶態(tài)的線程乏德,非常輕量級(jí)撤奸,一個(gè)線程中可以創(chuàng)建任意個(gè)協(xié)程。

協(xié)程很重要的一點(diǎn)就是當(dāng)它掛起的時(shí)候鹅经,它不會(huì)阻塞其他線程寂呛。協(xié)程底層庫(kù)也是異步處理阻塞任務(wù),但是這些復(fù)雜的操作被底層庫(kù)封裝起來(lái)瘾晃,協(xié)程代碼的程序流是順序的贷痪,不再需要一堆的回調(diào)函數(shù),就像同步代碼一樣蹦误,也便于理解劫拢、調(diào)試和開(kāi)發(fā)。它是可控的强胰,線程的執(zhí)行和結(jié)束是由操作系統(tǒng)調(diào)度的舱沧,而協(xié)程可以手動(dòng)控制它的執(zhí)行和結(jié)束。

kotlin協(xié)程使用

首先需要添加依賴:

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é)束之后,頁(yè)面才會(huì)被顯示出來(lái)玄窝。

2.launch:Job

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

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方法的定義:

從方法定義中可以看出黎休,launch() 是CoroutineScope的一個(gè)擴(kuò)展函數(shù),CoroutineScope簡(jiǎn)單來(lái)說(shuō)就是協(xié)程的作用范圍玉凯。launch方法有三個(gè)參數(shù):1.協(xié)程下上文势腮;2.協(xié)程啟動(dòng)模式;3.協(xié)程體:block是一個(gè)帶接收者的函數(shù)字面量漫仆,接收者是CoroutineScope作者:慕涵盛華鏈接:http://www.reibang.com/p/6e6835573a9c來(lái)源:簡(jiǎn)書(shū)著作權(quán)歸作者所有嫉鲸。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處歹啼。

1.協(xié)程下上文

上下文可以有很多作用玄渗,包括攜帶參數(shù)座菠,攔截協(xié)程執(zhí)行等等,多數(shù)情況下我們不需要自己去實(shí)現(xiàn)上下文藤树,只需要使用現(xiàn)成的就好浴滴。上下文有一個(gè)重要的作用就是線程切換Kotlin協(xié)程使用調(diào)度器來(lái)確定哪些線程用于協(xié)程執(zhí)行岁钓,Kotlin提供了調(diào)度器給我們使用:

Dispatchers.Main:使用這個(gè)調(diào)度器在 Android 主線程上運(yùn)行一個(gè)協(xié)程升略。可以用來(lái)更新UI 屡限。在UI線程中執(zhí)行

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

Dispatchers.Default:這個(gè)調(diào)度器經(jīng)過(guò)優(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è)枚舉類中:


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

啟動(dòng)模式作用

DEFAULT? ? ????????????????????????????默認(rèn)的模式逝撬,立即執(zhí)行協(xié)程體

LAZY? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?只有在需要的情況下運(yùn)行

ATOMIC? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?立即執(zhí)行協(xié)程體,但在開(kāi)始運(yùn)行之前無(wú)法取消

UNDISPATCHED? ? ? ? ? ? ? ? ? ? ?立即在當(dāng)前線程執(zhí)行協(xié)程體乓土,直到第一個(gè) suspend 調(diào)用

2.協(xié)程體

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

suspend函數(shù)會(huì)將整個(gè)協(xié)程掛起拦键,而不僅僅是這個(gè)suspend函數(shù),也就是說(shuō)一個(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是不阻塞線程的,也就是說(shuō)getResult1和getResult2是同時(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)大的诀豁。Retrofit從2.6.0開(kāi)始已經(jīng)支持協(xié)程了:可以定義成一個(gè)掛起函數(shù)。

interface Api {

????@POST("user/login")

????suspend fun login(): Call<User>

}

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

1.首先定義一個(gè)請(qǐng)求相關(guān)的支持DSL語(yǔ)法的接收者舷胜。

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

????}

????/**

????* 訪問(wèn)完成

????* @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) {

? ? ? ? ? ? ?//在主線程中開(kāi)啟協(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) {

????????????//訪問(wèn)接口成功

????????????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ì)象遇汞。還可以寫(xiě)成下面的樣子:

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

上面的寫(xiě)法是直接調(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族檬,這樣在頁(yè)面中的上下文就是協(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)閉頁(yè)面后歪赢,結(jié)束所有協(xié)程任務(wù)

????????job.cancel()

????}

}

+是CoroutineContext中的運(yùn)算符重載,包含兩者的上下文:


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


如果不需要處理訪問(wèn)失敗的情況单料,可以寫(xiě)成下面的樣子:


使用協(xié)程可以更好的控制任務(wù)的執(zhí)行埋凯,并且比線程更加的節(jié)省資源,更加的高效扫尖。結(jié)合DSL的代碼風(fēng)格白对,可以讓我們的程序更加直觀易懂、簡(jiǎn)潔優(yōu)雅换怖。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末甩恼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌条摸,老刑警劉巖悦污,帶你破解...
    沈念sama閱讀 222,000評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異屈溉,居然都是意外死亡塞关,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)子巾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)帆赢,“玉大人,你說(shuō)我怎么就攤上這事线梗∫冢” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,561評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵仪搔,是天一觀的道長(zhǎng)瘾婿。 經(jīng)常有香客問(wèn)我,道長(zhǎng)烤咧,這世上最難降的妖魔是什么偏陪? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,782評(píng)論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮煮嫌,結(jié)果婚禮上笛谦,老公的妹妹穿的比我還像新娘。我一直安慰自己昌阿,他們只是感情好饥脑,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,798評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著懦冰,像睡著了一般灶轰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上刷钢,一...
    開(kāi)封第一講書(shū)人閱讀 52,394評(píng)論 1 310
  • 那天笋颤,我揣著相機(jī)與錄音,去河邊找鬼内地。 笑死伴澄,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瓤鼻。 我是一名探鬼主播,決...
    沈念sama閱讀 40,952評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼贤重,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼茬祷!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起并蝗,我...
    開(kāi)封第一講書(shū)人閱讀 39,852評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤祭犯,失蹤者是張志新(化名)和其女友劉穎秸妥,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體沃粗,經(jīng)...
    沈念sama閱讀 46,409評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡粥惧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,483評(píng)論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了最盅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片突雪。...
    茶點(diǎn)故事閱讀 40,615評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖涡贱,靈堂內(nèi)的尸體忽然破棺而出咏删,到底是詐尸還是另有隱情,我是刑警寧澤问词,帶...
    沈念sama閱讀 36,303評(píng)論 5 350
  • 正文 年R本政府宣布督函,位于F島的核電站,受9級(jí)特大地震影響激挪,放射性物質(zhì)發(fā)生泄漏辰狡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,979評(píng)論 3 334
  • 文/蒙蒙 一垄分、第九天 我趴在偏房一處隱蔽的房頂上張望宛篇。 院中可真熱鬧,春花似錦锋喜、人聲如沸些己。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,470評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)段标。三九已至,卻和暖如春炉奴,著一層夾襖步出監(jiān)牢的瞬間逼庞,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,571評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工瞻赶, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留赛糟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,041評(píng)論 3 377
  • 正文 我出身青樓砸逊,卻偏偏與公主長(zhǎng)得像璧南,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子师逸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,630評(píng)論 2 359