Compose原理六之Kotlin协程上下文

12 阅读5分钟

一、什么是上下文

Kotlin协程中,上下文就是这样一个不可变的上下文元素集合,它携带了协程运行所需的各种配置和服务。

1、1 技术定义

// kotlin.coroutines.CoroutineContext.kt
public interface CoroutineContext {
    /**
     * 通过 Key 获取上下文中的元素
     * 类似于:context[Job.Key] 获取 Job 对象
     */
    public operator fun <E : Element> get(key: Key<E>): E?
    
    /**
     * 合并两个上下文
     * 类似于:context1 + context2
     */
    public operator fun plus(context: CoroutineContext): CoroutineContext
    
    /**
     * 遍历所有元素
     * 用于内部实现,一般不直接使用
     */
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R
    
    /**
     * 移除某个元素
     * 类似于:context.minusKey(Job.Key) 移除 Job
     */
    public fun minusKey(key: Key<*>): CoroutineContext
}

核心特点:

  1. 不可变性:一旦创建就不能修改,只能通过 + 操作符创建新的上下文
  2. 元素集合:包含多个不同类型的元素
  3. 类型安全:通过 Key 确保类型安全

1、2 Element的定义

每个上下文元素必须实现 CoroutineContext.Element 接口:

public interface Element : CoroutineContext {
    /**
     * 用于在上下文中查找此元素的唯一 Key
     * 每个 Element 类型都有一个对应的 Key 单例对象
     */
    public val key: Key<*>
}

public interface Key<E : Element>

1、3 Key的作用

Key 就像是元素的"身份证号",用于在上下文中唯一标识一个元素类型。

1、4 实际示例:Job 的实现

让我们看看 Job 是如何实现 Element 的:

// Job.kt (简化版)
public interface Job : CoroutineContext.Element {
    // Job 的 Key 是一个单例对象
    public companion object Key : CoroutineContext.Key<Job>
    
    // 实现 Element 接口,返回 Key
    override val key: CoroutineContext.Key<*>
        get() = Key
    
    // Job 的其他方法...
    public val isActive: Boolean
    public val isCompleted: Boolean
    public val isCancelled: Boolean
    public fun cancel(): Boolean
    // ...
}

关键理解:

  1. 每种 Element 类型都有一个唯一的 KeyJob.KeyCoroutineName.Key
  2. 一个 CoroutineContext 中每种 Key 最多只有一个对应的 Element:你不能在同一个上下文中有两个 Job
  3. 使用 context[Key] 可以获取对应的 Elementcontext[Job.Key] 返回 Job 对象

1、5 使用示例

// 创建一个包含多个元素的上下文
val context = Dispatchers.Main + Job() + CoroutineName("MyCoroutine")

// 通过 Key 获取元素
val job = context[Job.Key]  // 获取 Job 对象
val name = context[CoroutineName.Key]  // 获取 CoroutineName 对象
val dispatcher = context[ContinuationInterceptor.Key]  // 获取 Dispatcher

println(job)  // 输出:JobImpl{Active}@12345678
println(name)  // 输出:CoroutineName(MyCoroutine)
println(dispatcher)  // 输出:HandlerDispatcher@Main

二、常见的上下文元素详解

2、1 Job - 协程的生命周期控制器

作用: 控制协程的生命周期,包括取消、等待完成等。

Key: Job.Key

示例:

// 创建一个 Job
val job = Job()

// 创建一个包含 Job 的上下文
val context = Dispatchers.Main + job

// 使用 Job 控制协程
val scope = CoroutineScope(context)

scope.launch {
    println("协程开始执行")
    delay(1000)
    println("协程执行完毕")
}

// 取消协程
job.cancel()  // 这会取消所有使用这个 Job 的协程

Job 的状态:

  • isActive:协程是否活跃
  • isCompleted:协程是否已完成
  • isCancelled:协程是否被取消

2、2 CoroutineDispatcher - 协程的执行器

作用: 决定协程在哪个线程或线程池中执行。

Key: ContinuationInterceptor.Key(注意:Dispatcher 实际上是 ContinuationInterceptor 的子类)

常见的 Dispatcher:

Dispatcher说明使用场景
Dispatchers.Main主线程UI 操作、Android 界面更新
Dispatchers.IOIO 线程池网络请求、文件读写、数据库操作
Dispatchers.DefaultCPU 密集型线程池计算密集型任务
Dispatchers.Unconfined不限制线程特殊场景,一般不使用

示例:

// 在主线程执行
launch(Dispatchers.Main) {
    // 更新 UI
    textView.text = "Hello"
}

// 在 IO 线程执行
launch(Dispatchers.IO) {
    // 网络请求
    val response = httpClient.get("https://api.example.com")
}

