前言
筑基境后阶已过,你已驯服 Dispatchers 四大护法。Main、IO、Default、Unconfined 各司其职,withContext 的线程切换在你手中如行云流水。你自信满满,将协程应用于生产环境。
然而,一个诡异的崩溃报告打破了你的宁静:
“用户在商品详情页快速点击收藏按钮,应用崩了。日志显示
IllegalStateException,但我在collect里明明写了try-catch!” “我用了viewModelScope.launch,里面启动了 3 个子协程并发加载数据。其中一个子协程网络超时抛异常,结果整个页面的其他数据也全没了!” “我在GlobalScope里加了CoroutineExceptionHandler,为什么协程崩溃后它根本没被调用?”
这些困惑指向协程中最容易被误解的领域——异常处理机制。协程的异常传播不是简单的 try-catch,而是一套结合了结构化并发、Job 树、以及 SupervisorJob 防火墙的精密体系。
本讲是筑基境的最终章。你将彻底掌握协程异常处理的天网系统:
- 理解异常在 Job 树上的传播路径——为什么“一个子协程崩溃,全家遭殃”。
- 掌握
CoroutineExceptionHandler的安装位置与生效条件。 - 看清
SupervisorJob如何一键构筑异常防火墙。 - 学会
supervisorScope的隔离之道——让局部失败不影响全局。 - 在 Android 实战中构建“永不崩溃”的 UI 协程架构。
准备好构筑异常天网,让协程的崩溃无处遁形了吗?我们开始。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
异常传播的铁律:沿 Job 树上行,直到根部
在结构化并发的体系中,异常传播遵循一条不可动摇的铁律:
子协程未捕获的异常,会沿着
Job树向上传播,逐层取消父Job,直到抵达树根(Root Job)。传播过程中,所有被取消的父协程会同步取消其所有子协程。
关键规则:
- 异常沿着
Job的父子链向上传播(子 → 父 → 根)。 - 一旦父 Job 被取消,它会向下级联取消所有其他子协程。
- 异常最终到达根 Job 时,如果没有被
CoroutineExceptionHandler处理,应用崩溃。
CoroutineExceptionHandler:异常处理的最后一道防线
CoroutineExceptionHandler是CoroutineContext的一个可选元素,用于处理未捕获的异常。它只在根协程上生效——即直接由CoroutineScope启动的协程(Scope 的直接子协程)。子协程上的CoroutineExceptionHandler会被忽略。
val handler = CoroutineExceptionHandler { _, exception ->
println("捕获异常:${exception.message}")
}
val scope = CoroutineScope(Job() + Dispatchers.Main + handler)
scope.launch {
// 这是根协程,handler 会生效
throw RuntimeException("根协程异常") // 被 handler 捕获
}
scope.launch {
launch {
// 这是子协程,即使加了 handler 也会被忽略
throw RuntimeException("子协程异常") // 向上传播到根
}
}
flowchart TD
subgraph Scope[CoroutineScope]
Handler[CoroutineExceptionHandler 置于此]
Root[根协程 launch]
Child[子协程 launch]
end
Child -->|异常向上传播| Root
Root -->|触发| Handler
Handler -->|处理或忽略| Result[应用不崩溃]
Child2[子协程上的 Handler] -.->|被忽略| Ignored
style Handler fill:#ffb74d,stroke:#e65100,stroke-width:2px
style Root fill:#c8e6c9,stroke:#2e7d32
style Child fill:#e3f2fd,stroke:#1976d2
style Ignored fill:#ef9a9a
核心结论:
CoroutineExceptionHandler必须安装在根协程(直接由 Scope 启动)上。- 子协程上的 Handler 形同虚设——异常会继续向上传播。
- 如果异常到达根部仍无 Handler 处理,则应用崩溃。
SupervisorJob:构筑异常防火墙
在默认的 Job 树中,“一个子协程崩溃,全家遭殃”。但很多场景下你需要隔离子协程的异常——一个子协程失败,不应影响兄弟协程。
这就是 SupervisorJob 的用武之地。
SupervisorJob是Job的一种特殊实现。它重写了childCancelled方法,使其不将子协程的取消事件传播给自身或其他子协程。本质上,它构筑了一道单向防火墙——异常到此为止,不再向上也不向旁传播。
val supervisor = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Main + supervisor)
scope.launch {
// 子协程 A
launch {
delay(100)
throw RuntimeException("A 崩溃了") // 只影响 A 自身
}
// 子协程 B
launch {
delay(200)
println("B 依然执行") // 不受 A 影响
}
}
graph TD
subgraph NormalJob[普通 Job 树 连坐制]
NJ[父 Job] --> NC1[子协程 A]
NJ --> NC2[子协程 B]
NC1 -.->|崩溃| NJ
NJ -.->|取消| NC2
end
subgraph SupervisorJob[SupervisorJob 树 隔离制]
SJ[SupervisorJob] --> SC1[子协程 A]
SJ --> SC2[子协程 B]
SC1 -.->|崩溃| SJ
SJ -.->|不传播| SC2
end
style NormalJob fill:#ffcdd2,stroke:#b71c1c,stroke-width:2px
style SupervisorJob fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style NJ fill:#e57373
style SJ fill:#81c784
Android 中的重要事实:viewModelScope 和 lifecycleScope 底层都使用 SupervisorJob。这意味着你在 viewModelScope.launch 中启动的多个根协程,它们之间天然是隔离的。
supervisorScope:结构化并发的可配置隔离区
supervisorScope 是 SupervisorJob 的语法糖。它创建一个使用 SupervisorJob 的临时作用域,内部子协程彼此隔离。
suspend fun loadProductData() = supervisorScope {
val productDeferred = async { loadProduct() } // 必须成功
val reviewsDeferred = async { loadReviews() } // 必须成功
val recommendsDeferred = async { // 可失败
try {
loadRecommends()
} catch (e: Exception) {
emptyList()
}
}
Triple(
productDeferred.await(),
reviewsDeferred.await(),
recommendsDeferred.await()
)
}
supervisorScope vs coroutineScope:
| 对比维度 | coroutineScope | supervisorScope |
|---|---|---|
| 内部使用的 Job | 普通 Job | SupervisorJob |
| 子协程异常行为 | 一个失败,全体取消 | 一个失败,互不影响 |
| 自身异常传播 | 子协程未捕获异常向上抛出 | 子协程未捕获异常向上抛出 |
| 适用场景 | 关键任务组合,必须全部成功 | 部分任务可失败,不影响整体 |
实战:商品详情页的异常隔离架构
class ProductDetailViewModel : ViewModel() {
sealed class UiState {
object Loading : UiState()
data class Success(
val product: Product,
val reviews: List<Review>,
val recommends: List<Product>,
val recommendError: String? = null
) : UiState()
data class Error(val message: String) : UiState()
}
var uiState by mutableStateOf<UiState>(UiState.Loading)
private set
fun loadProduct(productId: String) {
viewModelScope.launch {
uiState = UiState.Loading
uiState = try {
supervisorScope {
val productDeferred = async { productRepo.getProduct(productId) }
val reviewsDeferred = async { reviewRepo.getReviews(productId) }
val recommendsDeferred = async {
try {
recommendRepo.getRecommends(productId)
} catch (e: Exception) {
emptyList()
}
}
UiState.Success(
product = productDeferred.await(),
reviews = reviewsDeferred.await(),
recommends = recommendsDeferred.await()
)
}
} catch (e: Exception) {
UiState.Error(e.message ?: "核心数据加载失败")
}
}
}
}
flowchart TD
Start[用户进入详情页] --> Launch[viewModelScope.launch]
Launch --> SS[supervisorScope]
SS --> Product[async: 商品信息 必须成功]
SS --> Reviews[async: 用户评价 必须成功]
SS --> Recs[async: 推荐列表 可失败]
Recs -->|内部 try-catch| RecOK[返回空列表]
Product --> Await[await 所有结果]
Reviews --> Await
RecOK --> Await
Await -->|成功| ShowContent[展示完整页面]
Await -->|核心数据失败| ShowError[展示错误信息]
style Start fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style SS fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style ShowContent fill:#a5d6a7
style ShowError fill:#ef9a9a
常见错误与避坑指南
错误 1:在子协程上安装 CoroutineExceptionHandler
scope.launch {
launch(CoroutineExceptionHandler { _, _ -> }) {
throw Exception() // Handler 不生效,异常向上传播
}
}
正确做法:Handler 必须安装在根协程或 Scope 初始化时。
错误 2:用 try-catch 包裹 launch 期望捕获内部异常
try {
scope.launch {
throw Exception() // 不会被外部 try-catch 捕获
}
} catch (e: Exception) { }
正确做法:launch 内部的异常需在协程体内部 try-catch,或使用 CoroutineExceptionHandler。
错误 3:在 supervisorScope 中不处理 async 的异常
supervisorScope {
val d = async { throw Exception() }
d.await() // 异常仍会抛出,导致 supervisorScope 失败
}
正确做法:对可能失败的 async,在内部 try-catch 或在 await 处包裹。
最佳实践
- 为根 Scope 安装
CoroutineExceptionHandler:作为最后的兜底防线。 - UI 层使用
viewModelScope+SupervisorJob的天然隔离。 supervisorScope用于挂起函数内部的隔离边界。- 对可能失败的子任务,在
async内部try-catch,不让异常传播。 - 核心任务与辅助任务分离:核心用
coroutineScope,辅助用supervisorScope。
总结与下回预告
恭喜,你已构筑异常天网,筑基境大圆满!
本讲核心收获:
- 异常沿 Job 树向上传播,
CoroutineExceptionHandler只在根协程生效。 SupervisorJob通过空实现childCancelled构筑防火墙。supervisorScope是创建隔离边界的最佳工具。- Android 中
viewModelScope底层使用SupervisorJob,天然隔离。
在下一讲 【金丹境·初阶】 中,我们将踏入 async/await 的并发艺术。你将学会如何用 Deferred 并发组合多个请求,用 LAZY 实现条件启动,以及结构化并发在 async 场景下的优雅应用。
【当前境界修为面板】
- 当前境界:
[筑基境 · 巅峰]✅ 筑基境大圆满 - 下一突破:
[金丹境 · 初阶](需领悟:async/await并发组合、Deferred结果等待、CoroutineStart.LAZY) - 修炼进度:
[████████████████████░░░░] 80% - 本讲获得法器:
异常传播天网诀、SupervisorJob 防火墙术、supervisorScope 隔离结界
【本讲思考题】
1、表象题:以下代码中,CoroutineExceptionHandler 会生效吗?为什么?
scope.launch {
launch(CoroutineExceptionHandler { _, _ -> println("捕获") }) {
throw RuntimeException()
}
}
2、场景题:你在 ViewModel 中并发加载 3 个接口数据。其中商品接口和评价接口必须全部成功才能展示,推荐接口失败则只展示留空。如何用协程实现这个需求?
3、原理题:SupervisorJob 的 childCancelled 方法为什么是空实现?请从异常传播链的角度简述。
道友,筑基四境已全部通关。你已打通全身经脉,协程的基本功炉火纯青。下一讲,我们将踏入金丹境,学习并发组合的高阶技法。金丹境·初阶见。
欢迎一键四连(
关注+点赞+收藏+评论)