使用Kotlin的協(xié)程實(shí)現(xiàn)簡(jiǎn)單的異步加載

眾所周知在android中當(dāng)執(zhí)行程序的耗時(shí)超過5秒時(shí)就會(huì)引發(fā)ANR而導(dǎo)致程序崩潰。由于UI的更新操作是在UI主線程進(jìn)行的,理想狀態(tài)下每秒展示60幀時(shí)人眼感受不到卡頓,1000ms/60幀,即每幀繪制時(shí)間不應(yīng)超過16.67ms。如果某項(xiàng)操作的耗時(shí)超過這一數(shù)值就會(huì)導(dǎo)致UI卡頓。因此在實(shí)際的開發(fā)中我通常把耗時(shí)操作放在一個(gè)新的線程中(比如從網(wǎng)絡(luò)獲取數(shù)據(jù),從SD卡讀取圖片等操作),但是呢在android中UI的更新只能在UI主線程中進(jìn)行更新,因此當(dāng)我們?cè)诜荱I線程中執(zhí)行某些操作的時(shí)候想要更新UI就需要與UI主線程進(jìn)行通信。在android中g(shù)oogle為我們提供了AsyncTask和Handler等工具來便捷的實(shí)現(xiàn)線程間的通信。有許多的第三方庫也為我們實(shí)現(xiàn)了這一功能,比如現(xiàn)在非常流行的RxJava庫。在本篇文章中呢我想給大家分享的是使用Kotlin的Coroutine(協(xié)程)來實(shí)現(xiàn)耗時(shí)操作的異步加載,現(xiàn)在有RxJava這么屌的庫我們?yōu)槭裁催€要了解這個(gè)呢?Kotlin如今已是android的官方開發(fā)語言了解他里邊的異步相關(guān)的操作是很有必要的。本文只講解Coroutine的基本使用方法,并不作深入底層的研究,我將以一個(gè)加載圖片的例子來向您展示Coroutine的基本使用方法。

使用Coroutine之前的初始配置

首先我們使用android studio 新建一個(gè)項(xiàng)目,并在新建項(xiàng)目的時(shí)候勾選【Include Kotlin support】,就像下邊這樣


項(xiàng)目創(chuàng)建成功后,我們需要在build.gradle文件中的android配置模塊下面增加如下的配置

kotlin {
    experimental {
        coroutines 'enable'
    }
}

然后在build.gradle文件中添加如下的依賴

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

完整的配置情況如下:


經(jīng)過上邊的步驟Coroutine的配置就已經(jīng)完成了。接下來我們就可以使用Coroutine了。

實(shí)現(xiàn)你的第一個(gè)Coroutine程序

現(xiàn)在我們來開始編寫我們的第一個(gè)Coroutine例子程序,這個(gè)程序的主要功能就是從手機(jī)媒體中加載一張圖片,并把它顯示在一個(gè)ImageView中。我們先來看看在未使用Coroutine之前使用同步的方式加載圖片的代碼如下:

val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
imageView.setImageBitmap(bitmap)

在上邊的代碼中我們從媒體讀取了一張圖片并把它轉(zhuǎn)化成Bitmap對(duì)象。因?yàn)檫@是一個(gè)IO操作,如果我們?cè)赨I主線程中調(diào)用這段代碼,將可能導(dǎo)致程序卡頓或產(chǎn)生ANR崩潰,所以我們需要在新開的線程中調(diào)用下邊的代碼

val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)

接著我們需要在UI線程中調(diào)用下邊的代碼來顯示加載的圖片

imageView.setImageBitmap(bitmap)

為了實(shí)現(xiàn)這一功能在傳統(tǒng)的android程序中我們需要使用Handler或AsyncTask將結(jié)果從非UI主線程發(fā)送到UI主線程進(jìn)行顯示,我們需要編寫許多額外的代碼。并且這些代碼的可讀性也不是十分的友好。下邊我們來看看使用Kotlin的Coroutine來實(shí)現(xiàn)圖片的加載的代碼,如下:

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

