如果英文較好公你,建議直接閱讀原文
譯文
什么是協(xié)程
基本上,coroutines是輕量級(jí)線程璃弄,它使得我們可以用串行的方式寫(xiě)出異步的哥捕、非阻塞的代碼牧抽。
Android中如何導(dǎo)入Kotlin協(xié)程
根據(jù)Kotlin Coroutines Github repo,我們需要導(dǎo)入kotlinx-coroutines-core和kotlinx-coroutines-android(類(lèi)似于RxJava的io.reactivex.rxjava2:rxandroid遥赚,該庫(kù)支持Android主線程扬舒,同時(shí)保證未捕獲的異常可以在應(yīng)用崩潰前輸出日志)凫佛。如果項(xiàng)目里使用了RxJava讲坎,可以導(dǎo)入kotlinx-coroutines-rx2來(lái)同時(shí)使用RxJava和協(xié)程孕惜,這個(gè)庫(kù)幫助將RxJava代碼轉(zhuǎn)為協(xié)程。
添加如下代碼導(dǎo)入
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.2"
記得添加最新的Kotlin版本到根build.gradle:
buildscript {
ext.kotlin_version = '1.3.50'
repositories {
jcenter()
...
}
...
}
OK晨炕,準(zhǔn)備工作已就緒衫画,讓我們開(kāi)始吧~
內(nèi)容目錄
- 掛起函數(shù)(Suspending functions)
- 協(xié)程作用域 (Coroutine scope)
(1) 自定義作用域(CoroutineScope)
(2) 主作用域(MainScope)
(3) 全局作用域(GlobalScope) - 協(xié)程上下文(Coroutine context)
(1) 調(diào)度器(Dispatchers)
(2) 協(xié)程異常處理器(CoroutineExceptionHandler)
(3) 任務(wù)(Job)
— (3.1) 父-子層級(jí)(Parent-child hierarchies)
— (3.2) SupervisorJob v.s. Job - 協(xié)程構(gòu)建器 (Coroutine builder)
(1) launch
(2) async - 協(xié)程體(Coroutine body)
協(xié)程基礎(chǔ)
先看看協(xié)程長(zhǎng)啥樣:
CoroutineScope(Dispatchers.Main + Job()).launch {
val user = fetchUser() // A suspending function running in the I/O thread.
updateUser(user) // Updates UI in the main thread.
}
private suspend fun fetchUser(): User = withContext(Dispatchers.IO) {
// Fetches the data from server and returns user data.
}
這段代碼在后臺(tái)線程拉取服務(wù)器數(shù)據(jù),然后回到主線程更新UI.
1. 掛起函數(shù)(Suspending functions)
掛起函數(shù)是Kotlin協(xié)程中的特殊函數(shù)瓮栗,用關(guān)鍵字suspend定義削罩。掛起函數(shù)可以中斷(suspend)當(dāng)前協(xié)程的執(zhí)行,這意味著它一直等待费奸,直到掛起函數(shù)恢復(fù)(resume)弥激。因?yàn)檫@篇博客關(guān)注協(xié)程的基本概念, Android中的Kotlin協(xié)程-掛起函數(shù)將會(huì)討論更多細(xì)節(jié)
我們回過(guò)頭來(lái)看看上面的代碼货邓,它可以分為4個(gè)部分:
2. 協(xié)程作用域(Coroutine scope)
為新協(xié)程定義一個(gè)作用域秆撮。每個(gè)協(xié)程構(gòu)建器都是CoroutineScope的拓展,繼承其coroutineContext以自動(dòng)傳遞上下文對(duì)象和取消换况。
所有的協(xié)程都在協(xié)程作用域里運(yùn)行职辨,并接受一個(gè)CoroutineContext(協(xié)程上下文,后文詳述)作為參數(shù)戈二。有幾個(gè)作用域我們可以使用:
(1) CoroutineScope
用自定義的協(xié)程上下文創(chuàng)建作用域舒裤。例如,根據(jù)我們的需要觉吭,指定線程腾供、父job和異常處理器(the thread, parent job and exception handler):
CoroutineScope(Dispatchers.Main + job + exceptionHandler).launch {
...
}
(2) MainScope
為UI組件創(chuàng)建一個(gè)主作用域。它使用SupervisorJob()鲜滩,在主線程運(yùn)行伴鳖,這意味著如果它的某個(gè)子任務(wù)(child job)失敗了,不會(huì)影響其他子任務(wù)徙硅。
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
(3) GlobalScope
這個(gè)作用域不跟任何任務(wù)(job)綁定榜聂。它用來(lái)啟動(dòng)頂級(jí)協(xié)程,這些協(xié)程可以運(yùn)行在整個(gè)的應(yīng)用生命周期嗓蘑,且永遠(yuǎn)不能取消须肆。
3. 協(xié)程上下文(Coroutine context)
協(xié)程總是運(yùn)行在某個(gè)CoroutineContext類(lèi)型的上下文中。協(xié)程上下文是一系列元素桩皿,用來(lái)指定線程策略豌汇、異常處理器、控制協(xié)程生命周期等泄隔【芗可以用+操作符將這些元素組合起來(lái)。
有3種最重要的協(xié)程上下文:調(diào)度器佛嬉,協(xié)程異常處理器柜思,任務(wù)(Dispatchers岩调,CoroutineExceptionHandler,Job)
(1) 調(diào)度器(Dispatchers)
指定協(xié)程在哪個(gè)線程執(zhí)行赡盘。協(xié)程可以隨時(shí)用withContext()
切換線程号枕。
Dispatchers.Default
使用共享的后臺(tái)線程緩存池。默認(rèn)情況下陨享,它使用的最大線程數(shù)等于CPU內(nèi)核數(shù)葱淳,但至少2個(gè)。這個(gè)線程看起來(lái)會(huì)像是Thread[DefaultDispatcher-worker-2,5,main]
.
Dispatchers.IO
跟Dispatchers.Default共享線程抛姑,但它數(shù)量受kotlinx.coroutines.io.parallelism限制赞厕,默認(rèn)最多是64個(gè)線程或CPU內(nèi)核數(shù)(其中的大值)。跟Dispatchers.Default一樣定硝,線程看起來(lái)像Thread[DefaultDispatcher-worker-1,5,main].
Dispatchers.Main
等效于主線程皿桑。線程看起來(lái)像Thread[main,5,main].
Dispatchers.Unconfined
未指定特定線程的協(xié)程分發(fā)器。協(xié)程在當(dāng)前線程執(zhí)行蔬啡,并讓協(xié)程恢復(fù)到對(duì)應(yīng)的suspending function用過(guò)的任意線程上诲侮。
CoroutineScope(Dispatchers.Unconfined).launch {
// Writes code here running on Main thread.
delay(1_000)
// Writes code here running on `kotlinx.coroutines.DefaultExecutor`.
withContext(Dispatchers.IO) { ... }
// Writes code running on I/O thread.
withContext(Dispatchers.Main) { ... }
// Writes code running on Main thread.
}
(2) CoroutineExceptionHandler
處理未捕獲的異常。
一般的箱蟆, 未捕獲異常只會(huì)從launch構(gòu)建器創(chuàng)建的協(xié)程中拋出. async構(gòu)建器創(chuàng)建的協(xié)程總是捕獲所有的異常沟绪,并在返回的Deferred對(duì)象中表示.
例子1:不能通過(guò)外層try-catch捕獲IOException()。不能用try-catch包圍整個(gè)協(xié)程作用域空猜,否則應(yīng)用還是會(huì)崩潰绽慈。
try {
CoroutineScope(Dispatchers.Main).launch {
doSomething()
}
} catch (e: IOException) {
// 無(wú)法捕獲IOException()
Log.d("demo", "try-catch: $e")
}
private suspend fun doSomething() {
delay(1_000)
throw IOException()
}
例子2:用CoroutineExceptionHandler捕獲IOException()。除CancellationException外的其他異常辈毯,如IOException()坝疼,將傳遞給CoroutineExceptionHandler。
// Handles coroutine exception here.
val handler = CoroutineExceptionHandler { _, throwable ->
Log.d("demo", "handler: $throwable") // Prints "handler: java.io.IOException"
}
CoroutineScope(Dispatchers.Main + handler).launch {
doSomething()
}
private suspend fun doSomething() {
delay(1_000)
throw IOException()
}
例子3:CancellationException()會(huì)被忽略谆沃。
如果協(xié)程拋出CancellationException钝凶,它將會(huì)被忽略(因?yàn)檫@是取消運(yùn)行中的協(xié)程的預(yù)期機(jī)制,所以該異常不會(huì)傳遞給CoroutineExceptionHandler)(譯注:不會(huì)導(dǎo)致崩潰)
// Handles coroutine exception here.
val handler = CoroutineExceptionHandler { _, throwable ->
// Won't print the log because the exception is "CancellationException()".
Log.d("demo", "handler: $throwable")
}
CoroutineScope(Dispatchers.Main + handler).launch {
doSomething()
}
private suspend fun doSomething() {
delay(1_000)
throw CancellationException()
}
例子4:用invokeOnCompletion可以獲取所有異常信息管毙。
CancellationException不會(huì)傳遞給CoroutineExceptionHandler腿椎,但當(dāng)該異常發(fā)生時(shí)桌硫,如果我們想打印出某些信息夭咬,可以使用invokeOnCompletion來(lái)獲取。
val job = CoroutineScope(Dispatchers.Main).launch {
doSomething()
}
job.invokeOnCompletion {
val error = it ?: return@invokeOnCompletion
// Prints "invokeOnCompletion: java.util.concurrent.CancellationException".
Log.d("demo", "invokeOnCompletion: $error")
}
}
private suspend fun doSomething() {
delay(1_000)
throw CancellationException()
}
(3) Job
控制協(xié)程的生命周期铆隘。一個(gè)協(xié)程有如下?tīng)顟B(tài):
查詢(xún)job的當(dāng)前狀態(tài)很簡(jiǎn)單卓舵,用Job.isActive。
狀態(tài)流圖是:
- 協(xié)程工作時(shí)job是active態(tài)的
- job發(fā)生異常時(shí)將會(huì)變成cancelling. 一個(gè)job可以隨時(shí)用cancel方法取消膀钠,這個(gè)強(qiáng)制使它立刻變?yōu)閏ancelling態(tài)
- 當(dāng)job工作完成時(shí)掏湾,會(huì)變成cancelled態(tài)
- 父job會(huì)維持在completing或cancelling態(tài)直到所有子job完成裹虫。注意completing是一種內(nèi)部狀態(tài),對(duì)外部來(lái)說(shuō)融击,completing態(tài)的job仍然是active的筑公。
(3.1) Parent-child hierarchies(父-子層級(jí))
弄明白狀態(tài)后,我門(mén)還必須知道父-子層級(jí)是如何工作的尊浪。假設(shè)我們寫(xiě)了如下代碼:
val parentJob = Job()
val childJob1 = CoroutineScope(parentJob).launch {
val childJob2 = launch { ... }
val childJob3 = launch { ... }
}
則其父子層級(jí)會(huì)長(zhǎng)這樣:
我們可以改變父job匣屡,像這樣:
val parentJob1 = Job()
val parentJob2 = Job()
val childJob1 = CoroutineScope(parentJob1).launch {
val childJob2 = launch { ... }
val childJob3 = launch(parentJob2) { ... }
}
則父子層級(jí)會(huì)長(zhǎng)這樣:
基于以上知識(shí),我們需要知道如下一些重要概念:
-
父job取消將立即導(dǎo)致所有子job取消
val parentJob = Job() CoroutineScope(Dispatchers.Main + parentJob).launch { val childJob = launch { delay(5_000) // This function won't be executed because its parentJob is // already cancelled after 1 sec. canNOTBeExcecuted() } launch { delay(1_000) parentJob.cancel() // Cancels parent job after 1 sec. } }
當(dāng)某個(gè)子job因?yàn)槌?a target="_blank">CancellationException外的異常而失敗或取消時(shí)拇涤,會(huì)立刻導(dǎo)致所有父job和其他子job取消捣作。但如果是CancellationException,則除該job的子job外的其他jobs不會(huì)受到影響鹅士。
例子1:如果拋出CancellationException券躁,只有childJob1下的job被取消。
val parentJob = Job()
CoroutineScope(Dispatchers.Main + parentJob).launch {
val childJob1 = launch {
val childOfChildJob1 = launch {
delay(2_000)
// This function won't be executed since childJob1 is cancelled.
canNOTBeExecuted()
}
delay(1_000)
// Cancel childJob1.
cancel()
}
val childJob2 = launch {
delay(2_000)
canDoSomethinghHere()
}
delay(3_000)
canDoSomethinghHere()
}
例子2:如果某個(gè)子job拋出IOException掉盅,則所有關(guān)聯(lián)job都會(huì)被取消
val parentJob = Job()
val handler = CoroutineExceptionHandler { _, throwable ->
Log.d("demo", "handler: $throwable") // Prints "handler: java.io.IOException"
}
CoroutineScope(Dispatchers.Main + parentJob + handler).launch {
val childJob1 = launch {
delay(1_000)
// Throws any exception "other than CancellationException" after 1 sec.
throw IOException()
}
val childJob2 = launch {
delay(2_000)
// The other child job: this function won't be executed.
canNOTBExecuted()
}
delay(3_000)
// Parent job: this function won't be executed.
canNOTBExecuted()
}
- cancelChildren(): 父job可以取消它的所有子job(遞歸到它們的子job)而不取消自己也拜。注意:如果一個(gè)job已取消,則它不能再作為父job運(yùn)行協(xié)程了怔接。
如果我們用Job.cancel()搪泳,父job將會(huì)變成cancelled(當(dāng)前是Cancelling),當(dāng)其所有子job都cancelled后扼脐,父job會(huì)成為cancelled態(tài)岸军。
val parentJob = Job()
val childJob = CoroutineScope(Dispatchers.Main + parentJob).launch {
delay(1_000)
// This function won't be executed because its parent is cancelled.
canNOTBeExecuted()
}
parentJob.cancel()
// Prints "JobImpl{Cancelling}@199d143", parent job status becomes "cancelling".
// And will be "cancelled" after all the child job is cancelled.
Log.d("demo", "$parentJob")
而如果我們用Job.cancelChildren(),父job將會(huì)變?yōu)锳ctive態(tài)瓦侮,我們?nèi)匀豢梢杂盟鼇?lái)運(yùn)行其他協(xié)程艰赞。
val parentJob = Job()
val childJob = CoroutineScope(Dispatchers.Main + parentJob).launch {
delay(1_000)
// This function won't be executed because its parent job is cancelled.
canNOTBeExecuted()
}
// Only children are cancelled, the parent job won't be cancelled.
parentJob.cancelChildren()
// Prints "JobImpl{Active}@199d143", parent job is still active.
Log.d("demo", "$parentJob")
val childJob2 = CoroutineScope(Dispatchers.Main + parentJob).launch {
delay(1_000)
// Since the parent job is still active, we could use it to run child job 2.
canDoSomethingHere()
}
(3.2) SupervisorJob v.s. Job
supervisor job的子job可以獨(dú)立失敗,而不影響其他子job肚吏。
正如前文提到的方妖,如果我們用Job()作為父job,當(dāng)某個(gè)子job失敗時(shí)將會(huì)導(dǎo)致所有子job取消罚攀。
val parentJob = Job()
val handler = CoroutineExceptionHandler { _, _ -> }
val scope = CoroutineScope(Dispatchers.Default + parentJob + handler)
val childJob1 = scope.launch {
delay(1_000)
// ChildJob1 fails with the IOException().
throw IOException()
}
val childJob2 = scope.launch {
delay(2_000)
// This line won't be executed due to childJob1 failure.
canNOTBeExecuted()
}
如果我們使用SupervisorJob()作為父job党觅,則其中一個(gè)子job取消時(shí)不會(huì)影響其他子jobs。
val parentJob = SupervisorJob()
val handler = CoroutineExceptionHandler { _, _ -> }
val scope = CoroutineScope(Dispatchers.Default + parentJob + handler)
val childJob1 = scope.launch {
delay(1_000)
// ChildJob1 fails with the IOException().
throw IOException()
}
val childJob2 = scope.launch {
delay(2_000)
// Since we use SupervisorJob() as parent job, the failure of
// childJob1 won't affect other child jobs. This function will be
// executed.
canDoSomethinghHere()
}
4. 協(xié)程構(gòu)建器(Coroutines Builder)
(1) launch
啟動(dòng)一個(gè)新協(xié)程斋泄,不會(huì)阻塞當(dāng)前線程杯瞻,返回一個(gè)指向當(dāng)前協(xié)程的Job引用。
(2) async and await
async協(xié)程構(gòu)建器是CoroutineScope的拓展方法炫掐。它創(chuàng)建一個(gè)協(xié)程魁莉,并以Deferred實(shí)現(xiàn)來(lái)返回它的未來(lái)結(jié)果,這是一個(gè)非阻塞的可取消future——一個(gè)帶結(jié)果的Job。
Async協(xié)程搭配await使用:不阻塞當(dāng)前線程的前提下持續(xù)等待結(jié)果旗唁,并在可延遲的任務(wù)完成后恢復(fù)(resume)畦浓,返回結(jié)果,或者如果deferred被取消了检疫,拋出相應(yīng)的異常讶请。
下列代碼展示了兩個(gè)suspending functions的串行調(diào)用。在fetchDataFromServerOne()和fetchDataFromServerTwo()中屎媳,我們做了一些耗時(shí)任務(wù)秽梅,分別耗時(shí)1秒。在launch構(gòu)建器里調(diào)用它們剿牺,會(huì)發(fā)現(xiàn)最終的耗時(shí)是它們的和:2秒企垦。
override fun onCreate(savedInstanceState: Bundle?) {
...
val scope = MainScope()
scope.launch {
val time = measureTimeMillis {
val one = fetchDataFromServerOne()
val two = fetchDataFromServerTwo()
Log.d("demo", "The sum is ${one + two}")
}
Log.d("demo", "Completed in $time ms")
}
}
private suspend fun fetchDataFromServerOne(): Int {
Log.d("demo", "fetchDataFromServerOne()")
delay(1_000)
return 1
}
private suspend fun fetchDataFromServerTwo(): Int {
Log.d("demo", "fetchDataFromServerTwo()")
delay(1_000)
return 2
}
日志是:
2019-12-09 00:00:34.547 D/demo: fetchDataFromServerOne()
2019-12-09 00:00:35.553 D/demo: fetchDataFromServerTwo()
2019-12-09 00:00:36.555 D/demo: The sum is 3
2019-12-09 00:00:36.555 D/demo: Completed in 2008 ms
耗時(shí)是兩個(gè)suspending functions延時(shí)的和。該協(xié)程在fetchDataFromServerOne()結(jié)束前會(huì)中斷(suspend)晒来,然后執(zhí)行fetchDataFromServerTwo()钞诡。
如果我們想同時(shí)運(yùn)行兩個(gè)方法以減少耗時(shí)呢?Async閃亮登場(chǎng)湃崩!Async和launch很像荧降。它啟動(dòng)一個(gè)可以和其他協(xié)程同時(shí)運(yùn)行的新協(xié)程,返回Deferred引用——一個(gè)帶返回值的Job攒读。
public interface Deferred<out T> : Job {
public suspend fun await(): T
...
}
在Deferred上調(diào)用await()獲取結(jié)果朵诫,例如:
override fun onCreate(savedInstanceState: Bundle?) {
...
val scope = MainScope()
scope.launch {
val time = measureTimeMillis {
val one = async { fetchDataFromServerOne() }
val two = async { fetchDataFromServerTwo() }
Log.d("demo", "The sum is ${one.await() + two.await()}")
}
// Function one and two will run asynchrously,
// so the time cost will be around 1 sec only.
Log.d("demo", "Completed in $time ms")
}
}
private suspend fun fetchDataFromServerOne(): Int {
Log.d("demo", "fetchDataFromServerOne()")
delay(1_000)
return 1
}
private suspend fun fetchDataFromServerTwo(): Int {
Log.d("demo", "fetchDataFromServerTwo()")
Thread.sleep(1_000)
return 2
}
日志是:
2019-12-08 23:52:01.714 D/demo: fetchDataFromServerOne()
2019-12-08 23:52:01.718 D/demo: fetchDataFromServerTwo()
2019-12-08 23:52:02.722 D/demo: The sum is 3
2019-12-08 23:52:02.722 D/demo: Completed in 1133 ms
5. 協(xié)程體(Coroutine body)
在CoroutineScope中運(yùn)行的代碼,包括常規(guī)函數(shù)或掛起函數(shù)——掛起函數(shù)在結(jié)束前會(huì)中斷協(xié)程薄扁,下篇博客將會(huì)詳述剪返。
今天就到這里啦。下篇博客將會(huì)深入介紹掛起函數(shù)及其用法邓梅。 Android中的Kotlin協(xié)程-掛起函數(shù).