使用Kotlin Coroutines進(jìn)行簡(jiǎn)單的異步加載

計(jì)算機(jī)很擅長(zhǎng)多任務(wù)操作埋酬。為了編寫(xiě)出好的軟件我們需要對(duì)多任務(wù)操作和異步有個(gè)很好的了解。在Android上面這些包括了activities和fragments的異步的生命周期回調(diào)宫静。

Kotlin Coroutines(Kotlin協(xié)程)是最近加入到了異步API和庫(kù)的工具箱中。它不是一個(gè)解決所有問(wèn)題的銀彈(a silver bullet)避咆,但是在很多情境下它可以讓問(wèn)題變得更簡(jiǎn)單滋饲。本文不會(huì)深入探討coroutines的內(nèi)部工作原理,而只是舉一個(gè)怎樣在android開(kāi)發(fā)中使用kotlin coroutines的例子躲雅。

Let’s get started!

準(zhǔn)備鼎姊,編寫(xiě)Gradle

目前為止Kotlin Coroutines還是實(shí)驗(yàn)性的特性,因此使用Kotlin Coroutines需要在app模塊的build.gradle添加一些東西相赁,直接在android片段后面加上下面的代碼:

kotlin {
    experimental {
        coroutines 'enable'
    }
}

然后再添加兩個(gè)依賴:

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

你的第一個(gè)coroutine

我們的需求是從媒體存儲(chǔ)(media storage)中加載一個(gè)圖片然后通過(guò)一個(gè)ImageView展示相寇,同步方法可以這樣寫(xiě):

fun loadBitmapFromMediaStore(imageId: Int, imagesBaseUri: Uri): Bitmap {
  val uri = Uri.withAppendedPath(imagesBaseUri, imageId.toString())
  return MediaStore.Images.Media.getBitmap(contentResolver, uri)
}

由于這個(gè)方法是IO操作因此必須在后臺(tái)線程中進(jìn)行。函數(shù)返回Bitmap后我們使用ImageView展示它:

imageView.setImageBitmap(bitmap)

這個(gè)調(diào)用必須在UI線程否則會(huì)crash钮科。只需要三行代碼裆赵,我們可以這樣寫(xiě):

val uri = Uri.withAppendedPath(imagesBaseUri, imageId.toString())
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
imageView.setImageBitmap(bitmap)

取決于加載Bitmap的線程和時(shí)間,上面的代碼將導(dǎo)致應(yīng)用程序暫時(shí)凍結(jié)(糟糕的用戶體驗(yàn))或崩潰跺嗽。如果使用Kotlin Coroutines我們可以這樣寫(xiě):

val job = launch(Background) {
  val uri = Uri.withAppendedPath(imagesBaseUri, imageId.toString())
  val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, 
  launch(UI) {
    imageView.setImageBitmap(bitmap)
  }
}

現(xiàn)在我們先暫時(shí)忽略返回值job,等一會(huì)再討論它。重點(diǎn)launch方法以及它的兩個(gè)參數(shù)Background和UI桨嫁。這段代碼和之前的三行代碼的不同處在于launch()函數(shù)的調(diào)用植兰。我們可以很容易地遵循這段代碼,它與前面的三行完全同步的代碼的的示例幾乎完全相同璃吧。

函數(shù)launch()所做的事情是創(chuàng)建和啟動(dòng)一個(gè)coroutine楣导。Background參數(shù)是一個(gè)CoroutineContext保證這個(gè)coroutine運(yùn)行在后臺(tái)線程中因此引用不會(huì)卡頓或者crash,你可以這樣聲明一個(gè)CoroutineContext:

internal val Background = newFixedThreadPoolContext(2, "bg")

這行代碼將會(huì)給coroutine創(chuàng)建一個(gè)新的context且名叫“bg”畜挨,它會(huì)使用兩個(gè)常規(guī)線程來(lái)執(zhí)行它的任務(wù)筒繁。

