5.協(xié)程上下文
CoroutineContext
表示協(xié)程上下文,是 Kotlin 協(xié)程的一個(gè)基本結(jié)構(gòu)單元。協(xié)程上下文主要承載著資源獲取,配置管理等工作,是執(zhí)行環(huán)境的通用數(shù)據(jù)資源的統(tǒng)一管理者。它有很多作用,包括攜帶參數(shù),攔截協(xié)程執(zhí)行等等。如何運(yùn)用協(xié)程上下文是至關(guān)重要的,以此來實(shí)現(xiàn)正確的線程行為、生命周期、異常以及調(diào)試。
協(xié)程使用以下幾種元素集定義協(xié)程的行為,它們均繼承自CoroutineContext
:
Job
:??????????協(xié)程的句柄,對(duì)協(xié)程的控制和管理生命周期。CoroutineName
:??????協(xié)程的名稱,可用于調(diào)試。CoroutineDispatcher
:???調(diào)度器,確定協(xié)程在指定的線程來執(zhí)行。CoroutineExceptionHandler
:協(xié)程異常處理器,處理未捕獲的異常。這里暫不做深入分析,后面的文章會(huì)講解到,敬請(qǐng)期待。
協(xié)程上下文的數(shù)據(jù)結(jié)構(gòu)特征更加顯著,與List和Map非常類似。它包含用戶定義的一些數(shù)據(jù)集合,這些數(shù)據(jù)與協(xié)程密切相關(guān)。它是一個(gè)有索引的 Element
實(shí)例集合。每個(gè) element
在這個(gè)集合有一個(gè)唯一的Key
。
//協(xié)程的持久上下文。它是[Element]實(shí)例的索引集,這個(gè)集合中的每個(gè)元素都有一個(gè)唯一的[Key]。
public interface CoroutineContext {
//從這個(gè)上下文中返回帶有給定[key]的元素或null。
public operator fun get(key: Key<E>): E?
//從[initial]值開始累加該上下文的項(xiàng),并從左到右應(yīng)用[operation]到當(dāng)前累加器值和該上下文的每個(gè)元素。
public fun fold(initial: R, operation: (R, Element) -> R): R
//返回一個(gè)上下文,包含來自這個(gè)上下文的元素和來自其他[context]的元素。
public operator fun plus(context: CoroutineContext): CoroutineContext
//返回一個(gè)包含來自該上下文的元素的上下文,但不包含指定的[key]元素。
public fun minusKey(key: Key<*>): CoroutineContext
//[CoroutineContext]元素的鍵。[E]是帶有這個(gè)鍵的元素類型。
public interface Key<E : Element>
//[CoroutineContext]的一個(gè)元素。協(xié)程上下文的一個(gè)元素本身就是一個(gè)單例上下文。
public interface Element : CoroutineContext {
//這個(gè)協(xié)程上下文元素的key
public val key: Key<*>
public override operator fun get(key: Key<E>): E?
}
}
:?可以通過get(key) key
從這個(gè)上下文中獲取這個(gè)Element
元素或者null
。fold()
:????提供遍歷當(dāng)前上下文中所有元素的能力。plus(context)
:?顧名思義它是一個(gè)加法運(yùn)算,多個(gè)上下文元素可以通過+
的形式整合成一個(gè)上下文返回。minusKey(key)
:?與plus
相反,減法運(yùn)算,刪除當(dāng)前上下文中指定key
的元素,返回的是不包含指定Element
:????協(xié)程上下文的一個(gè)元素,本身就是一個(gè)單例上下文,里面有一個(gè)key
,是這個(gè)元素的索引。
Element
本身也實(shí)現(xiàn)了CoroutineContext
接口,像Int
實(shí)現(xiàn)了List
一樣,為什么元素本身也是集合呢?主要是Element
它不會(huì)存放除它自己以外的數(shù)據(jù);Element
屬性又有一個(gè)key
,是協(xié)程上下文這個(gè)集合中元素的索引。這個(gè)索引在元素里面,說明元素一產(chǎn)生就找到自己的位置。
注意:協(xié)程上下文的內(nèi)部實(shí)現(xiàn)實(shí)際是一個(gè)單鏈表。
CoroutineName
//用戶指定的協(xié)程名稱。此名稱用于調(diào)試模式。
public data class CoroutineName(
//定義協(xié)程的名字
val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
//CoroutineName實(shí)例在協(xié)程上下文中的key
public companion object Key : CoroutineContext.Key<CoroutineName>
}
CoroutineName
是用戶用來指定的協(xié)程名稱的,用于方便調(diào)試和定位問題:
GlobalScope.launch(CoroutineName("GlobalScope")) {
launch(CoroutineName("CoroutineA")) {//指定協(xié)程名稱
val coroutineName = coroutineContext[CoroutineName]//獲取協(xié)程名稱
print(coroutineName)
}
}
協(xié)程內(nèi)部可以通過coroutineContext
這個(gè)全局屬性直接獲取當(dāng)前協(xié)程的上下文。打印數(shù)據(jù)如下:
kotlin
復(fù)制代碼[DefaultDispatcher-worker-2] CoroutineName(CoroutineA)
上下文組合
從上面的協(xié)程創(chuàng)建的函數(shù)中可以看到,協(xié)程上下文的參數(shù)只有一個(gè),但是怎么傳遞多個(gè)上下文元素呢?CoroutineContext
可以使用 " + " 運(yùn)算符進(jìn)行合并。由于CoroutineContext
是由一組元素組成的,所以加號(hào)右側(cè)的元素會(huì)覆蓋加號(hào)左側(cè)的元素,進(jìn)而組成新創(chuàng)建的CoroutineContext
。
GlobalScope.launch {
//通過+號(hào)運(yùn)算添加多個(gè)上下文元素
var context = CoroutineName("協(xié)程1") + Dispatchers.Main
print("context == $context")
context += Dispatchers.IO //添加重復(fù)Dispatchers元素,Dispatchers.IO 會(huì)替換 ispatchers.Main
print("context == $context")
val contextResult = context.minusKey(context[CoroutineName]!!.key)//移除CoroutineName元素
print("contextResult == $contextResult")
}
注意:如果有重復(fù)的元素(key
一致)則會(huì)右邊的會(huì)代替左邊的元素。打印數(shù)據(jù)如下:
context == [CoroutineName(協(xié)程1), Dispatchers.Main]
context == [CoroutineName(協(xié)程1), Dispatchers.IO]
contextResult == Dispatchers.IO
6.啟動(dòng)模式
CoroutineStart
是一個(gè)枚舉類,為協(xié)程構(gòu)建器定義啟動(dòng)選項(xiàng)。在協(xié)程構(gòu)建的start
參數(shù)中使用,
啟動(dòng)模式 | 含義 | 說明 |
---|---|---|
DEFAULT |
默認(rèn)啟動(dòng)模式,立即根據(jù)它的上下文調(diào)度協(xié)程的執(zhí)行 | 是立即調(diào)度,不是立即執(zhí)行,DEFAULT 是餓漢式啟動(dòng),launch 調(diào)用后,會(huì)立即進(jìn)入待調(diào)度狀態(tài),一旦調(diào)度器 OK 就可以開始執(zhí)行。如果協(xié)程在執(zhí)行前被取消,其將直接進(jìn)入取消響應(yīng)的狀態(tài)。 |
LAZY |
懶啟動(dòng)模式,啟動(dòng)后并不會(huì)有任何調(diào)度行為,直到我們需要它執(zhí)行的時(shí)候才會(huì)產(chǎn)生調(diào)度 | 包括主動(dòng)調(diào)用該協(xié)程的start 、join 或者await 等函數(shù)時(shí)才會(huì)開始調(diào)度,如果調(diào)度前就被取消,協(xié)程將直接進(jìn)入異常結(jié)束狀態(tài)。 |
ATOMIC |
類似[DEFAULT],以一種不可取消的方式調(diào)度協(xié)程的執(zhí)行 | 雖然是立即調(diào)度,但其將調(diào)度和執(zhí)行兩個(gè)步驟合二為一了,就像它的名字一樣,其保證調(diào)度和執(zhí)行是原子操作,因此協(xié)程也一定會(huì)執(zhí)行。 |
UNDISPATCHED |
類似[ATOMIC],立即執(zhí)行協(xié)程,直到它在當(dāng)前線程中的第一個(gè)掛起點(diǎn)。 | 是立即執(zhí)行,因此協(xié)程一定會(huì)執(zhí)行。即使協(xié)程已經(jīng)被取消,它也會(huì)開始執(zhí)行,但不同之處在于它在同一個(gè)線程中開始執(zhí)行。 |
這些啟動(dòng)模式的設(shè)計(jì)主要是為了應(yīng)對(duì)某些特殊的場景。業(yè)務(wù)開發(fā)實(shí)踐中通常使用DEFAULT
和LAZY
這兩個(gè)啟動(dòng)模式就夠了。
7.suspend 掛起函數(shù)
suspend
是 Kotlin 協(xié)程最核心的關(guān)鍵字,使用suspend
關(guān)鍵字修飾的函數(shù)叫作掛起函數(shù)
,掛起函數(shù)
只能在協(xié)程體內(nèi)或者在其他掛起函數(shù)
內(nèi)調(diào)用。否則 IDE 就會(huì)提示一個(gè)錯(cuò)誤:
Suspend function 'xxxx' should be called only from a coroutine or another suspend function
協(xié)程提供了一種避免阻塞線程并用更簡單、更可控的操作替代線程阻塞的方法:協(xié)程掛起和恢復(fù) 。協(xié)程在執(zhí)行到有suspend
標(biāo)記的函數(shù)時(shí),當(dāng)前函數(shù)會(huì)被掛起(暫停),直到該掛起函數(shù)內(nèi)部邏輯完成,才會(huì)在掛起的地方resume
恢復(fù)繼續(xù)執(zhí)行。
本質(zhì)上,掛起函數(shù)就是一個(gè)提醒作用,函數(shù)創(chuàng)建者給函數(shù)調(diào)用者的提醒,表示這是一個(gè)比較耗時(shí)的任務(wù),被創(chuàng)建者用suspend
標(biāo)記函數(shù),調(diào)用者只需把掛起函數(shù)放在協(xié)程里面,協(xié)程會(huì)自動(dòng)調(diào)度處理,完成后在原來的位置恢復(fù)執(zhí)行。
注意:協(xié)程會(huì)在主線程中運(yùn)行,suspend 并不代表后臺(tái)執(zhí)行。
如果需要處理一個(gè)函數(shù),且這個(gè)函數(shù)在主線程上執(zhí)行太耗時(shí),但是又要保證這個(gè)函數(shù)是主線程安全的,那么您可以讓 Kotlin 協(xié)程在 Default 或 IO 調(diào)度器上執(zhí)行工作。在 Kotlin 中,所有協(xié)程都必須在調(diào)度器中運(yùn)行,即使它們是在主線程上運(yùn)行也是如此。協(xié)程可以 自行掛起(暫停) ,而調(diào)度器負(fù)責(zé)將其 恢復(fù) 。
掛起點(diǎn)
協(xié)程內(nèi)部掛起函數(shù)調(diào)用的地方稱為掛起點(diǎn) ,或者有下面這個(gè)標(biāo)識(shí)的表示這個(gè)就是掛起點(diǎn)。
掛起和恢復(fù)
協(xié)程在常規(guī)函數(shù)的基礎(chǔ)上添加了suspend
和 resume
兩項(xiàng)操作用于處理長時(shí)間運(yùn)行的任務(wù):
suspend
:也稱掛起或暫停,用于掛起(暫停)執(zhí)行當(dāng)前協(xié)程,并保存所有局部變量。resume
:恢復(fù),用于讓已掛起(暫停)的協(xié)程從掛起(暫停)處恢復(fù)繼續(xù)執(zhí)行。
Kotlin 使用堆棧幀來管理要運(yùn)行哪個(gè)函數(shù)以及所有局部變量。 掛起 (暫停)協(xié)程時(shí),會(huì)復(fù)制并保存當(dāng)前的堆棧幀以供稍后使用,將信息保存到Continuation
對(duì)象中。恢復(fù)協(xié)程時(shí),會(huì)將堆棧幀從其保存位置復(fù)制回來,對(duì)應(yīng)的Continuation
通過調(diào)用resumeWith
函數(shù)才會(huì)恢復(fù)協(xié)程的執(zhí)行,然后函數(shù)再次開始運(yùn)行。同時(shí)返回Result
類型的成功或者異常的結(jié)果。
//Continuation接口表示掛起點(diǎn)之后的延續(xù),該掛起點(diǎn)返回類型為“T”的值。
public interface Continuation<in T> {
//對(duì)應(yīng)這個(gè)Continuation的協(xié)程上下文
public val context: CoroutineContext
//恢復(fù)相應(yīng)協(xié)程的執(zhí)行,傳遞一個(gè)成功或失敗的結(jié)果作為最后一個(gè)掛起點(diǎn)的返回值。
public fun resumeWith(result: Result<T>)
}
//將[value]作為最后一個(gè)掛起點(diǎn)的返回值,恢復(fù)相應(yīng)協(xié)程的執(zhí)行。
fun Continuation.resume(value: T): Unit =
resumeWith(Result.success(value))
//恢復(fù)相應(yīng)協(xié)程的執(zhí)行,以便在最后一個(gè)掛起點(diǎn)之后重新拋出[異常]。
fun Continuation.resumeWithException(exception: Throwable): Unit =
resumeWith(Result.failure(exception))
Kotlin 的 Continuation
類有一個(gè) resumeWith
函數(shù)可以接收 Result 類型的參數(shù)。在結(jié)果成功獲取時(shí),調(diào)用resumeWith(Result.success(value))
或者調(diào)用拓展函數(shù)resume(value)
;出現(xiàn)異常時(shí),調(diào)用resumeWith(Result.failure(exception))
或者調(diào)用拓展函數(shù)resumeWithException(exception)
,這就是 Continuation
的恢復(fù)調(diào)用。
Continuation
類似于網(wǎng)絡(luò)請(qǐng)求回調(diào)Callback
,也是一個(gè)請(qǐng)求成功和一個(gè)請(qǐng)求失敗的回調(diào):
public interface Callback {
//請(qǐng)求失敗回調(diào)
void onFailure(Call call, IOException e);
//請(qǐng)求成功回調(diào)
void onResponse(Call call, Response response) throws IOException;
}
注意:suspend
不一定真的會(huì)掛起,如果只是提供了掛起的條件,但是協(xié)程沒有產(chǎn)生異步調(diào)用,那么協(xié)程還是不會(huì)被掛起。
那么協(xié)程是如何做到掛起和恢復(fù)?
suspend本質(zhì)(奪命七步)
一個(gè)掛起函數(shù)
要掛起,那么它必定得有一個(gè)掛起點(diǎn)
,不然無法知道函數(shù)是否掛起,從哪掛起呢?
@GET("users/{login}")
suspend fun getUserSuspend(@Path("login") login: String): User
第一步 :將上面的掛起函數(shù)解析成字節(jié)碼:通過AS的工具欄中Tools
->kotlin
->show kotlin ByteCode
kotlin
復(fù)制代碼public abstract getUserSuspend(Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
上面的掛起函數(shù)本質(zhì)是這樣的,你會(huì)發(fā)現(xiàn)多了一個(gè)參數(shù),這個(gè)參數(shù)就是Continuation
,也就是說調(diào)用掛起函數(shù)的時(shí)候需要傳遞一個(gè)Continuation
給它,只是傳遞這個(gè)參數(shù)是由編譯器悄悄傳,而不是我們傳遞的。這就是掛起函數(shù)為什么只能在協(xié)程或者其他掛起函數(shù)中執(zhí)行,因?yàn)橹挥袙炱鸷瘮?shù)或者協(xié)程中才有Continuation
。
第二步 :這里的Continuation
參數(shù),其實(shí)它類似CallBack
回調(diào)函數(shù),resumeWith()
就是成功或者失敗回調(diào)的結(jié)果:
public interface Continuation<in T> {
//協(xié)程上下文
public val context: CoroutineContext
//恢復(fù)相應(yīng)協(xié)程的執(zhí)行,傳遞一個(gè)成功或失敗的[result]作為最后一個(gè)掛起點(diǎn)的返回值。
public fun resumeWith(result: Result<T>)
}
第三步 :但是它是從哪里傳進(jìn)來的呢?這個(gè)函數(shù)只能在協(xié)程或者掛起函數(shù)中執(zhí)行,說明Continuation
很有可能是從協(xié)程充傳入來的,查看協(xié)程構(gòu)建的源碼:
public fun CoroutineScope.launch(): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
第四步 :通過launch
啟動(dòng)一個(gè)協(xié)程的時(shí)候,他通過coroutine
的start
方法啟動(dòng)協(xié)程:
public fun start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
initParentJob()
start(block, receiver, this)
}
第五步 :然后start
方法里面調(diào)用了CoroutineStart
的invoke
,這個(gè)時(shí)候我們發(fā)現(xiàn)了Continuation
:
public operator fun invoke(block: suspend () -> T, completion: Continuation<T>): Unit =
when (this) {
DEFAULT -> block.startCoroutineCancellable(completion)
ATOMIC -> block.startCoroutine(completion)
UNDISPATCHED -> block.startCoroutineUndispatched(completion)
LAZY -> Unit // will start lazily
}
第六步 :而 Continuation
通過block.startCoroutine(completion)
傳入:
public fun (suspend () -> T).startCoroutine(completion: Continuation
第七步 :最終回調(diào)到上面Continuation
的resumeWith()
恢復(fù)函數(shù)里面。這里可以看出協(xié)程體本身就是一個(gè)Continuation
,這也就解釋了為什么必須要在協(xié)程內(nèi)調(diào)用suspend
掛起函數(shù)了。(由于篇幅原因這里不做深入分析,后續(xù)的文章會(huì)分析這里,敬請(qǐng)期待!)
額外知識(shí)點(diǎn):在創(chuàng)建協(xié)程的底層源碼中,創(chuàng)建協(xié)程會(huì)返回一個(gè)
Continuation
實(shí)例,這個(gè)實(shí)例就是套了幾層馬甲的協(xié)程體,調(diào)用它的resume
可以觸發(fā)協(xié)程的執(zhí)行。
任何一個(gè)協(xié)程體或者掛起函數(shù)中都隱含有一個(gè)Continuation
實(shí)例,編譯器能夠?qū)@個(gè)實(shí)例進(jìn)行正確的傳遞,并將這個(gè)細(xì)節(jié)隱藏在協(xié)程的背后,讓我們的異步代碼看起來像同步代碼一樣。
@GET("users/{login}")
suspend fun getUserSuspend(@Path("login") login: String): User
GlobalScope.launch(Dispatchers.Main) {//開始協(xié)程:主線程
val result = userApi.getUserSuspend("suming")//網(wǎng)絡(luò)請(qǐng)求(IO 線程)
tv_name.text = result?.name //更新 UI(主線程)
}
launch()
創(chuàng)建的這個(gè)協(xié)程,在執(zhí)行到某一個(gè)suspend
掛起函數(shù)的時(shí)候,這個(gè)協(xié)程會(huì)被掛起,從當(dāng)前線程掛起。也就是說這個(gè)協(xié)程從正在執(zhí)行它的線程上脫離,這個(gè)協(xié)程在掛起函數(shù)指定的線程上繼續(xù)執(zhí)行,當(dāng)協(xié)程的任務(wù)完成時(shí),再resume
恢復(fù)切換到原來的線程上繼續(xù)執(zhí)行。
在主線程進(jìn)行的 suspend 和 resume 的兩個(gè)操作, 既實(shí)現(xiàn)了將耗時(shí)任務(wù)交由后臺(tái)線程完成,保障了主線程安全 ,也在不增加代碼復(fù)雜度和保證代碼可讀性的前提下做到不阻塞主線程的執(zhí)行??梢哉f,在 Android 平臺(tái)上協(xié)程主要就用來解決異步和切換線程這兩個(gè)問題。
-
JAVA
+關(guān)注
關(guān)注
19文章
2952瀏覽量
104479 -
編程
+關(guān)注
關(guān)注
88文章
3565瀏覽量
93536 -
ui
+關(guān)注
關(guān)注
0文章
203瀏覽量
21330 -
kotlin
+關(guān)注
關(guān)注
0文章
60瀏覽量
4179
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論