Android開發(fā)之MVVM模式實(shí)踐(四):協(xié)程的簡單認(rèn)識(shí)和使用

前言

大家好驮履,我是小益惭蟋!眾所周知,在2017年Google I/O大會(huì)上竖慧,Google宣布將Kotlin作為Android的第一開發(fā)語言嫌套,而且近年來很多中大型公司招聘Andorid開發(fā)者都要求會(huì)使用Kotlin開發(fā),所以如果你還沒又開始使用Kotlin圾旨,就趕緊上船吧踱讨!

推薦

文章將率先在公眾號(hào)「碼途有道」上發(fā)布,歡迎大家關(guān)注砍的!

一痹筛、認(rèn)識(shí)協(xié)程

使用Kotlin開發(fā)有很多好處,在這我就不一一贅述了廓鞠。而在Kotlin中有一樣?xùn)|西叫做“協(xié)程”帚稠,一些同學(xué)可能聽過甚至使用過,但也有很多同學(xué)可能對其不是很了解床佳,這里我簡單介紹一下協(xié)程是什么滋早!

協(xié)程簡介

協(xié)程其實(shí)是一種編程思想,在很多語言中都存在砌们,大致作用也都一樣杆麸,都是為了幫助我們在處理多線程編碼時(shí)提供更友好和簡便的編碼方式,只是每種語言的具體實(shí)現(xiàn)不一樣浪感,在Kotlin中的協(xié)程我們可以簡單的理解為是一個(gè)線程框架昔头。

普通線程的不便

我們通過一個(gè)小例子對比一下使用普通線程和使用協(xié)程實(shí)現(xiàn)起來有什么不同:

假設(shè)有四個(gè)接口請求A、B影兽、C揭斧、D,接口C需要獲取接口A與接口B的執(zhí)行結(jié)果后才能執(zhí)行峻堰,接口D需要獲取接口C的執(zhí)行結(jié)果才能執(zhí)行讹开。

上述場景是我們在平時(shí)Android開發(fā)中最常見的場景盅视,如果用普通線程的方式來實(shí)現(xiàn),接口請求的結(jié)果一般使用callback獲取萧吠,接口A與接口B應(yīng)該是并發(fā)執(zhí)行左冬,我們必須在接口A執(zhí)行完成的callback中檢查接口B是否執(zhí)行完成,同樣必須在接口B執(zhí)行完成的callback中檢查接口A是否執(zhí)行完成纸型,這樣才能在第一時(shí)間將兩個(gè)接口的結(jié)果交給接口C拇砰,而接口C則需要在執(zhí)行完成的callback中調(diào)用接口D,使其執(zhí)行狰腌。

fun apiA(object: callback(){
        獲取 —> 結(jié)果A
        調(diào)用 —> 接口AB執(zhí)行完畢檢測方法()
} )

fun apiB(object: callback(){
        獲取 —> 結(jié)果A
        調(diào)用 —> 接口AB執(zhí)行完畢檢測方法()
} )

fun 接口AB執(zhí)行完畢檢測方法(){
    if( 結(jié)果A!=null && 結(jié)果B!=null ){
         apiC(結(jié)果A , 結(jié)果B, object: callback(){
            調(diào)用 —> apiD(結(jié)果C)
         }
    }
}

// 開始執(zhí)行
fun start(){
    apiA();
    apiB();
}

可以看出除破,在整個(gè)執(zhí)行流程中都充滿了callback,并且接口A與B之間的相互監(jiān)測是否執(zhí)行完畢也會(huì)多寫很多代碼琼腔。如果這種邏輯再稍微復(fù)雜點(diǎn)瑰枫,簡直就是回調(diào)地獄,代碼的閱讀體驗(yàn)也會(huì)變的越來越差丹莲。當(dāng)然我們可以使用Handler等消息通知方式來統(tǒng)一處理光坝,但是閱讀體驗(yàn)也是提升有限。

以上不便甥材,其歸根結(jié)底是因?yàn)榫€程是系統(tǒng)調(diào)度的盯另,系統(tǒng)控制線程的執(zhí)行結(jié)束,我們開發(fā)者在主線程中是無法得知線程何時(shí)執(zhí)行結(jié)束洲赵,只能等待線程自己通知我們鸳惯。

協(xié)程的優(yōu)勢

而上述場景使用協(xié)程來實(shí)現(xiàn),其實(shí)現(xiàn)邏輯大致如下:

val 結(jié)果A = 協(xié)程A執(zhí)行()

val 結(jié)果B = 協(xié)程B執(zhí)行()

val 結(jié)果C = 協(xié)程C執(zhí)行(結(jié)果A, 結(jié)果B)

協(xié)程D執(zhí)行(結(jié)果C)

