用線程做類比的話,協程的 context
可以認爲是協程的“線程私有變量”,同時這個私有變量是不可變的。也就是說,我們在創建一個協程的時候,他的 context
攜帶的信息就已經確定了下來。由於協程是很輕量的,在需要改變協程上下文的時候,我們可以直接新創建一個協程,所以不可變性不算是一個大問題。
它的兩個跟我們我們日常開發聯繫比較大的方法如下:
@SinceKotlin("1.3")
public interface CoroutineContext {
interface Key<E : Element>
public operator fun <E : Element> get(key: Key<E>): E?
public operator fun plus(context: CoroutineContext): CoroutineContext
// 注:省略了 fold 和 minusKey
}
context
可以認爲是一個 map
,通過它的 get
方法可以取出集合裏的內容,比如:
val interceptor: ContinuationInterceptor? = context[ContinuationInterceptor]
interceptor
我們下篇文章再聊。這裏用來做索引的 ContinuationInterceptor
其實是類 ContinuationInterceptor
的 companion object,並且這個伴生對象繼承了 CoroutineContext.Key<ContinuationInterceptor>
。初看起來這種寫法有點奇怪,但習慣了以後還是不得不承認這個是很優雅的設計(相當於一個協變類型的 map)。下面我們看看如何給 context
帶上自定義的信息。
Element
是集合裏的內容:
interface CoroutineContext {
// ...
/**
* An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
*/
public interface Element : CoroutineContext {
/**
* A key of this coroutine context element.
*/
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
}
}
實現它很簡單:
class CoroutineName(val name: String) : CoroutineContext.Element {
// companion object 是一個類對象,理所應當的,它能繼承其他類
companion object : CoroutineContext.Key<CoroutineName>
override val key = CoroutineName
}
fun main() = runBlocking {
GlobalScope.launch(Dispatchers.IO + CoroutineName("my coroutine")) {
// CoroutineName 的 companion object 繼承的是 Key<CoroutineName>,
// 所以 coroutineContext[CoroutineName] 的類型是 CoroutineName?
val name = coroutineContext[CoroutineName]?.name ?: "no name"
println("name = $name")
}
Unit
}
// 打印:
// name = my coroutine
// 或者更常見的:
// 這裏的 + 使用的是 CoroutineContext 裏定義的 operator plus
someCoroutineScope.launch(Dispatchers.IO + CoroutineName("my coroutine")) {
val name = coroutineContext[CoroutineName]?.name ?: return
// ...
}
由於這種定義 context
是固定的模型,kotlin 還提供了一個便捷類:
class CoroutineName(val name: String) : AbstractCoroutineContextElement(CoroutineName) {
companion object : CoroutineContext.Key<CoroutineName>
}
關於協程上下文,最後還剩下的問題是,我們如何拿到它。其中一種方法在上面的例子中我們已經看到:
public interface CoroutineScope {
/**
* The context of this scope.
*/
public val coroutineContext: CoroutineContext
}
傳遞給 CoroutineScope.launch()
的 lambda 有一個類型爲 CoroutineScope
的 receiver,這就是前面我們能夠直接使用 coroutineContext
的原因。
另一個獲取 context
的途徑是 Continuation
,例如:
interface Callback {
fun onResult(enabled: Boolean)
}
fun isFeatureXEnabled(callback: Callback) {
// ...
}
suspend fun isFeatureXEnabled() = suspendCoroutine { conn: Continuation<Boolean> ->
isFeatureXEnabled(object : Callback {
override fun onResult(enabled: Boolean) {
val context = conn.context
conn.resume(enabled)
}
})
}
Continuation
的 context
字段就是 CoroutineContext
:
@SinceKotlin("1.3")
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
關於協程上下文的內容差不多就是這些了,如果讀者有想分享出來的內容,歡迎給我留言。