眾所周知在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