PS:此處特別提示一點(diǎn)叠萍,上述協(xié)程A與B的邏輯看起來是協(xié)程A先執(zhí)行完后再協(xié)程B執(zhí)行芝发,但其實(shí)A、B也是并發(fā)執(zhí)行的

從上述流程中不難看出苛谷,其寫法直接從異步代碼寫法變成了同步代碼寫法辅鲸,邏輯瞬間清晰了很多,少了很多callback腹殿,代碼的閱讀體驗(yàn)也是直線上升独悴。而這都?xì)w功于協(xié)程可以靈活的在不同線程之間切換,開發(fā)者可以明確的控制協(xié)程的結(jié)束赫蛇,再也不用被動(dòng)的等待通知绵患。說到此處雾叭,有的同學(xué)可能就想到了RxJava悟耘,兩者都能實(shí)現(xiàn)我們想要的效果,不過兩者的設(shè)計(jì)理念并不相同织狐,語法也是天差地別暂幼,有興趣的同學(xué)兩者可以都了解一下筏勒。

綜上,我們可以得出一點(diǎn)旺嬉,協(xié)程可以將異步編碼簡化管行,用同步的方式寫異步,開發(fā)者可以靈活的控制協(xié)程的執(zhí)行與結(jié)束以及線程的切換邪媳。而這也是我們在Android開發(fā)中最在意的特性捐顷,協(xié)程的其他優(yōu)點(diǎn)我們暫時(shí)不必去了解。

二雨效、導(dǎo)入?yún)f(xié)程

目前高版本的Android Studio添加Kotlin支持時(shí)迅涮,已經(jīng)自動(dòng)添加Kotlin協(xié)程支持了,如果不能使用協(xié)程徽龟,可以添加如下依賴

implementation  "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5"
implementation  "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5"

三叮姑、協(xié)程的幾種使用方式

1. runBlocking

runBlocking {
    getUserInfo(userId)
}

此方法一般不推薦使用,因?yàn)樗鼤?huì)阻塞線程

2. GlobalScope.launch

GlobalScope.launch {
    getUserInfo(userId)
}

此種方法需要慎用据悔,雖然它不會(huì)阻塞線程传透,但是它的生命周期與app一致,并且不能取消

3. 自行創(chuàng)建CoroutineScope

// 此處的context是CoroutineContext极颓,和Activity繼承的Context不是同一個(gè)東西
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
     getUserInfo(userId)
}

此方法比較推薦朱盐,使用此方法我們可以靈活的控制協(xié)程的生命周期。所以下面我們主要介紹此種方式的協(xié)程使用方式讼昆,其他兩種方式有興趣的同學(xué)可以自行去了解托享。

四、協(xié)程的簡單使用

協(xié)程目前常用的幾個(gè)方法有launch浸赫、withContext以及async/await闰围,下面我們主要介紹一下launchwithContextasync/await留到下章與suspend一起講解既峡。

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
}

可以看出运敢,launchCoroutineScope擴(kuò)展函數(shù)(即launch是CoroutineScope的內(nèi)部函數(shù))校仑,并且最后返回了一個(gè)新的協(xié)程(即創(chuàng)建了新的Coroutine),具體實(shí)現(xiàn)我們暫且不管传惠。其使用方法如下:

// 切換到主線程
coroutineScope.launch(Dispatchers.Main) {
    ...
}

// 切換到IO線程執(zhí)行
coroutineScope.launch(Dispatchers.IO) {
    ...
}

// 小例子
coroutineScope.launch(Dispatchers.IO) {
    val userInfo = getUserInfo()
}
tv_name = "xxx"

我們可以通過指定Dispatchers來切換到不同的線程迄沫,如果不指定,則協(xié)程默認(rèn)在其所處方法的線程中執(zhí)行任務(wù)卦方。在{ }中的代碼就是協(xié)程需要執(zhí)行的代碼塊羊瘩,在最后的小例子中,tv_name = "xxx"并不會(huì)等待getUserInfo()執(zhí)行完畢才執(zhí)行,因?yàn)?code>coroutineScope.launch(Dispatchers.IO) { }的執(zhí)行并不會(huì)阻塞UI線程的執(zhí)行尘吗,coroutineScope.launch(Dispatchers.IO) { }就相當(dāng)于是另開了一個(gè)線程逝她,就如同new Thread().start()一樣。所以協(xié)程與其外部線程的關(guān)系我們一定要理清睬捶,我們再通過一個(gè)具體的例子看下協(xié)程內(nèi)部:

coroutineScope.launch(Dispatchers.Main) {
    tv_name.text = "xxx"
    // 切換到IO線程
    launch(Dispatchers.IO){
        // 獲取用戶信息
        val userInfo = getUserInfo()
        // 切換到UI線程
        launch(Dispatchers.Main){
            // 修改用戶名顯示
            tv_name.text = userInfo.username
            // 切換到IO線程
            launch(Dispatchers.IO) {
                // 獲取消息列表
                val msgList = getMessageList(userInfo.token)
                // 切換到UI線程
                launch(Dispatchers.Main){
                    // 顯示消息列表
                }
            }
        }
    }
}