我們先忽略返回值job,我們稍后會(huì)進(jìn)行介紹,在這兒我們關(guān)心的事情是launch函數(shù)和參數(shù)Background與UI。與之前使用同步的方式加載圖片相比唯一的不同就在于這兒我們調(diào)用了lauch函數(shù)。lauch()創(chuàng)建并啟動(dòng)了一個(gè)協(xié)程,這兒的參數(shù)Background是一個(gè)CoroutineContext對(duì)象,確保這個(gè)協(xié)程運(yùn)行在一個(gè)后臺(tái)線程,確保你的應(yīng)用程序不會(huì)因耗時(shí)操作而阻塞和崩潰。你可以像下邊這樣定義一個(gè)CoroutineContext:

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

他將使用含有兩個(gè)線程的線程池來執(zhí)行協(xié)程里邊的操作。在第一個(gè)協(xié)程里邊我們又調(diào)用了launch(UI)創(chuàng)建并啟動(dòng)了一個(gè)新的協(xié)程,這兒的UI并不是我們自己創(chuàng)建的,他是Kotlin在Android平臺(tái)里邊預(yù)定義的一個(gè)CoroutineContext,代表著在UI主線程中執(zhí)行協(xié)程里邊的操作。所以我們將更新程序界面的操作imageView.setImageBitmap(bitmap)放在了這個(gè)協(xié)程里。通過這兒的例子代碼你會(huì)發(fā)現(xiàn)在kotlin里邊使用協(xié)程來實(shí)現(xiàn)線程間的通信和切換非常的簡(jiǎn)單,比RxJava還簡(jiǎn)單。看上去就跟你寫同步的方式的代碼一樣。

取消協(xié)程

在上邊的例子中我們返回了一個(gè)Job類型的對(duì)象job。通過調(diào)用job.cancel()我們能夠取消一個(gè)協(xié)程。例如當(dāng)我們退出當(dāng)前Activity的時(shí)候,圖片還沒有加載完。這個(gè)時(shí)候我們就可以在onDestroy中調(diào)用job.cancel()來取消這個(gè)未完成的任務(wù)。這與我們使用Rxjava時(shí)調(diào)用dipose()或使用AsyncTask時(shí)調(diào)用cancel() 來取消未完成的操作的作用是一樣的。

LifecycleObserver

android 架構(gòu)組件(Android Architecture Components)里邊引入了許多非常好的東西,比如:ViewModel, Room 和 LiveData以及 Lifecycle API。給予我們一種非常安全簡(jiǎn)便的方式監(jiān)聽Activity和Fragment的生命周期變化。接下來我們將使用他們來對(duì)之前加載圖片的例子進(jìn)行改進(jìn),利用lifecycle對(duì)Activity生命周期進(jìn)行監(jiān)聽并做出相應(yīng)的處理(監(jiān)聽到Activity調(diào)用onDestroy()時(shí)自動(dòng)取消后臺(tái)任務(wù))。

我們定義如下的代碼來使用協(xié)程:

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

我們也創(chuàng)建了LifecycleOwner的一個(gè)擴(kuò)展函數(shù):

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

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

在這個(gè)函數(shù)里邊有許多新的東西,即使看上去感到疑惑也不要緊,我們會(huì)一步一步的對(duì)其進(jìn)行講解。我們?cè)谒袑?shí)現(xiàn)LifecycleOwner接口的類中擴(kuò)展了一個(gè)load函數(shù)。也就是說當(dāng)我們使用支持庫的時(shí)候我們可以在Activity或Fragment中直接調(diào)用這個(gè)load函數(shù)(支持庫里邊的AppCompatActivity和Fragment實(shí)現(xiàn)了LifecycleOwner接口)。為了能夠在這個(gè)函數(shù)里邊訪問lifecycle成員添加CoroutineLifecycleListener作為一個(gè)觀察者。

load()函數(shù)使用名為loader的lambda表達(dá)式作為參數(shù)(這個(gè)lambda表達(dá)式返回一個(gè)泛型類型T),在load()函數(shù)里邊我們調(diào)用了名叫async的函數(shù),這個(gè)函數(shù)的作用也是用于創(chuàng)建一個(gè)協(xié)程。它使用Background作為上下文。注意第二個(gè)參數(shù)start = CoroutineStart.LAZY。它的意思是不會(huì)立即啟動(dòng)一個(gè)協(xié)程。直到你顯示的請(qǐng)求他返回一個(gè)值的時(shí)候它才會(huì)啟動(dòng),稍后你會(huì)看到具體怎樣做。這個(gè)協(xié)程返回了一個(gè)Deferred<T>對(duì)象到調(diào)用者。它與我們之前提到的job對(duì)象是類似的,但是他可以攜帶一個(gè)延遲的值,類似于JavaScript 中的Promise或Java APIs中的Future<T>。

