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

評論