在第一個(gè)協(xié)程(launch(Background)創(chuàng)建的)中我們調(diào)用了launch(UI),launch(UI)將會(huì)出發(fā)另一個(gè)協(xié)程coroutine巴元,這個(gè)coroutine運(yùn)行在預(yù)先定義好的使用UI線程的context毡咏。這意味著imageView.setImageBitmap()將會(huì)運(yùn)行在UI線程而不會(huì)導(dǎo)致應(yīng)用crash。

取消協(xié)程

上面的代碼可能是您在使用其他api之前沒(méi)有做過(guò)的逮刨。第一個(gè)挑戰(zhàn)是activity的生命周期問(wèn)題呕缭。如果在加載完成之前我們銷毀了activity,那么調(diào)用imageView.setImageBitmap()將會(huì)導(dǎo)致應(yīng)用崩潰修己。為了避免這中情況恢总,我們必須取消這個(gè)加載。這個(gè)是launch()的返回值需要做的睬愤,我們把這個(gè)返回值job保存起來(lái)片仿,在activity的onStop()中這樣做:

job.cancel()

這和RxJava (Disposable調(diào)用dispose())或者AsyncTask (調(diào)用cancel())所做的事情是一樣的。為了執(zhí)行后臺(tái)操作而閱讀語(yǔ)法尤辱,我們并沒(méi)有獲得更多的便利性砂豌。我們來(lái)看看能否解決這個(gè)問(wèn)題。

生命周期觀察者LifecycleObserver

自從支持庫(kù)(support library)出來(lái)之后啥刻,Android Architecture Components應(yīng)該算是Google送給androiders最好的禮物了奸鸯。有很多的文章來(lái)講解ViewModel、Room和LiveData可帽。另一個(gè)偉大的部分是Lifecycle API娄涩,利用它我們可以很方便地監(jiān)聽(tīng)activity和fragment的生命周期變化并作出相應(yīng)的反應(yīng)。結(jié)合coroutines我們使用下面的代碼:

class CoroutineLifecycleListener(val deferred: Deferred<*>) : LifecycleObserver {
  @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
  fun cancelCoroutine() {
    if (!deferred.isCancelled) {
      deferred.cancel()
    }
  }
}

我們?yōu)長(zhǎng)ifecycleOwner (FragmentActivity和support Fragment實(shí)現(xiàn)了它)創(chuàng)建一個(gè)叫做load的擴(kuò)展函數(shù)(我默認(rèn)并希望你了解kotlin的擴(kuò)展函數(shù)映跟,這是kotlin的基礎(chǔ)知識(shí)蓄拣,如果不懂這個(gè)基礎(chǔ),那么能懂kotlin coroutines就是奇跡了):

fun <T> LifecycleOwner.load(loader: () -> T): Deferred<T> {
  val deferred = async(context = Background, start = CoroutineStart.LAZY) {
    loader()
  }

  lifecycle.addObserver(CoroutineLifecycleListener(deferred))
  return deferred
}

好吧努隙,我承認(rèn)這個(gè)擴(kuò)展函數(shù)有很多新的東西球恤,我們一點(diǎn)一點(diǎn)來(lái)分析。

我們給LifecycleOwner添加了這個(gè)擴(kuò)展函數(shù)荸镊,而且Activity和Fragment實(shí)現(xiàn)了LifecycleOwner咽斧,那么在activity和fragment里我們可以直接調(diào)用這個(gè)load()函數(shù)堪置,函數(shù)里面我們獲取成員變量lifecycle,并添加觀察者CoroutineLifecycleListener张惹。

load()函數(shù)的參數(shù)是一個(gè)叫做loader的lambda舀锨,這個(gè)lambda返回范型T。函數(shù)內(nèi)部宛逗,我們調(diào)用async()來(lái)創(chuàng)建一個(gè)coroutine坎匿,async()將會(huì)運(yùn)行在后臺(tái)因?yàn)樗膮?shù)是Background coroutine context,需要注意的是async()還有第二個(gè)參數(shù):start = CoroutineStart.LAZY雷激,這表示這個(gè)coroutine不會(huì)啟動(dòng)直到有人顯示地請(qǐng)求它返回值替蔬,后面內(nèi)容你將會(huì)看到怎么使用它。

