在 Kotlin 协程的开发中,我们每天都在写 GlobalScope.launch 或者在 ViewModel 中使用 viewModelScope。但你是否曾停下来思考过:为什么启动协程一定要一个 Scope?既然有了 Context 来描述协程信息,为什么还要多出一个 Scope 概念?
本文将带你拨开迷雾,厘清 CoroutineScope 与 CoroutineContext 的本质定位及二者的协作逻辑。用一句话概括它们本质:CoroutineContext 是协程的属性信息,而 CoroutineScope 是这些信息的生存空间。
1、CoroutineContext 的本质
从本质上讲,CoroutineContext 是协程执行所依赖的全部信息集合。如果把协程比作一个正在运行的「任务」,那么 Context 就是这个任务的「简历」和「运行环境配置」。
1.1 核心组成要素
Context 并不是一个单一的变量,它更像是一个元素集合(类似 Map),常见的成员包括:
- Job:控制协程的生命周期(取消、完成、父子关系)。
- ContinuationInterceptor(Dispatcher):决定协程在哪个线程运行(如
Dispatchers.IO)。 - CoroutineName:给协程起个名字,方便调试。
- CoroutineExceptionHandler:处理未捕获的异常。
技术细节:你可以使用 + 运算符来合并两个 Context。如果相加的是同类型的元素(例如两个不同的 Job),右侧的元素会覆盖左侧的元素。
2、CoroutineScope 的本质
如果说 Context 是数据,那么 CoroutineScope 就是这些数据的持有者和操作者。
2.1 它是 Context 的容器
CoroutineScope 内部持有一个 coroutineContext 属性。它的存在是为了让你能方便地获取当前协程的全部上下文信息。
- 通过
scope.coroutineContext[Job]访问生命周期控制对象。 - 通过
scope.coroutineContext[ContinuationInterceptor]获取当前的调度器。
2.2 它是启动子协程的载体
在 Kotlin 中,launch 和 async 被定义为 CoroutineScope 的扩展函数。这意味着,启动一个新协程必须依赖一个 Scope。这样做最大的好处是:自动继承。子协程会自动从父 Scope 的 Context 中提取信息,从而实现「结构化并发」。
2.3 到底谁拥有 Context
理解 Context 的归属是消除困惑的关键。我们可以从三个维度来看待一个协程:
- Job 视角:Job 本身就是 Context 的一部分,它无法反向定义完整的 Context。
- CoroutineScope 视角:这往往是一种循环定义(Scope 持有 Context,Context 描述 Scope)。
- 代码块
{}视角:这是唯一具有业务意义的视角。当你写下{ ... }里的业务逻辑时,这个代码块在哪个线程跑、被谁取消,全由它关联的coroutineContext决定。
总结:CoroutineScope 的双重职能就是:为当前代码块提供上下文,并作为启动下一级子协程的入口。
2.4 手动创建的 CoroutineScope
我们经常会看到这种代码:
1 | val scope = CoroutineScope(Dispatchers.Default) |
这种手动创建的 Scope 与协程体内部自带的 Scope 有本质区别:
- 无绑定代码块:它不对应任何实际存在的协程
{}。 - 仅为启动而生:它的
coroutineContext并不代表「当前正在执行的上下文」,它仅仅是一个模板。当你调用scope.launch时,它会将这个模板传递给新产生的协程。 - 自动补全:即便你只传了
Dispatchers,系统也会自动为它生成一个默认的Job,以确保通过该 Scope 启动的协程可以被统一管理。
3、GlobalScope 的本质
当你调用 GlobalScope 时,IDE 会弹出警告。这并非因为它被废弃,而是因为它被标记了 @DelicateCoroutinesApi(精致/易错 API)。这是一种语义化警告:提示开发者该 API 极其容易误用,使用前需明确其生命周期行为。
3.1 单例与 EmptyCoroutineContext
GlobalScope 在本质上是一个单例对象,全局唯一。它与其他 CoroutineScope 最根本的区别在于:它的内部没有内置 Job。
普通 Scope:无论是
MainScope()还是CoroutineScope(Dispatchers.Default),其coroutineContext中一定包含一个Job实例。GlobalScope:它的
coroutineContext被硬编码为EmptyCoroutineContext。1
2
3
4
5
6
7/*** kotlinx.coroutines/CoroutineScope.kt ***/
public object GlobalScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}关键点:如果你尝试访问
GlobalScope.coroutineContext[Job],返回的结果恒为null。所以,下面启动协程的写法在本质上是没有任何区别的。1
2
3
4
5
6
7GlobalScope.launch {
doWork()
}
CoroutineScope(EmptyCoroutineContext).launch {
doWork()
}
3.2 结构化并发的断裂
在 Kotlin 的结构化并发体系中,父子协程关系的建立依赖于 Job 的层级传递。
正常情况:在
launch内部再次launch,子协程会自动关联父协程的 Job。GlobalScope 情况:由于
GlobalScope自身没有 Job,当它启动一个协程时,没有 Job 可以作为「父 Job」传递给新协程。1
2
3
4val job = GlobalScope.launch {
// 协同逻辑
}
println(job.parent) // 输出:null这意味着:由 GlobalScope 启动的协程,在逻辑上是「孤儿协程」。它们不属于任何层级结构,不受任何外部作用域的约束。
3.3 什么时候该用 GlobalScope
既然它打破了结构化并发,为什么还要设计它?
解决无生命周期绑定的启动冗余
在 Kotlin 中,启动协程必须有 Scope。如果你需要启动一个与 UI 生命周期(如
Activity/Fragment)完全无关,甚至需要存活至应用进程结束的任务,手动创建一个CoroutineScope(Dispatchers.Default)并维护它显得非常繁琐。GlobalScope提供了开箱即用的入口。确保协程间的完全隔离
由于没有 Job 链条,
GlobalScope启动的协程具备天然的隔离性:- 取消隔离:
GlobalScope本身无法被取消,它启动的任务也不会因为其他任务的失败而连锁取消。 - 异常隔离:其中一个协程崩溃,不会波及到由
GlobalScope启动的其他协程。
- 取消隔离:
3.4 资源泄漏的本质
很多人认为 GlobalScope 会导致资源泄漏,这其实是一个因果倒置的误解。
- 风险根源:泄漏的本质是因为协程启动后没有被手动管理或及时取消。
- 对比实验:
- 使用
GlobalScope.launch { ... }而不取消。 - 使用
CoroutineScope(Dispatchers.Default).launch { ... }而不取消。这两个行为导致的资源浪费风险是完全一致的。
- 使用
GlobalScope 并不是毒药,它只是一个不带自动束缚的纯净容器。使用它的核心准则在于:主动进行生命周期管理。如果你明确该任务需要全局运行,且你拥有手动控制其结束的能力(或者它本就该随进程结束),那么 GlobalScope 就是合理的工具。
4、挂起函数中获取 CoroutineContext
在 Kotlin 协程开发中,我们经常需要在「挂起函数」内部获取当前的「协程上下文」。看似简单的操作,背后却隐藏着编译器注入、命名冲突以及库设计的权衡。
4.1 挂起函数的困境
在普通的协程块(如 launch 或 async)中,我们可以直接访问 coroutineContext 属性。这是因为这些代码块的 Receiver 是 CoroutineScope。
但在一个普通的挂起函数中:
1 | suspend fun doWork() { |
如果你尝试直接写 coroutineContext,编译器会报错,除非你显式地将函数定义为 CoroutineScope.doWork(),也就是 CoroutineScope 的扩展函数。那么,非扩展的挂起函数该如何感知外层的执行环境呢?
4.2 穿透边界的属性
为了解决上述问题,Kotlin 协程库引入了一个特殊的挂起属性:coroutineContext。
它的本质是「伪装」的函数,虽然看起来像属性,但它的底层实现是一个挂起函数:
1
suspend fun get(): CoroutineContext
编译器的「魔法」注入
如果你查看源码,会发现它的
get()方法体居然是throw NotImplementedError()。为什么运行不报错? 因为它的真实逻辑是由 Kotlin 编译器在编译期动态注入的。它能穿透挂起函数的边界,直接抓取调用该函数的外层CoroutineScope所持有的上下文。1
2
3
4
5
6
7
8
9
10
11
12/*** kotlinx.coroutines/Continuation.kt ***/
/**
* Returns the context of the current coroutine.
*/
public suspend inline val coroutineContext: CoroutineContext
get() {
throw NotImplementedError("Implemented as intrinsic")
}等价的函数封装
为了符合函数调用的习惯,官方还提供了一个封装函数:
currentCoroutineContext()。查看源码,你会发现这个函数本质上还是对coroutineContext的调用。另外,源码注释解也解释了定义这个函数的根本原因是为了避免在接收者位置上与CoroutineScope.coroutineContext发生名称冲突,并给出了场景代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* Returns the current [CoroutineContext] retrieved by using [kotlin.coroutines.coroutineContext].
* This function is an alias to avoid name clash with [CoroutineScope.coroutineContext] in a receiver position:
*
* ```
* launch { // this: CoroutineScope
* val flow = flow<Unit> {
* coroutineContext // Resolves into the context of outer launch, which is incorrect, see KT-38033
* currentCoroutineContext() // Retrieves actual context where the flow is collected
* }
* }
* ```
*/
public suspend inline fun currentCoroutineContext(): CoroutineContext = coroutineContext
5、作用域构造器 coroutineScope
在真实的开发中,coroutineScope 和 supervisorScope 是两个绕不开的核心函数。很多开发者虽然经常看到它们,但对其真正的「存在意义」和「应用场景」往往感到模糊。简单来说,coroutineScope 和 supervisorScope 都是作用域构造器。这两个函数的主要功能是:创建一个新的协程作用域,并在该作用域内执行代码块。
- coroutineScope:创建一个使用普通
Job的作用域。 - supervisorScope 情况:创建一个使用
SupervisorJob的作用域。
5.1 与 launch 的区别
很多开发者会将 coroutineScope 与 launch 混淆,因为它们看起来都在「创建用域」。但本质区别在于:
launch:普通函数。有参数,可以指定
ContinuationInterceptor,也可以指定父Job。非阻塞/不挂起。它启动一个协程后立即返回,不会等待内部逻辑完成。1
2
3
4
5
6
7
8
9
10
11
12
13
14/*** 打印:Step:3 → Step:1 → Step:2 ***/
fun main(): Unit = runBlocking {
val job = launch {
delay(1000)
println("Step:1")
launch {
delay(2000)
println("Step:2")
}
}
println("Step:3")
job.join()
}coroutineScope:挂起函数。无参数,因此无法通过参数定制
CoroutineContext。它会挂起当前的调用者协程,直到作用域内所有的子协程(包括递归启动的子协程)全部执行完毕,它才会返回。1
2
3
4
5
6
7
8
9
10
11
12
13/*** 打印:Step:1 → Step:2 → Step:3 ***/
fun main(): Unit = runBlocking {
coroutineScope {
delay(1000)
println("Step:1")
launch {
delay(2000)
println("Step:2")
}
}
println("Step:3")
}
5.2 深度拆解 coroutineScope
机制:继承与重组,当你调用
coroutineScope时,它会:步骤 描述 1 继承外部的 CoroutineContext(如 Dispatcher、Name 等)。2 不继承外部 Job,而是创建一个新的子 Job 绑定到当前作用域。 3 提供一个新的 CoroutineScope作为代码块的this。为挂起函数提供 Context,在一个标准的
suspend函数内部,你无法直接调用launch或async,因为它们需要CoroutineScope类型的接收者。使用coroutineScope包裹逻辑。它能提供合法的this作用域,让你在挂起函数里自由地启动子协程。1
2
3
4
5suspend fun work() {
coroutineScope {
launch { println("Job 1 finished") }
}
}特性验证:串行等待,由于它是挂起函数,具有天然的「等待」属性:
1
2
3
4
5
6
7
8suspend fun work() {
coroutineScope {
launch { delay(2000); println("Job 1 finished") }
launch { delay(1000); println("Job 2 finished") }
}
// 只有上面两个 launch 都跑完(约 2 秒后),才会执行到这里
println("All jobs in scope finished")
}支持返回值,不同于
launch返回Job,coroutineScope具有返回值,其值为代码块最后一行表达式的结果。这使得它在封装逻辑时非常灵活。1
2
3
4
5
6
7
8fun main(): Unit = runBlocking {
val result = coroutineScope {
val baseDeferred = async { "Base" }
val taskDeferred = async { "Task" }
"${baseDeferred.await()} ${taskDeferred.await()}"
}
println(result)
}异常捕获,在协程树中,子协程的未捕获异常通常会向上传播,导致整个协程树崩溃。普通的
try-catch无法直接捕获子协程launch内部抛出的异常。由于coroutineScope是挂起函数,当内部子协程抛出异常且未被处理时,coroutineScope会在挂起恢复处抛出该异常。你可以在coroutineScope外层套上try-catch。这样,即使内部网络请求失败,你也能够在局部处理错误,而不会让整个业务流程挂掉。1
2
3
4
5
6
7
8
9
10
11
12fun main(): Unit = runBlocking {
val result = try {
coroutineScope {
val baseDeferred = async { "Base" }
val taskDeferred = async { throw RuntimeException("Error") }
"${baseDeferred.await()} ${taskDeferred.await()}"
}
} catch (e: Exception) {
e.message
}
println(result)
}逻辑封装,正是由于
coroutineScope支持返回值以及异常捕获的能力,使得我们可以将一些模块化的代码进行独立封装。1
2
3
4
5
6
7
8
9
10
11
12suspend fun work(): String {
val result = try {
coroutineScope {
val baseDeferred = async { "Base" }
val taskDeferred = async { throw RuntimeException("Error") }
"${baseDeferred.await()} ${taskDeferred.await()}"
}
} catch (e: Exception) {
e.message
}
return result.orEmpty()
}
5.3 异常隔离的利器 supervisorScope
supervisorScope 与 coroutineScope 的唯一区别在于其内部使用的是 SupervisorJob,其核心差异如下。如需了解更多详细信息,请参考 《Kotlin-协程(二)结构化并发》 中的「5.3.4 最佳实践」章节。
| 特性 | coroutineScope |
supervisorScope |
|---|---|---|
| Job 类型 | 普通 Job | SupervisorJob |
| 异常传播 | 一个子协程失败,其余全部取消 | 子协程异常相互隔离,互不影响 |
| 本质 | 挂起函数 | 挂起函数 |
| 主要用途 | 逻辑拆分、异常捕获封装 | 独立任务执行、防止连锁崩溃 |
6、withContext 的本质
在 Kotlin 协程的日常开发中,withContext 可能是除了 launch 和 async 之外频率最高的函数。很多开发者将其简单理解为「切换线程的工具」,但这种理解只触及了表象。
6.1 可参数化的协程作用域
如果说 coroutineScope 是一个单纯的协程作用域构建器,那么 withContext 的本质就是「支持上下文定制的 coroutineScope」。它们的核心区别在于参数化能力:
- coroutineScope:无法接收任何参数,它直接继承父协程的上下文。
- withContext:唯一的不同点在于它允许传入一个
CoroutineContext参数,用于临时修改当前协程的属性(如 Dispatcher、Job 名称等)。
6.2 源码层面的“孪生兄弟”
从源码结构上看,withContext 与 coroutineScope 极其相似。它们的外层都封装在 suspendCoroutineUninterceptedOrReturn 之中,这是实现非阻塞挂起的关键。
coroutineScope 源码
1
2
3
4
5
6
7
8
9
10
11/*** kotlinx.coroutines/CoroutineScope.kt ***/
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}withContext 源码,当调用
withContext(context)时,系统会经历以下步骤:- 上下文合并:将传入的
context与当前的currentContext进行合并。 - 活性检查:调用
ensureActive()。这是一个关键细节,如果当前协程已被取消,withContext会立即抛出CancellationException,避免无效计算。 - 路径分发:根据上下文是否变化,走入不同的执行分支。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36/*** kotlinx.coroutines/Builders.common.kt ***/
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
// compute new context
val oldContext = uCont.context
// Copy CopyableThreadContextElement if necessary
val newContext = oldContext.newCoroutineContext(context)
// always check for cancellation of new context
newContext.ensureActive()
// FAST PATH #1 -- new context is the same as the old one
if (newContext === oldContext) {
val coroutine = ScopeCoroutine(newContext, uCont)
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
// FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed)
// `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher)
if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
val coroutine = UndispatchedCoroutine(newContext, uCont)
// There are changes in the context, so this thread needs to be updated
withCoroutineContext(coroutine.context, null) {
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
// SLOW PATH -- use new dispatcher
val coroutine = DispatchedCoroutine(newContext, uCont)
block.startCoroutineCancellable(coroutine, coroutine)
coroutine.getResult()
}
}- 上下文合并:将传入的
6.3 三条执行路径
withContext 的强大之处在于它会根据上下文的变更程度,自动选择最轻量的执行方式。比如,路径一里的 withContext(EmptyCoroutineContext) 在语义上完全等价于 coroutineScope。
| 路径类型 | 判定条件 | 使用的对象 | 执行逻辑 |
|---|---|---|---|
| 路径一:无变更 | 新旧上下文完全一致(或传入 EmptyCoroutineContext) |
ScopeCoroutine |
行为等价于 coroutineScope,直接在原线程顺序执行,性能损耗最小。 |
| 路径二:仅属性变更 | 变更了 Context 但 ContinuationInterceptor(调度器)未变 |
onDispatchedCoroutine |
注入新上下文,但不触发线程切换。 |
| 路径三:调度器变更 | 变更了 Dispatcher(如从 Main 切到 IO) |
dispatchedCoroutine |
最完整的逻辑:既注入新上下文,又执行线程池调度。 |
6.4 串行协程启动器
很多开发者会拿 withContext 与 launch/async 做比较。其实它们在执行模式上有本质区别:
- launch / async:并行逻辑。它们开启一个新的协程并在后台运行,当前流程继续向下执行。
- withContext:串行逻辑。它是一个串行协程启动器。
我们通常并不是因为「想要串行」才使用 withContext(因为代码默认就是串行的),而是因为「我们需要切换上下文(如切换线程),同时必须保持执行流的顺序性」。
6.5 命名即契约
withContext 的命名已经精准界定了它的用途:在(with)特定的上下文(Context) 中执行代码块。因为 withContext 解决的是「在不开启并行任务的前提下,临时改变运行环境」的需求。
- 本质一:它是
coroutineScope的增强版,支持上下文定制。 - 本质二:它是串行化的执行器,确保代码块执行完之前,外部协程处于挂起状态。
7、CoroutineName
在 Kotlin 协程中,CoroutineName 是一个非常实用但常被忽视的上下文元素。它主要用于调试(Debugging)和日志追踪(Logging),帮助开发者在复杂的并发场景中快速识别某个协程的身份。
7.1 什么是 CoroutineName
CoroutineName 是协程上下文(CoroutineContext)的一个组成部分。它的核心作用是给协程起一个「人类可读」的名字。
- 默认值:如果你不指定,协程通常只会被标识为 “
coroutine”。 - 用途:在打印日志、查看 Thread Dump 或使用调试器时,这个名字会附加在线程名后面,或者作为协程的标签显示。
7.2 如何使用 CoroutineName
你可以在创建协程(使用 launch 或 async)时,通过 + 运算符将其添加到上下文中。
方式一
1
2
3
4
5
6
7
8fun main(): Unit = runBlocking {
val name = CoroutineName("CustomName")
val scope = CoroutineScope(EmptyCoroutineContext)
val job = scope.launch(name) {
println("CoroutineName:${coroutineContext[CoroutineName]}")
}
job.join()
}方式二
1
2
3
4
5
6
7
8fun main(): Unit = runBlocking {
val name = CoroutineName("CustomName")
val scope = CoroutineScope(name)
val job = scope.launch {
println("CoroutineName:${coroutineContext[CoroutineName]}")
}
job.join()
}方式三
1
2
3
4
5
6
7
8fun main(): Unit = runBlocking {
val name = CoroutineName("CustomName")
val scope = CoroutineScope(Dispatchers.IO + name)
val job = scope.launch {
println("CoroutineName:${coroutineContext[CoroutineName]}")
}
job.join()
}
7.3 CoroutineName 关键特性
上下文继承
像其他的上下文元素一样,
CoroutineName是可以继承的。如果父协程有名字,子协程默认会继承这个名字,除非你在子协程中显式覆盖它。在日志中访问
你可以通过
coroutineContext[CoroutineName]?.name在代码中随时获取当前协程的名字,方便在打印 Log 时作为前缀。
7.4 最佳实践建议
- 复杂业务必带:在处理复杂的业务逻辑(如多步骤的支付流程、长链接维护)时,给核心协程命名能极大降低排查问题的成本。
- 配合日志框架:如果你使用 SLF4J 等日志库,可以通过 MDC(Mapped Diagnostic Context)将
CoroutineName自动注入到每一行日志中。 - 不要过度依赖:名字仅用于调试。不要尝试通过解析名字字符串来控制业务逻辑。
8、CoroutineContext 的运算
在 Kotlin 协程开发中,我们经常写下 Dispatchers.IO + CoroutineName("CustomName") 这样的代码,或者通过 context[Job] 来获取当前的 Job。这些看似简单的运算符背后,隐藏着一套精妙的 Operator 函数映射与递归数据结构设计。
8.1 加法运算
当我们使用 + 运算符连接两个上下文时,编译器实际上调用了 operator fun plus()。
结构本质:CombinedContext
CoroutineContext并非一个简单的 List 或 Map,它更像是一个左倾的二叉树或递归链表。合并后的结果通常是一个CombinedContext实例,它包含两个属性:- left:
CoroutineContext(指向左侧剩余的部分) - element:
Element(当前节点存储的单个元素)
- left:
同类型覆盖逻辑
plus操作遵循「右侧优先」原则。如果右侧添加的元素类型(Key)在左侧已经存在,旧元素会被剔除。- 实证:
dispatcher + job1 + name + job2
最终结果中只包含
job2,job1会被新加入的同类型元素覆盖并移除。- 实证:
特殊优化
- EmptyContext:若右侧为
EmptyCoroutineContext,则直接返回左侧,不进行任何包装。 - 拦截器置顶:为了提高协程频繁查找调度器的性能,
ContinuationInterceptor(即 Dispatcher)在合并过程中会被强制移动到CombinedContext的最外层,方便快速检索。
- EmptyContext:若右侧为
查看源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/**
* Returns a context containing elements from this context and elements from other [context].
* The elements from this context with the same key as in the other one are dropped.
*/
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
// make sure interceptor is always last in the context (and thus is fast to get when present)
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
8.2 中括号获取
很多开发者感到困惑:为什么 context[Job] 中括号里填的是类型名?在 Java 或标准 Kotlin 中,中括号通常接收的是对象实例。
语法糖背后的 get 函数:
context[Job]等价于context.get(Job)。这里的Job实际上是Job.Companion对象的简写。1
2
3
4
5
6public interface Job : CoroutineContext.Element {
/**
* Key for [Job] instance in the coroutine context.
*/
public companion object Key : CoroutineContext.Key<Job>
}Key 机制与类型安全:在
Job接口内部,定义了一个单例。- Job.Key:它的类型是
CoroutineContext.Key<Job>。 - 泛型推导:
get函数的签名是fun <E : Element> get(key: Key<E>): E?。当你传入Job(即Job.Key)时,泛型E被推导为Job,因此返回值直接就是Job?类型,无需手动强转。这就是类型安全的检索。
1
public operator fun <E : Element> get(key: Key<E>): E?
- Job.Key:它的类型是
8.3 删除操作
minusKey 用于从复杂的上下文结构中移除指定类型的元素。
- 调用方式:
context.minusKey(Job)。 - 内部逻辑:它会递归遍历
CombinedContext。如果匹配到 Key,则返回其left节点;如果不匹配,则重建节点。 - 结果显现:如果一个
CombinedContext移除了右侧元素,原本被包裹在left中的左侧元素就会重新显现并成为新的根节点。
9、自定义 CoroutineContext
在 Kotlin 协程中,自定义 CoroutineContext 是进阶开发的必经之路。它不仅能让你像传递「全局变量」一样在协程间传递数据,还能实现日志追踪、权限校验、业务埋点等功能。要自定义一个上下文元素,本质上是实现 CoroutineContext.Element 接口。
9.1 实现 Element 接口
自定义上下文通常包含两个部分:数据类本身和它的唯一 Key。
关键点解析:
AbstractCoroutineContextElement:这是一个便捷的抽象类,帮我们处理了大部分接口模板代码。Key:必须定义一个Key。就像 Map 的键一样,协程框架靠它在复杂的CombinedContext中精准找到你的数据。1
2
3
4
5
6
7class Trace : AbstractCoroutineContextElement(Trace) {
companion object Key : CoroutineContext.Key<Trace>
suspend fun report() {
println("event reported success")
}
}
9.2 使用自定义上下文
一旦定义好,你就可以像使用 Dispatchers.IO 一样使用它。
1 | fun main(): Unit = runBlocking { |
9.3 总结
自定义 CoroutineContext 的步骤可以概括为:
- 类比 Map:把 Context 想象成一个只读的、类型安全的 Key-Value 集合。
- 定义 Key:使用
companion object确保 Key 的单例性质。 - 组合与提取:利用
+注入,利用[]获取。