再談協(xié)程之CoroutineContext我能玩一年
點擊上方藍字關(guān)注我,知識會給你力量
Kotlin Coroutines的核心是CoroutineContext接口。所有的coroutine生成器函數(shù),比如launch和async都有相同的第一個參數(shù),即context: CoroutineContext。所有協(xié)程構(gòu)建器都被定義為CoroutineScope接口的擴展函數(shù),該接口有一個抽象的只讀屬性coroutineContext:CoroutineContext。
?每個coroutine builder都是CoroutineScope的擴展,并繼承其coroutineContext以自動傳遞和取消上下文元素。
?
-
launch
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job (source)
-
async
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> (source)
CoroutineContext是Kotlin coroutines的一個基本構(gòu)建模塊。因此,為了實現(xiàn)線程、生命周期、異常和調(diào)試的正確行為,能夠操縱它是至關(guān)重要的。
CoroutineContext創(chuàng)建了一組用來定義協(xié)程行為的元素,它是一個數(shù)據(jù)結(jié)構(gòu),封裝了協(xié)程執(zhí)行的關(guān)鍵信息,它主要包含下面這些部分:
-
Job:協(xié)程的生命周期的句柄 -
協(xié)程調(diào)度器(CoroutineDispatcher) -
CoroutineName:協(xié)程的名字 -
CoroutineExceptionHandler:協(xié)程的異常處理
?當協(xié)程中發(fā)生異常時,如果異常沒有被處理,同時CoroutineExceptionHandler也沒有被設(shè)置,那么異常會被分發(fā)到JVM的ExceptionHandler中,在Android中,如果你沒設(shè)置全局的ExceptionHandler,那么App將會Crash。
?
CoroutineContext的數(shù)據(jù)結(jié)構(gòu)
我們可以來看一眼CoroutineContext的數(shù)據(jù)結(jié)構(gòu)。
?CoroutineContext是一個元素實例的索引集,從數(shù)據(jù)結(jié)構(gòu)上來看,它是set和map的混合體。這個集合中的每個元素都有一個唯一的Key,Key是通過引用來比較的。
?
CoroutineContext接口的API一開始可能看起來很晦澀,但實際上它只是一個類型安全的異構(gòu)map,從CoroutineContext.Key實例(根據(jù)類的文檔,通過引用而不是值進行比較,也就是說,只有同一個對象的key才是相同的)到CoroutineContext.Element實例(這個Map由不同類型的Element組成,Element的索引為Key)。
為了理解為什么必須重新定義一個新的接口,而不是簡單地使用一個標準的Map,我們可以參考下面這樣一個類似的等效申明。
typealias CoroutineContext = Map<CoroutineContext.Key<*>, CoroutineContext.Element>
在這個情況下,get方法就無法從所使用的Key中推斷出獲取的元素類型,盡管這些信息在Key的泛型中實際上是可用的。
fun get(key: CoroutineContext.Key<*>): CoroutineContext.Element?
因此,每當從Map中獲取一個元素時,它需要被轉(zhuǎn)換為實際類型。而在CoroutineContext類中,更加通用的get方法實際上是根據(jù)作為參數(shù)傳遞的Key的泛型來定義返回的Element類型。
fun <E : Element> get(key: Key<E>): E?
這樣,元素就可以安全地被獲取,而不需要進行類型轉(zhuǎn)換,因為它們的類型是在使用的Key中指定的。所以,在真實的CoroutineContext中,get函數(shù),通過Key獲取CoroutineContext中的Element類型元素。調(diào)用者可以通過CoroutineContext[Key]這種方式來獲取Key類型的元素,類似從List中取出索引為index的某個元素——List[index]。
由于Key在CoroutineContext中是靜態(tài)的,所以多個實例共享一個key,所以在這個「Map」里,多個同類型的元素只會存在一個,這樣所有的實例都會是唯一的了。
例如我們要獲取協(xié)程的CoroutineName,就可以通過下面的方式。
coroutineContext[CoroutineName]
?coroutineContext這個屬性是Kotlin在編譯期生成的參數(shù),編譯期會將當前的CoroutineContext傳給每個suspend函數(shù),這樣在CoroutineScope中就可以直接獲取當前的協(xié)程信息。
?
但是這里有點奇怪,為了找到一個CoroutineName,我們只用了CoroutineName。這不是一個類型,也不是一個類,而是個伴生對象。這是Kotlin的一個特點,一個類的名字本身就可以作為其伴生對象的引用,所以coroutineContext[CoroutineName]只是coroutineContext[CoroutineName.Key]的一個簡寫方式。
所以,實際上最原始的寫法應(yīng)該是這樣。
coroutineContext[object : CoroutineContext.Key<CoroutineName> {}]
而在CoroutineName的類中,我們發(fā)現(xiàn)了這樣的代碼。
public companion object Key : CoroutineContext.Key<CoroutineName>
正是這個伴生對象,讓我們可以很方便的引用。
coroutineContext[CoroutineName.Key] ------> coroutineContext[CoroutineName]
這個技巧在協(xié)程庫中使用的非常多。
「+」操作符
CoroutineContext沒有實現(xiàn)一個集合接口,所以它沒有典型的集合相關(guān)的操作符。但它重載了一個重要的操作符,即「加號」操作符。
加號運算符將CoroutineContext實例相互結(jié)合。它會合并它們所包含的元素,用操作符右邊的上下文中的元素覆蓋左邊的上下文中的元素,很像Map上的行為。
?[加號運算符]返回一個包含來自這個上下文的元素和其他上下文的元素的上下文。這個上下文中與另一個上下文中Key值相同的元素會被刪除。
?
CoroutineContext.Element接口實際上繼承了CoroutineContext。這很方便,因為它意味著CoroutineContext.Element實例可以簡單地被視為包含單一元素的CoroutineContext,也就是它們自己。
?Coroutine上下文的一個元素本身就是一個只包含自身的上下文。
?
有了這個「+」運算符,就可以被用來輕松地將元素以及元素與元素之間結(jié)合成一個新的上下文。需要注意的是它們的組合順序,因為+運算符是不對稱的。
?在一個上下文不需要容納任何元素的情況下,可以使用EmptyCoroutineContext對象。可以預(yù)期的是,將這個對象添加到任何其他的上下文中,對該上下文是沒有任何影響的。
?
例如下面這個例子。
(Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.IO, “name”)
?要注意的是,一個新的協(xié)程上下文,除了繼承父協(xié)程的上下文之外,一定有一個新創(chuàng)建的Job對象,用于控制該協(xié)程的生命周期。
?
CoroutineContext通過這種方式來添加元素的好處是,添加元素后,生成的CombinedContext,它也繼承自CoroutineContext,從而在使用協(xié)程構(gòu)造器函數(shù),例如launch時,可以傳入單個的CoroutineContext,也可以通過「+」來傳入多個CoroutineContext的組合,而不用使用list參數(shù)或者vararg參數(shù)。
從上面的圖中我們可以看出,CoroutineContext實際上是不可變的,每次執(zhí)行「+」操作后,都會生成新的CombinedContext(它也是CoroutineContext的實現(xiàn)),而CombinedContext,才是CoroutineContext的真正實現(xiàn)者。
CombinedContext
我們來看下CombinedContext的申明。
internal class CombinedContext(
private val left: CoroutineContext,
private val element: Element
) : CoroutineContext, Serializable {
left、element,老司機一看就知道了,赤裸裸的鏈表。
?從繼承上來看,CombinedContext、CoroutineContext、Element,三者都是CoroutineContext。
?
由于在Kotlin中,CoroutineContext的Element是有限的幾種,所以這種數(shù)據(jù)結(jié)構(gòu)的性能是比較符合預(yù)期的。
通過CombinedContext我們來看下前面的plus操作符,具體在干什么,總結(jié)下代碼。
首先,plus之后,不出意外的話,會返回CombinedContext,只其中會包含plus兩邊的對象,這里有兩種可能,一種是A + B的時候,B中有和A相同的Key,那么A中的對應(yīng)Key會被刪掉,使用B中的Key,否則的話,就直接鏈起來。
所以,在CombinedContext中對Element進行查找,就變成了,如果CombinedContext中的element(也就是當前的節(jié)點)包含了對應(yīng)的Key,那么就返回,否則就從left中繼續(xù)遞歸這個過程,所以,在CombinedContext中,遍歷的順序是從右往左進行遞歸。
另外,所有Element中,有一個比較特殊的類型——ContinuationInterceptor,這個對象永遠會放置在最后面,這樣是為了方便遍歷,真是天選之子。
Elements
正如前面所解釋的,CoroutineContext本質(zhì)上是一個Map,它總是持有一個預(yù)定義的Set。由于所有的Key都必須實現(xiàn)CoroutineContext.Key接口,通過搜索CoroutineContext.Key實現(xiàn)的代碼,并檢查它們與哪個元素類相關(guān)聯(lián),就很容易找到公共元素的列表。Elements的實現(xiàn)類基本就是下面這幾種:ContinuationInterceptor、Job、CoroutineExceptionHandler和CoroutineName,也就是說CoroutineContext本質(zhì)上只會有這幾種類型的元素。
-
ContinuationInterceptor被調(diào)用于continuations,以管理底層執(zhí)行線程。在實踐中,ContinuationInterceptor總是繼承CoroutineDispatcher基類。 -
Job持有了一個正在執(zhí)行的coroutine的生命周期和任務(wù)層次結(jié)構(gòu)的句柄。 -
CoroutineExceptionHandler被那些不傳播異常的coroutine構(gòu)建器(即launch和actor)使用,以便確定在遇到異常時該怎么做。 -
CoroutineName通常用于調(diào)試。
每個Key被定義為其相關(guān)元素接口或類的伴生對象。這樣,Key可以通過使用元素類型的名稱直接被引用。例如,coroutineContext[Job]將返回coroutineContext所持有的Job的實例,如果不包含任何實例,則返回null。
如果不考慮可擴展性,CoroutineContext甚至可以簡單地被定義為一個類。
class CoroutineContext(
val continuationInterceptor: ContinuationInterceptor?,
val job: Job?,
val coroutineExceptionHandler: CoroutineExceptionHandler,
val name: CoroutineName?
)
協(xié)程作用域構(gòu)建器
每個CoroutineScope都會有一個coroutineContext屬性,通過它,我們可以獲取當前coroutine的Element Set。
public interface CoroutineScope {
/**
* The context of this scope.
* Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
* Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
*
* By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
*/
public val coroutineContext: CoroutineContext
}
lifecycleScope.launch {
println("My context is: $coroutineContext")
}
當我們想啟動一個coroutine時,我們需要在一個CoroutineScope實例上調(diào)用一個構(gòu)建器函數(shù)。在構(gòu)建器函數(shù)中,我們實際上可以看到三個上下文在起作用。
-
CoroutineScope接收器是由它提供CoroutineContext的方式來定義的,這是繼承的上下文。 -
構(gòu)建器函數(shù)在其第一個參數(shù)中接收一個CoroutineContext實例,我們將其稱為上下文參數(shù)。 -
構(gòu)建器函數(shù)中的暫停塊參數(shù)有一個CoroutineScope接收器,它本身也提供一個CoroutineContext,這就是Coroutine的上下文。
看一下launch和async的源碼,它們都以相同的語句開始。
val newContext = newCoroutineContext(context)
CoroutineScope上的newCoroutineContext擴展函數(shù)處理繼承的上下文與上下文參數(shù)的合并,以及提供默認值和做一些額外的配置。合并被寫成coroutineContext + context ,其中coroutineContext是繼承的上下文,context是上下文參數(shù)??紤]到前面解釋的關(guān)于CoroutineContext.plus操作符的內(nèi)容,右邊的操作符優(yōu)先,因此來自context參數(shù)的屬性將覆蓋繼承的context中的屬性。其結(jié)果就是我們所說的父級上下文。
?parent context = default values + inherited context + context argument
?
作為接收器傳遞給suspending函數(shù)的CoroutineScope實例實際上是coroutine本身,總是繼承AbstractCoroutine,它實現(xiàn)了CoroutineScope并且也是一個Job。coroutine上下文由該類提供,并將返回之前獲得的父級上下文,它將自己添加到該類中,有效地覆蓋了Job。
?coroutine context = parent context + coroutine job
?
CoroutineContext默認值
CoroutineContext的四個基本元素中,有些元素是有默認值的,例如CoroutineDispatcher的默認值是Dispatchers.Default,CoroutineName的默認值是Coroutine。
當一個正在被coroutine使用的上下文中缺少某個元素時,它會使用一個默認值。
-
ContinuationInterceptor的默認值是Dispatchers.Default。這在newCoroutineContext中有記錄。因此,如果繼承的上下文和上下文參數(shù)都沒有dispatcher,那么就會使用默認的dispatcher。在這種情況下,coroutine context也將繼承默認的dispatcher。 -
如果上下文沒有Job,那么被創(chuàng)建的coroutine就沒有父級。 -
如果上下文沒有CoroutineExceptionHandler ,那么就會使用全局異常處理程序(但沒有在上下文中)。這最終會調(diào)用handleCoroutineExceptionImpl,它首先使用java ServiceLoader來加載CoroutineExceptionHandler的所有實現(xiàn),然后將異常傳播給當前線程的未捕獲異常處理程序。在Android上,一個名為AndroidExceptionPreHandler的特殊異常處理程序被自動執(zhí)行,用來向Thread上隱藏的uncaughtExceptionPreHandler屬性報告異常,但它會在導(dǎo)致應(yīng)用程序崩潰之后,將異常記錄到終端日志。 -
coroutine的默認名稱是 "coroutine",用CoroutineName這個Key,來從上下文中獲取命名。
看看之前提出的假設(shè)將CoroutineScope作為一個類的方式,可以通過添加在默認值的方式來實現(xiàn)它。
val defaultExceptionHandler = CoroutineExceptionHandler { ctx, t ->
ServiceLoader.load(
serviceClass,
serviceClass.classLoader
).forEach{
it.handleException(ctx, t)
}
Thread.currentThread().let {
it.uncaughtExceptionHandler.uncaughtException(it, exception)
}
}
class CoroutineContext(
val continuationInterceptor: ContinuationInterceptor =
Dispatchers.Default,
val parentJob: Job? =
null,
val coroutineExceptionHandler: CoroutineExceptionHandler =
defaultExceptionHandler,
val name: CoroutineName =
CoroutineName("coroutine")
)
示例
通過一些例子,讓我們看看在一些coroutine表達式中產(chǎn)生的上下文,最重要的是分析它們繼承了哪些dispatchers和parent job。
Global Scope Context
GlobalScope.launch {
/* ... */
}
如果我們查看GlobalScope的源代碼,我們會發(fā)現(xiàn)它對coroutineContext的實現(xiàn)總是返回一個EmptyCoroutineContext。因此,在這個coroutine中使用的最終的上下文,將使用所有的默認值。
例如,上面的語句與下面的語句是相同的,只不過下面的代碼中明確指定了默認的dispatcher。
GlobalScope.launch(Dispatchers.Default) {
/* ... */
}
Fully Qualified Context
反過來說,我們可以將所有的參數(shù)都傳遞自己的設(shè)置,覆蓋原有的默認實現(xiàn)。
coroutineScope.launch(
Dispatchers.Main +
Job() +
CoroutineName("HelloCoroutine") +
CoroutineExceptionHandler { _, _ -> /* ... */ }
) {
/* ... */
}
繼承的上下文中的任何元素實際上都會被覆蓋,這樣的好處是,無論在哪個CoroutineScope上調(diào)用該語句都有相同的行為。
CoroutineScope Context
在Android的Coroutines UI編程指南中,我們在結(jié)構(gòu)化并發(fā)、生命周期和coroutine父子層次結(jié)構(gòu)部分找到了以下例子,展示了如何在一個Activity中實現(xiàn)CoroutineScope。
abstract class ScopedAppActivity: AppCompatActivity() {
private val scope = MainScope()
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
/* ... */
}
在這個例子中,MainScope輔助工廠函數(shù)被用來創(chuàng)建一個具有預(yù)定義的UI dispatcher和supervisor job的作用域。這是一個設(shè)計上的選擇,這樣在這個作用域上調(diào)用的所有coroutine構(gòu)建器將使用Main dispatcher而不是Default。
在作用域的上下文中定義元素,是在使用上下文的地方,覆蓋庫的默認值的一種方式。該作用域還提供了一個job,因此從該作用域啟動的所有coroutine都有同一個父級。這樣,就有一個單一的點來取消它們,它與Activity的生命周期綁定。
Overriding Parent Job
我們可以讓一些上下文元素從scope中繼承,其他的則在上下文參數(shù)中添加,這樣就可以把兩者結(jié)合起來。例如,當使用NonCancellable job時,它通常是作為參數(shù)傳遞的上下文中的唯一元素。
withContext(NonCancellable) {
/* ... */
}
在此塊中執(zhí)行的代碼將從其調(diào)用的上下文中繼承dispatcher,但它將通過使用NonCancellable作為父代來覆蓋該上下文的Job。這樣一來,這個coroutine將始終處于活動狀態(tài)。
Binding to Parent Job
當使用launch和async時,它們作為CoroutineScope的擴展函數(shù),scope中的Elements(包括job)都會自動繼承。然而,當使用CompletableDeferred時(這是一個有用的工具),可以將基于回調(diào)的API綁定到coroutine,它的parent job需要手動提供。
val call: Call
val deferred = CompletableDeferred<Response>()
call.enqueue(object: Callback {
override fun onResponse(call: Call, response: Response) {
completableDeferred.complete(response)
}
override fun onFailure(call: Call, e: IOException) {
completableDeferred.completeExceptionally(e)
}
})
deferred.await()
這種類型的架構(gòu),使得等待調(diào)用的結(jié)果變得更加容易。然而,由于協(xié)程的結(jié)構(gòu)化并發(fā),如果不能被取消,deferred可能會導(dǎo)致內(nèi)存泄露。所以,確保CompletableDeferred被正確取消的最簡單的方法是將它與它的parent job綁定。
val deferred = CompletableDeferred<Response>(coroutineContext[Job])
Accessing Context Elements
當前上下文中的元素可以通過使用top-level suspending的coroutineContext函數(shù)的只讀屬性來獲取。
println("Running in ${coroutineContext[CoroutineName]}")
例如,上面的語句可以用來打印當前coroutine的名稱。
如果我們愿意,我們實際上可以從單個元素重建一個與當前上下文相同的協(xié)程上下文。
val inheritedContext = sequenceOf(
Job,
ContinuationInterceptor,
CoroutineExceptionHandler,
CoroutineName
).mapNotNull { key -> coroutineContext[key] }
.fold(EmptyCoroutineContext) { ctx: CoroutineContext, elt ->
ctx + elt
}
launch(inheritedContext) {
/* ... */
}
盡管對于理解上下文的構(gòu)成很有趣,但這個例子在實踐中完全沒有用處。我們可以通過將啟動的上下文參數(shù)保留為默認的空值來獲得完全相同的行為。
Nested Context
最后一個例子很重要,因為它呈現(xiàn)了最新版本的coroutines中的行為變化,其中,構(gòu)建器函數(shù)成為CoroutineScope的擴展。
GlobalScope.launch(Dispatchers.Main) {
val deferred = async {
/* ... */
}
/* ... */
}
鑒于async是在作用域上調(diào)用的(而不是一個頂級函數(shù)),它將繼承作用域的dispatcher,這里被指定為Dispatchers.Main,而不是使用默認的dispatcher。在以前的coroutines版本中,async中的代碼將在Dispatchers.Default提供的工作線程上運行,但現(xiàn)在它將在UI線程上運行,這可能導(dǎo)致應(yīng)用程序阻塞甚至崩潰。
解決辦法就是更明確地說明在async中使用的dispatcher。
launch(Dispatchers.Main) {
val deferred = async(Dispatchers.Default) {
/* ... */
}
/* ... */
}
Coroutine API Design
協(xié)程API旨在靈活且富有表現(xiàn)力。通過使用簡單的 + 運算符組合上下文,語言設(shè)計者可以在啟動協(xié)程時輕松定義協(xié)程的屬性,并從執(zhí)行上下文繼承這些屬性。這使開發(fā)人員可以完全控制他們的協(xié)程,同時保持語法流暢。
參考鏈接:https://proandroiddev.com/demystifying-coroutinecontext-1ce5b68407ad
向大家推薦下我的網(wǎng)站 https://xuyisheng.top/ 點擊原文一鍵直達
專注 Android-Kotlin-Flutter 歡迎大家訪問
往期推薦
更文不易,點個“三連”支持一下??