接下來我們定義Deferred<T>類(前面我們?cè)趌oad函數(shù)中返回的類型)的一個(gè)擴(kuò)展函數(shù)then(),它也使用一個(gè)名叫block的lambda表達(dá)式作為參數(shù)。這個(gè)lambda表達(dá)式以T類型的對(duì)象作為參數(shù)。具體代碼如下:

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

這個(gè)函數(shù)使用launch()創(chuàng)建了另外一個(gè)協(xié)程,這個(gè)新的協(xié)程將運(yùn)行在程序的主線程中。我們?cè)谶@個(gè)新的協(xié)程中調(diào)用了then函數(shù)中傳入的名叫block的lambda表達(dá)式并使用await()函數(shù)作為它的參數(shù)。await()是在主線程中調(diào)用的,但是他并不會(huì)阻塞主線程的執(zhí)行,它將掛起這個(gè)函數(shù),主線程可以繼續(xù)做其他的事情。當(dāng)值從其他協(xié)程中返回的時(shí)候,他將被喚醒并將值從Deferred傳遞到這個(gè)lambda中。掛起函數(shù)(Suspending functions)是協(xié)程中最主要的概念。

一旦Activity的onDestroy方法被調(diào)用的時(shí)候,我們?cè)趌oad()函數(shù)中添加的lifecycle觀察者將會(huì)取消第一個(gè)協(xié)程,也會(huì)使第二個(gè)協(xié)程被取消,避免block()被調(diào)用。

Kotlin Coroutine DSL

上邊我們定義了兩個(gè)擴(kuò)展函數(shù)和一個(gè)用于取消協(xié)程的類,讓我們來看看如何使用它們,代碼如下:

load {
  MediaStore.Images.Media.getBitmap(contentResolver,uri)
} then {
  imageView.setImageBitmap(it)
}

在上邊的代碼中我們傳遞一個(gè)lambda到load()函數(shù)中,在這個(gè)lambda中調(diào)用了loadBitmapFromMediaStore()函數(shù)運(yùn)行在一個(gè)后臺(tái)進(jìn)程中。一旦loadBitmapFromMediaStore()函數(shù)返回Bitmap,load()函數(shù)將返回Deferred<Bitmap>。擴(kuò)展的函數(shù)then()是被infix修飾的,因此當(dāng)Deferred<Bitmap>返回之后我們可以使用上面那種奇特的語法調(diào)用它。我們傳遞到then()中的lambda將接收到一個(gè)Bitmap對(duì)象。因此我們可以簡(jiǎn)單的調(diào)用imageView.setImageBitmap(it)顯示這個(gè)Bitmap。

上邊的代碼可以被應(yīng)用到任何別的需要使用異步調(diào)用并將值轉(zhuǎn)遞到主線程的操作中。和RxJava這種框架比起來Kotlin的協(xié)程可能沒有它那么強(qiáng)大。但是Kotlin的協(xié)程可讀性更強(qiáng),也更簡(jiǎn)單。現(xiàn)在你可以安全的使用它來執(zhí)行你的異步操作了,再也不用擔(dān)心內(nèi)存泄漏的發(fā)生了。如下是將上邊的代碼用于從網(wǎng)絡(luò)加載數(shù)據(jù)并顯示的例子:

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

以上就是本篇文章所要分享的全部內(nèi)容,希望能夠?qū)δ阌兴鶐椭H绻惆l(fā)現(xiàn)文章中有不對(duì)的地方也歡迎你幫忙指出,以便我做出及時(shí)的更正。

源碼地址:https://github.com/chenyi2013/CoroutineDemo
參考文章:
https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md
https://developer.android.com/topic/libraries/architecture/lifecycle.html
https://kotlinlang.org/docs/reference/coroutines.html
https://hellsoft.se/simple-asynchronous-loading-with-kotlin-coroutines-f26408f97f46

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

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