// 在默认线程池执行
launch(Dispatchers.Default) {
    // 计算密集型任务
    val result = heavyComputation()
}

2、3 CoroutineName - 协程的名称

作用: 为协程命名,方便调试和日志追踪。

Key: CoroutineName.Key

示例:

// 创建一个带名称的协程
launch(CoroutineName("DataLoader")) {
    println("协程名称: ${coroutineContext[CoroutineName]}")
    // 输出:协程名称: CoroutineName(DataLoader)
}

// 在调试时,协程名称会显示在日志中
// 例如:[DataLoader @coroutine#1] Loading data...

2、4 CoroutineExceptionHandler - 异常处理器

作用: 处理协程中未捕获的异常。

Key: CoroutineExceptionHandler.Key

示例:

// 创建异常处理器
val handler = CoroutineExceptionHandler { context, exception ->
    println("协程异常: $exception")
    println("协程上下文: $context")
}

// 使用异常处理器
val scope = CoroutineScope(Dispatchers.Main + handler)

scope.launch {
    throw RuntimeException("出错了!")
}
// 输出:
// 协程异常: java.lang.RuntimeException: 出错了!
// 协程上下文: [CoroutineExceptionHandler@12345678, JobImpl{Cancelled}@87654321]

三、上下文的 + 操作符详解

3、1 基本用法

使用 + 操作符可以合并两个上下文:

// 示例 1:逐步构建上下文
val context1 = Dispatchers.Main
val context2 = context1 + Job()
val context3 = context2 + CoroutineName("MyCoroutine")

// context3 包含:Dispatchers.Main, Job, CoroutineName

// 示例 2:一次性构建
val context = Dispatchers.Main + Job() + CoroutineName("MyCoroutine")

3、2 覆盖规则

如果两个上下文包含相同 Key 的元素,右边的元素会覆盖左边的

// 覆盖示例
val context1 = Dispatchers.Main + Job() + CoroutineName("Parent")
val context2 = context1 + Dispatchers.IO

// context2 中的 Dispatcher 变成了 Dispatchers.IO(覆盖了 Dispatchers.Main)
// 但 Job 和 CoroutineName 保持不变

println(context2[ContinuationInterceptor])  // 输出:Dispatchers.IO
println(context2[Job])  // 输出:原来的 Job
println(context2[CoroutineName])  // 输出:CoroutineName(Parent)

3、3 为什么右边覆盖左边?

这是为了提供灵活性。想象一下:

// 基础上下文
val baseContext = Dispatchers.Main + Job() + CoroutineName("Base")

// 在特定场景下,我们想改变 Dispatcher,但保留其他配置
val specialContext = baseContext + Dispatchers.IO

// specialContext 现在包含:
// - Dispatchers.IO(覆盖了 Main)
// - Job(继承自 baseContext)
// - CoroutineName("Base")(继承自 baseContext)

3、4 实际应用示例

// 创建一个基础作用域
val baseScope = CoroutineScope(
    Dispatchers.Main +
    Job() +
    CoroutineName("BaseScope") +
    CoroutineExceptionHandler { _, e ->
        println("全局异常处理: $e")
    }
)

// 在基础作用域中启动协程
baseScope.launch {
    // 这个协程继承了所有基础配置
    println("协程名称: ${coroutineContext[CoroutineName]}")
    // 输出:协程名称: CoroutineName(BaseScope)
    
    // 但我们可以覆盖某些配置
    launch(Dispatchers.IO + CoroutineName("Child")) {
        // 这个子协程覆盖了 Dispatcher 和 CoroutineName
        println("子协程名称: ${coroutineContext[CoroutineName]}")
        println("子协程 Dispatcher: ${coroutineContext[ContinuationInterceptor]}")
        // 输出:
        // 子协程名称: CoroutineName(Child)
        // 子协程 Dispatcher: Dispatchers.IO
    }
}

四、子协程的上下文继承机制

4、1 继承规则

当使用 launchasync 启动子协程时:

  1. 默认继承父协程的完整上下文
  2. 可以覆盖特定元素
  3. 未覆盖的元素保持不变

4、2 详细示例

// 创建父协程作用域
val scope = CoroutineScope(
    Dispatchers.Main +
    Job() +
    CoroutineName("Parent") +
    CoroutineExceptionHandler { _, e -> println("父异常: $e") }
)

