第3章 默认Job与SupervisorJob选择
3.1 核心原理:失败传播机制
在 Kotlin 协程中,Job 和 SupervisorJob 是两种最常用的 Job 实现。它们最本质的区别在于子协程异常是否传播到父级。
- Job(默认):子协程抛出未捕获异常时,父 Job 立即取消,并导致所有活跃的子协程(包括兄弟协程)统一失败。这是“连带失败”模型,适用于整个任务组必须原子化成功/失败的场景。
- SupervisorJob:子协程的异常被隔离,父 Job 和兄弟子协程不受影响。这是“独立失败”模型,适合单个子任务失败不应影响其他任务的场景。
关键区别:
Job 的异常传播是自底向上再自顶向下的扩散,而 SupervisorJob 的异常仅在发生异常的子协程内处理。
3.2 传播流程图(Job vs SupervisorJob)
3.3 代码实现:创建 Scope 时的选择
在 Service 中,通常在 onCreate() 或初始化阶段创建协程 Scope。下面对比两种方式:
实践建议:使用
SupervisorJob() 时,父 Job 类型必须显式传入,否则默认仍为普通 Job()。
示例1:使用默认 Job(连带失败)
class MyService : Service() {
private val scope = CoroutineScope(Dispatchers.IO + Job()) // 默认Job
fun startTasks() {
scope.launch {
// 任务1
val result1 = fetchFromNetwork()
}
scope.launch {
// 任务2:抛出异常时会导致任务1也被取消
if (someCondition) throw RuntimeException("Task2 failed")
}
}
}
示例2:使用 SupervisorJob(独立失败)
class MyService : Service() {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) // 显式SupervisorJob
fun startTasks() {
scope.launch {
// 任务1
val result1 = fetchFromNetwork()
}
scope.launch {
// 任务2:异常不会影响任务1
if (someCondition) throw RuntimeException("Task2 failed")
}
}
}
3.4 概念对比
| 特性 | Job(默认) | SupervisorJob |
|---|---|---|
| 异常传播 | 子异常 → 父取消 → 所有子取消 | 子异常仅影响自身,父/兄弟不受影响 |
| Scope 类型 | CoroutineScope |
CoroutineScope + SupervisorJob() |
| 适合场景 | 原子化任务组(如数据库事务) | 独立子任务(如日志上报、缓存刷新) |
| 异常处理代价 | 整个 Scope 重建,开销大 | 仅重建失败子协程,开销小 |
| 默认行为 | 引发 JobCancellationException 传播 |
子协程异常被封装为 CompletionHandler 回调 |
3.5 实际场景选择策略
- 强一致性任务组(如数据同步时写库 + 上报 + 清理缓存):使用默认
Job(),任一子任务失败则整个任务组回滚或重试。 - 弱依赖独立任务(如首页加载多个 API 数据,单个失败允许重试而不影响其他):使用
SupervisorJob()。 - 混合场景:最外层 Scope 使用
SupervisorJob(),内部每个“原子组”再嵌套普通Job()。
常见错误:在
launch 中直接 catch 异常并忽略,却仍然使用了普通 Job()。这会导致异常虽然被 catch,但 JobCancellationException 仍然会向上传播,导致父 Scope 取消。正确做法:要么使用 SupervisorJob(),要么在祖父辈 catch 并处理。
3.6 调试与日志观察
可通过协程的 CoroutineExceptionHandler 或 invokeOnCompletion 来观察异常传播路径:
val handler = CoroutineExceptionHandler { _, exception ->
Log.e("ScopeTutorial", "Caught: ${exception.message}")
}
scope = CoroutineScope(Dispatchers.IO + SupervisorJob() + handler)
scope.launch {
delay(100)
throw RuntimeException("Test failure")
}
// 日志输出:只有该子协程的异常被 handler 捕获
// 父 Job 和兄弟协程仍然运行
若将 SupervisorJob() 换成 Job(),则日志中除了子协程异常外,还会看到 JobCancellationException 从父 Scope 传播。
3.7 性能与稳定性优化
在 Service 长期后台任务中,选择不当会导致Scope 反复重建,引发内存泄漏或资源浪费。参考下列优化原则:
- 首选
SupervisorJob()作为顶层 Scope,除非明确需要原子失败。 - 每个子任务内使用
try-catch处理预期异常,防止未捕获异常意外传播。 - 避免在
onDestroy()中cancel()时强制传播异常,优先使用scope.cancel()清理所有子协程。
经验法则:80% 的 Service 场景应使用
SupervisorJob(),只有并发事务型操作才用默认 Job()。选择正确后,异常处理代码量可减少 60% 以上。