這個(gè)coroutine返回一個(gè)Deferred<T>對(duì)象給調(diào)用者屎暇,它和之前的job變量很像承桥,但是它可以攜帶deferred值比如一個(gè)JavaScript Promise或者常規(guī)Java APIs中的Future<T>,好處是它可以有一個(gè)工作在coroutines中的await()方法恭垦,馬上你就可以看到快毛。

下面我們給Deferred<T>定義另一個(gè)擴(kuò)展函數(shù)then(),而Deferred<T>正是上一個(gè)擴(kuò)展函數(shù)返回類型番挺。它也接受一個(gè)lambda參數(shù)block唠帝,block使用一個(gè)T類型的對(duì)象作為參數(shù)并返回Unit。

infix fun <T> Deferred<T>.then(block: (T) -> Unit): Job {
  return launch(context = UI) {
    block(this@then.await())
  }
}

這個(gè)函數(shù)使用launch()創(chuàng)建一個(gè)運(yùn)行在UI線程的coroutine玄柏。block的lambda表達(dá)式傳遞給這個(gè)coroutine襟衰,它把完成的Deferred對(duì)象最為自己的參數(shù)。我們調(diào)用await()方法來(lái)暫停當(dāng)前coroutine直到Deferred對(duì)象返回了值粪摘。

需要注意的是這個(gè)擴(kuò)展函數(shù)使用了中綴符號(hào)infix瀑晒,如果你不懂中綴符號(hào),下面的對(duì)then函數(shù)的調(diào)用你可能不太明白是咋回事徘意,我在這里簡(jiǎn)單地說(shuō)明下苔悦,正常的函數(shù)調(diào)用是object.function(parameter),如果函數(shù)是infix函數(shù)椎咧,可以使用這樣的調(diào)用方式:object function parameter玖详,比如kotlin標(biāo)準(zhǔn)庫(kù)的add函數(shù)就是infix函數(shù),我們就可以這樣調(diào)用add函數(shù):1 add 2等價(jià)于1.add(2)勤讽,大致是這么回事蟋座,有意見(jiàn)請(qǐng)留言。

這正是kotlin coroutines的迷人之處脚牍。await()的調(diào)用雖然是在UI線程完成的向臀,但是它并不會(huì)阻塞UI線程。它只是暫停函數(shù)的執(zhí)行直到準(zhǔn)備好诸狭,Deferred對(duì)象傳遞給lambda后它就會(huì)喚起并開(kāi)始執(zhí)行券膀。當(dāng)這個(gè)coroutine 暫停的時(shí)候君纫,UI線程可以繼續(xù)執(zhí)行其它的事情。Suspending functions是kotlin coroutines的核心概念和奇跡所在三娩。

load()函數(shù)中添加的生命周期觀察者(lifecycle observer)在activity的onDestroy()中會(huì)cancel掉第一個(gè)coroutine庵芭,這樣會(huì)導(dǎo)致第二個(gè)coroutine也會(huì)被cancel掉因此避免block()執(zhí)行。

Kotlin Coroutine DSL(Domain Specific Language雀监,領(lǐng)域?qū)S谜Z(yǔ)言)

這兩個(gè)擴(kuò)展函數(shù)考慮到了coroutine的取消問(wèn)題,下面看下我們的代碼:

load {
  loadBitmapFromMediaStore(imageId, imagesBaseUri)
} then {
  imageView.setImageBitmap(it)
}

上面的代碼我們給第一個(gè)擴(kuò)展函數(shù)load()傳遞一個(gè)lambda表達(dá)式眨唬,這個(gè)lambda調(diào)用了必須運(yùn)行在后臺(tái)線程的loadBitmapFromMediaStore()方法会前。lambda返回Bitmap類型,因此load()擴(kuò)展函數(shù)返回Deferred<Bitmap>類型匾竿。