scope.launch {
    // 这个协程继承了父作用域的所有配置
    println("=== 父协程 ===")
    println("名称: ${coroutineContext[CoroutineName]}")
    println("Dispatcher: ${coroutineContext[ContinuationInterceptor]}")
    println("Job: ${coroutineContext[Job]}")
    println("异常处理器: ${coroutineContext[CoroutineExceptionHandler]}")
    
    // 启动子协程,覆盖部分配置
    launch(
        context = Dispatchers.IO + CoroutineName("Child"),
        block = {
            println("\n=== 子协程 ===")
            println("名称: ${coroutineContext[CoroutineName]}")
            println("Dispatcher: ${coroutineContext[ContinuationInterceptor]}")
            println("Job: ${coroutineContext[Job]}")
            println("异常处理器: ${coroutineContext[CoroutineExceptionHandler]}")
        }
    )
}

输出:

=== 父协程 ===
名称: CoroutineName(Parent)
Dispatcher: Dispatchers.Main
Job: JobImpl{Active}@12345678
异常处理器: CoroutineExceptionHandler@87654321

=== 子协程 ===
名称: CoroutineName(Child)  ← 被覆盖
Dispatcher: Dispatchers.IO  ← 被覆盖
Job: JobImpl{Active}@12345678  ← 继承自父协程
异常处理器: CoroutineExceptionHandler@87654321  ← 继承自父协程

4、3 继承链图

父作用域上下文
┌─────────────────────────────────────┐
│ Dispatchers.Main                    │
│ Job(parent)                         │
│ CoroutineName("Parent")             │
│ CoroutineExceptionHandler           │
└─────────────────────────────────────┘
              ↓ 继承
子协程上下文(未覆盖)
┌─────────────────────────────────────┐
│ Dispatchers.Main  ← 继承            │
│ Job(child) ← 新创建,parent=parent   │
│ CoroutineName("Parent")  ← 继承      │
│ CoroutineExceptionHandler  ← 继承    │
└─────────────────────────────────────┘

子协程上下文(覆盖部分)
┌─────────────────────────────────────┐
│ Dispatchers.IO  ← 覆盖               │
│ Job(child)  ← 新创建,parent=parent   │
│ CoroutineName("Child")  ← 覆盖       │
│ CoroutineExceptionHandler  ← 继承    │
└─────────────────────────────────────┘

4、4 Job 的特殊继承规则

重要: 子协程的 Job 总是新的,但它的 parent 是父协程的 Job。

val parentJob = Job()
val scope = CoroutineScope(Dispatchers.Main + parentJob)

scope.launch {
    val childJob = coroutineContext[Job]!!
    
    println("父 Job: $parentJob")
    println("子 Job: $childJob")
    println("子 Job 的父 Job: ${childJob.parent}")
    
    // 输出:
    // 父 Job: JobImpl{Active}@12345678
    // 子 Job: JobImpl{Active}@87654321
    // 子 Job 的父 Job: JobImpl{Active}@12345678
}

这意味着:

  • 取消父 Job 会取消所有子 Job
  • 子 Job 失败会影响父 Job(除非使用 SupervisorJob

五、上下文的其他操作

5、1 minusKey - 移除元素

val context = Dispatchers.Main + Job() + CoroutineName("MyCoroutine")

// 移除 Job
val contextWithoutJob = context.minusKey(Job.Key)

println(context[Job])  // 输出:JobImpl{Active}@12345678
println(contextWithoutJob[Job])  // 输出:null

5、2 fold - 遍历元素

val context = Dispatchers.Main + Job() + CoroutineName("MyCoroutine")

// 遍历所有元素
context.fold("元素列表: ") { acc, element ->
    "$acc, ${element.key}"
}
// 输出:元素列表: , ContinuationInterceptor.Key, Job.Key, CoroutineName.Key

六、实际应用场景

6、1 创建自定义协程作用域

// 创建一个专门用于网络请求的作用域
class NetworkScope {
    private val job = SupervisorJob()
    private val handler = CoroutineExceptionHandler { _, e ->
        println("网络请求失败: $e")
    }
    
    val scope = CoroutineScope(
        Dispatchers.IO +
        job +
        CoroutineName("NetworkScope") +
        handler
    )
    
    fun cleanup() {
        job.cancel()
    }
}

// 使用
val networkScope = NetworkScope()

networkScope.scope.launch {
    // 网络请求
    val data = fetchData()
    println(data)
}

// 清理
networkScope.cleanup()

七、总结

上下文的核心概念:

  1. 不可变的元素集合:类似于一个不可变的 Map,Key 是元素类型,Value 是元素实例
  2. Element 和 Key:每个 Element 类型都有一个唯一的 Key,用于标识和查找
  3. + 操作符:用于合并上下文,右边的元素会覆盖左边的相同 Key 的元素
  4. 上下文继承:子协程默认继承父协程的上下文,但可以覆盖特定元素
  5. 常见元素
    • Job:控制生命周期
    • CoroutineDispatcher:决定执行线程
    • CoroutineName:协程名称
    • CoroutineExceptionHandler:异常处理