在上述例子中黔宛,我們先在IO線程中獲取用戶信息,之后在主線程中更新用戶姓名擒贸,然后在IO線程中獲取消息列表臀晃,最后在主線程中顯示消息列表。整個(gè)流程通過launch來創(chuàng)建在不同線程中工作的子協(xié)程完成介劫,并且完全是一種同步編碼的體驗(yàn)积仗。另外,如果兩個(gè)launch處于同一層次蜕猫,如下:

coroutineScope.launch(Dispatchers.Main) {
    // 切換到IO線程
    launch(Dispatchers.IO){
        // 獲取用戶信息
        val userInfo = getUserInfo()
    }
        // 切換到IO線程
    launch(Dispatchers.IO){
        // 獲取首頁信息
        val userInfo = getHomeInfo()
    }
}

那么getUserInfo()getHomeInfo()會(huì)并發(fā)執(zhí)行寂曹,也就是說這兩個(gè)launch是并行的,這種方式適合于那種只管并發(fā)回右,不用返回結(jié)果的場景隆圆,例如多個(gè)網(wǎng)絡(luò)請求并發(fā),但是相互之間沒有關(guān)聯(lián)翔烁。

withContext

雖然使用launch已經(jīng)使得整個(gè)編碼方式由異步變?yōu)橥矫煅酰嵌鄠€(gè)launch嵌套并不美觀,我們可以優(yōu)化一下蹬屹,使用withContext來改寫:

coroutineScope.launch(Dispatchers.Main) {
    tv_name.text = "xxx"
    // 獲取用戶信息
    val userInfo = withContext(Dispatchers.Main) {
        getUserInfo()
    }
    tv_name.text = userInfo.username
    // 獲取消息列表
    val msgList = withContext(Dispatchers.IO) {
        getMessageList(userInfo!!.token)
    }
    // 顯示消息列表
}

在從上述代碼中侣背,withContext都是順序執(zhí)行,不像多個(gè)同級(jí)的launch是并發(fā)執(zhí)行慨默。使用withContext可以消除launch的嵌套贩耐,閱讀體驗(yàn)更佳。不過withContext只能在CoroutineScope內(nèi)部使用厦取,不能單獨(dú)在其他方法中使用潮太,比如:

class BaseActivity : Activity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        withContext(Dispatchers.IO){
            ...
        }
    }
}

這種寫法是不行的,因?yàn)?code>withContext只能在CoroutineScope內(nèi)部使用虾攻。

五铡买、小結(jié)

本章內(nèi)容簡單介紹了協(xié)程是什么以及協(xié)程的基本使用,下章內(nèi)容將會(huì)介紹寫協(xié)程中的async/awaitsuspend關(guān)鍵字霎箍,這兩者在我們的日常開發(fā)中使用頻率非常高奇钞,尤其是suspend。最后推薦一個(gè)Kotlin協(xié)程認(rèn)知系列文章漂坏,強(qiáng)烈推薦對協(xié)程不是很理解的同學(xué)去閱讀景埃!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末筋夏,一起剝皮案震驚了整個(gè)濱河市蒂胞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌条篷,老刑警劉巖骗随,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異赴叹,居然都是意外死亡鸿染,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門乞巧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涨椒,“玉大人,你說我怎么就攤上這事绽媒〔隙” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵是辕,是天一觀的道長囤热。 經(jīng)常有香客問我,道長获三,這世上最難降的妖魔是什么旁蔼? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮疙教,結(jié)果婚禮上棺聊,老公的妹妹穿的比我還像新娘。我一直安慰自己贞谓,他們只是感情好躺屁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著经宏,像睡著了一般犀暑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上烁兰,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天耐亏,我揣著相機(jī)與錄音,去河邊找鬼沪斟。 笑死广辰,一個(gè)胖子當(dāng)著我的面吹牛暇矫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播择吊,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼李根,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了几睛?” 一聲冷哼從身側(cè)響起房轿,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎所森,沒想到半個(gè)月后囱持,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡焕济,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年纷妆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晴弃。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡掩幢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出上鞠,到底是詐尸還是另有隱情粒蜈,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布旗国,位于F島的核電站枯怖,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏能曾。R本人自食惡果不足惜度硝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望寿冕。 院中可真熱鬧蕊程,春花似錦、人聲如沸驼唱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玫恳。三九已至辨赐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間京办,已是汗流浹背掀序。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留惭婿,地道東北人不恭。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓叶雹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親换吧。 傳聞我的和親對象是個(gè)殘疾皇子折晦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

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