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

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

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

Let’s get started!

準(zhǔn)備,編寫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

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

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

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

imageView.setImageBitmap(bitmap)

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

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我們可以這樣寫:

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)行在后臺線程中因此引用不會(huì)卡頓或者crash,你可以這樣聲明一個(gè)CoroutineContext:

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

這行代碼將會(huì)給coroutine創(chuàng)建一個(gè)新的context且名叫“bg”,它會(huì)使用兩個(gè)常規(guī)線程來執(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之前沒有做過的。第一個(gè)挑戰(zhàn)是activity的生命周期問題。如果在加載完成之前我們銷毀了activity,那么調(diào)用imageView.setImageBitmap()將會(huì)導(dǎo)致應(yīng)用崩潰。為了避免這中情況,我們必須取消這個(gè)加載。這個(gè)是launch()的返回值需要做的,我們把這個(gè)返回值job保存起來,在activity的onStop()中這樣做:

job.cancel()

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

生命周期觀察者LifecycleObserver

自從支持庫(support library)出來之后,Android Architecture Components應(yīng)該算是Google送給androiders最好的禮物了。有很多的文章來講解ViewModel、Room和LiveData。另一個(gè)偉大的部分是Lifecycle API,利用它我們可以很方便地監(jiān)聽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)長ifecycleOwner (FragmentActivity和support Fragment實(shí)現(xiàn)了它)創(chuàng)建一個(gè)叫做load的擴(kuò)展函數(shù)(我默認(rèn)并希望你了解kotlin的擴(kuò)展函數(shù),這是kotlin的基礎(chǔ)知識,如果不懂這個(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)來分析。

我們給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()來創(chuàng)建一個(gè)coroutine,async()將會(huì)運(yùn)行在后臺因?yàn)樗膮?shù)是Background coroutine context,需要注意的是async()還有第二個(gè)參數(shù):start = CoroutineStart.LAZY,這表示這個(gè)coroutine不會(huì)啟動(dòng)直到有人顯示地請求它返回值,后面內(nèi)容你將會(huì)看到怎么使用它。

這個(gè)coroutine返回一個(gè)Deferred<T>對象給調(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類型的對象作為參數(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對象最為自己的參數(shù)。我們調(diào)用await()方法來暫停當(dāng)前coroutine直到Deferred對象返回了值。

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

這正是kotlin coroutines的迷人之處。await()的調(diào)用雖然是在UI線程完成的,但是它并不會(huì)阻塞UI線程。它只是暫停函數(shù)的執(zhí)行直到準(zhǔn)備好,Deferred對象傳遞給lambda后它就會(huì)喚起并開始執(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言)

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

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

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

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

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

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

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

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


原文地址,翻譯的不是很好,大致只翻譯了技術(shù)部分,一些啰嗦的段落和句子沒有翻譯??,有好的意見請留言。原文的第二個(gè)擴(kuò)展函數(shù)then()有bug,具體bug和bugfix請看原文的兩條評論:評論1評論2

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,667評論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,635評論 2 380

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