协程上下文CoroutineContext
1. CoroutineContext 是什么?
- 本质 :一个键值对的集合 (类似于
Map),存储了协程运行时的配置信息。 - 核心组件 :
- 调度器(Dispatcher) :决定协程在哪个线程池运行(如
Dispatchers.Main、Dispatchers.IO)。 - Job:控制协程的生命周期(取消、父子关系)。
- 异常处理器(CoroutineExceptionHandler):处理未捕获的异常。
- 协程名称(CoroutineName):调试时标识协程。
- 调度器(Dispatcher) :决定协程在哪个线程池运行(如
kotlin
val context: CoroutineContext = Job() + Dispatchers.IO + CoroutineName("MyWorker")
记住 :每个协程都运行在一个
CoroutineContext中
2. CoroutineContext 的关键特性
(1)组合与继承
-
协程上下文可以通过
+操作符组合:kotlinval context = Dispatchers.IO + CoroutineName("MyCoroutine") -
子协程默认继承父协程的上下文(可覆盖部分元素)。
-
如果两个元素有相同
Key(比如两个Dispatcher),后面的会覆盖前面的。 -
这就是为什么你可以这样写:
kotlin
withContext(Dispatchers.IO) { ... } // 覆盖当前 dispatcher,但保留 Job 和其他元素
(2)层级结构
当你在一个协程内部启动新协程(子协程),子协程会继承父协程的上下文,但可以覆盖部分元素。
kotlin
launch(Dispatchers.Main) { // 父协程:Main 线程 + 默认 Job
println("Parent context: $coroutineContext")
launch(Dispatchers.IO) { // 子协程
// 继承了父 Job(所以能被父协程取消)
// 但 Dispatcher 被替换为 IO
println("Child context: $coroutineContext")
}
}
输出结果:
text
Parent context: [StandaloneCoroutine{Active}, Main]
Child context: [StandaloneCoroutine{Active}@1234, IO]
(3)协程上下文的继承公式
子协程上下文 = 父协程上下文 + 新指定的上下文
ini
childContext = parentContext[Job]!!.plus(parentContext.minusKey(Job)) + customContext
简化理解就是:
- Job 会被继承并建立父子关系
- 其他元素(如 Dispatcher)可被覆盖
- 如果你显式传入新的 Job(如
Job()),则断开父子关系(不推荐!)
javascript
launch(Job()) { ... } // 创建了"孤儿协程",无法被父作用域管理!
协程异常处理
(1)学习协程异常处理的必要性
- 默认情况下,任何一个子协程抛出未捕获异常,会立即取消整个协程作用域(包括兄弟协程)。
- 在 Android 中,这可能导致 ViewModel 或 Activity 中所有后台任务突然停止。
- 如果不处理,未捕获异常会 crash 应用 (虽然
CancellationException不会)。
(2)自动传播异常 vs 向用户暴露异常
- 自动传播 :协程框架默认将异常向上传播到作用域根部,若无人处理,则抛给
Thread.uncaughtExceptionHandler→ App crash。 - 向用户暴露:你主动捕获异常,转为 UI 提示(如 Toast、Snackbar)。
kotlin
// ❌ 默认:crash
viewModelScope.launch {
api.loadData() // 抛出 IOException → crash
}
// ✅ 主动暴露
viewModelScope.launch {
try {
api.loadData()
} catch (e: IOException) {
_uiState.value = ErrorState("网络错误")
}
}
(3)非根协程的异常传播 & 异常的传播特性
核心规则:
| 作用域类型 | 子协程异常是否影响兄弟协程? | 是否向上传播? |
|---|---|---|
coroutineScope / 默认 Job |
✅ 是 | ✅ 是(取消整个作用域) |
supervisorScope / SupervisorJob |
❌ 否 | ❌ 否(仅当前协程失败) |
- 普通 Job: "一荣俱荣,一损俱损"
- SupervisorJob: "各自负责,互不影响"
(4)SupervisorJob 与 supervisorScope
SupervisorJob():创建一个"监督者" Job
scss
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
scope.launch {
throw RuntimeException("A failed") // 只取消自己
}
scope.launch {
delay(1000)
println("B still runs!") // ✅ 会执行
}
supervisorScope { }:临时作用域,内部协程互不影响
kotlin
suspend fun fetchAll() = supervisorScope {
val job1 = launch { fetchUser() } // 失败不影响 job2
val job2 = launch { fetchNotifications() }
joinAll(job1, job2)
}
适用场景:并行加载多个独立数据源(如首页卡片),一个失败不应阻断其他。
(5)异常捕获的时机与位置 + 常见错误
正确做法:
- 在抛出异常的协程内部 使用
try-catch - 或使用
CoroutineExceptionHandler
kotlin
// 方式1:内部 try-catch(推荐)
launch {
try {
riskyOperation()
} catch (e: Exception) {
handle(e)
}
}
// 方式2:全局异常处理器(适合兜底)
val handler = CoroutineExceptionHandler { _, exception ->
logError(exception)
}
launch(handler) { riskyOperation() }
常见错误:
在外部函数 try-catch launch 块 → 无效!
php
try {
launch { throw Exception() } // ❌ 异常发生在另一个协程,这里 catch 不到
} catch (e: Exception) { ... }
在 supervisorScope 中忘记 catch,导致异常静默丢失
php
supervisorScope {
launch { throw Exception() } // ❌ 异常未被捕获,也不会 crash,但你不知道!
}
解决方案:即使使用 Supervisor,也要在每个 launch 内部处理异常。
(6)异常捕获阻止 App 闪退
在 Android 中,未处理的协程异常最终会调用 Thread.getDefaultUncaughtExceptionHandler(),导致 crash。
安全做法:
- ViewModel 中 :每个
viewModelScope.launch内部处理异常 - 全局兜底(谨慎使用):
arduino
// Application.onCreate()
Thread.setDefaultUncaughtExceptionHandler { thread, exception ->
// 上报 crash 日志
// 注意:不要在这里启动新协程!
defaultHandler.uncaughtException(thread, exception)
}
但更好的方式是:在协程层面就处理掉异常,而不是依赖全局 handler。
(7)取消与异常
CancellationException是特殊的 :- 它是协程取消的正常信号
- 不会触发
CoroutineExceptionHandler - 不会导致应用崩溃
- 在
finally块中仍可执行清理
scss
launch {
try {
delay(1000)
} finally {
println("Cleanup") // 即使被 cancel() 也会执行
}
}
不要把
CancellationException当作错误处理!
(8)异常聚合(Exception Aggregation)
当多个子协程同时失败 ,协程框架会将它们包装成一个 AggregateException(实际是第一个异常,其他作为 suppressed exceptions)。
kotlin
try {
coroutineScope {
launch { throw IOException("A") }
launch { throw IllegalArgumentException("B") }
}
} catch (e: Exception) {
println(e) // 主异常是 "A"
e.suppressed.forEach { println("Suppressed: $it") } // 包含 "B"
}
这在测试或批量任务中需要注意:不能只看主异常。
总结
| 概念 | 关键点 |
|---|---|
| CoroutineContext | Job + Dispatcher + ExceptionHandler + Name;子协程继承父上下文 |
| 异常传播 | 默认"全家桶取消";SupervisorJob 实现"独立失败" |
| 异常捕获 | 必须在协程内部 try-catch;外部 catch 无效 |
| CancellationException | 正常取消信号,不 crash,不触发 handler |
| Android 实践 | ViewModel 中每个 launch 自己处理异常;慎用全局 handler |