計(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胰柑。