上面代碼對(duì)第二個(gè)擴(kuò)展函數(shù)then()的調(diào)用看起來(lái)很玄幻瓦宜,這是中綴符號(hào)infix特有的調(diào)用方式。傳遞給then()的lambda接收一個(gè)Bitmap岭妖,因此我們可以調(diào)用imageView.setImageBitmap(it)方法临庇。多謝生命周期觀察者(lifecycle observer),取消(Cancellation)這個(gè)問(wèn)題我們也考慮到了昵慌。

上面的代碼對(duì)于這種異步調(diào)用的情景是通用的:首先從后臺(tái)線程獲取數(shù)據(jù)假夺,然后在UI線程展示數(shù)據(jù)。貌似kotlin coroutines不像RxJava那樣強(qiáng)大斋攀,因?yàn)镽xJava可以處理多個(gè)調(diào)用已卷,但是kotlin coroutines更簡(jiǎn)單易讀并且覆蓋了大部分的應(yīng)用場(chǎng)景。你可以寫(xiě)出這樣安全的代碼淳蔼,而不必?fù)?dān)心泄漏Context或者在每次調(diào)用中處理線程:

load { restApi.fetchData(query) } then { adapter.display(it) }

load()then()代碼如此簡(jiǎn)短而不足以搞一個(gè)新的library侧蘸,但是我希望將來(lái)一旦kotlin coroutines有了穩(wěn)定的正式版本,在Kotlin-based library中能夠出現(xiàn)類似的東西鹉梨。

到目前為止讳癌,你有兩個(gè)選擇,既可以采用上面的簡(jiǎn)單代碼也可以看下Anko Coroutines存皂。在這里我還發(fā)布了一個(gè)更加完整的版本晌坤。祝你在kotlin coroutines的冒險(xiǎn)中旅途愉快!


原文地址艰垂,翻譯的不是很好泡仗,大致只翻譯了技術(shù)部分,一些啰嗦的段落和句子沒(méi)有翻譯??猜憎,有好的意見(jiàn)請(qǐng)留言娩怎。原文的第二個(gè)擴(kuò)展函數(shù)then()有bug,具體bug和bugfix請(qǐng)看原文的兩條評(píng)論:評(píng)論1評(píng)論2胰柑。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末截亦,一起剝皮案震驚了整個(gè)濱河市爬泥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌崩瓤,老刑警劉巖袍啡,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異却桶,居然都是意外死亡境输,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門(mén)颖系,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)嗅剖,“玉大人,你說(shuō)我怎么就攤上這事嘁扼⌒帕福” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵趁啸,是天一觀的道長(zhǎng)强缘。 經(jīng)常有香客問(wèn)我,道長(zhǎng)不傅,這世上最難降的妖魔是什么旅掂? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮蛤签,結(jié)果婚禮上辞友,老公的妹妹穿的比我還像新娘。我一直安慰自己震肮,他們只是感情好称龙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著戳晌,像睡著了一般鲫尊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上沦偎,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天疫向,我揣著相機(jī)與錄音,去河邊找鬼豪嚎。 笑死搔驼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的侈询。 我是一名探鬼主播舌涨,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼扔字!你這毒婦竟也來(lái)了囊嘉?” 一聲冷哼從身側(cè)響起温技,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扭粱,沒(méi)想到半個(gè)月后舵鳞,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡琢蛤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年蜓堕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虐块。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡俩滥,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贺奠,到底是詐尸還是另有隱情,我是刑警寧澤错忱,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布儡率,位于F島的核電站,受9級(jí)特大地震影響以清,放射性物質(zhì)發(fā)生泄漏儿普。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一掷倔、第九天 我趴在偏房一處隱蔽的房頂上張望眉孩。 院中可真熱鬧,春花似錦勒葱、人聲如沸浪汪。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)死遭。三九已至,卻和暖如春凯旋,著一層夾襖步出監(jiān)牢的瞬間呀潭,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工至非, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钠署,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓荒椭,卻偏偏與公主長(zhǎng)得像谐鼎,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子戳杀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

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