一句话总结
- Job → “连坐制” :一个子协程崩溃,所有兄弟协程和父协程全被取消。
- SupervisorJob → “责任隔离” :一个子协程崩溃,其他兄弟协程和父协程不受影响。
一、异常传播机制:Job的“连坐”之道
在 Kotlin 协程中,Job 的异常处理机制遵循一种严格的父子关系。当一个子协程因未捕获的异常而失败时,它会执行以下两个关键操作:
- 向上传播异常:子协程会将异常向其父协程传播。
- 触发父协程的取消:父协程在接收到异常后,会立即将自己标记为“失败”,并进入取消状态。
- 递归取消:一旦父协程被取消,它会递归地取消所有其他子协程。
正是这种“异常向上,取消向下”的传播机制,导致了“连坐制”:一个子协程的失败会像多米诺骨牌一样,导致整个协程家族的崩溃。这种行为模式适用于那些所有子任务都必须成功的场景,一个失败就意味着整个任务的失败。
二、责任隔离:SupervisorJob的“拦截”策略
SupervisorJob 旨在打破 Job 的异常传播规则。它的核心思想是**“谁犯错,谁负责”**。
- 核心机制:
SupervisorJob会拦截子协程的异常,阻止其向上传播给父协程。 - 独立失败:当一个子协程失败时,它只会取消自己,而不会影响其兄弟协程或父协程。这使得开发者可以独立处理每个协程的错误,而不会导致整个协程作用域的崩溃。
- 手动捕获:需要注意的是,
SupervisorJob并不会自动处理异常。未捕获的异常仍然会从协程的根部抛出,如果无人处理,最终仍会导致应用崩溃。因此,必须手动捕获异常(使用try-catch或CoroutineExceptionHandler)。
三、实践中的选择:何时使用哪种Job?
选择使用 Job 还是 SupervisorJob,取决于任务之间的关联性。
| 选择 | 场景 | 示例 |
|---|---|---|
| Job | 多个任务之间存在强依赖关系,任何一个失败都意味着整个流程失败。 | 用户注册流程:创建用户 -> 发送验证邮件 -> 跳转主页。如果发送邮件失败,整个注册流程都应回滚或取消。 |
| SupervisorJob | 多个任务之间相互独立,一个任务的失败不应该影响其他任务。 | 主页数据加载:加载用户信息、加载推荐列表、加载广告。如果广告加载失败,用户和推荐列表仍然可以正常显示。 |
四、高级异常处理:CoroutineExceptionHandler
无论是 Job 还是 SupervisorJob,CoroutineExceptionHandler 都是处理未捕获异常的最后一道防线。
- 如何使用:
CoroutineExceptionHandler可以作为一个元素添加到CoroutineContext中。当协程中抛出未捕获异常时,它会被CoroutineExceptionHandler拦截并处理。 - 最佳实践:在 Android 开发中,通常将
CoroutineExceptionHandler添加到全局的CoroutineScope中,用于记录崩溃日志或执行其他清理操作,从而防止应用崩溃。
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught exception: $exception")
}
// SupervisorJob + CoroutineExceptionHandler
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch {
launch { throw RuntimeException("子协程A炸了!") } // ✅ 异常被handler捕获
launch { delay(2000); println("子协程B正常执行!") } // ✅ 不受影响
}
五、总结
Job 的“连坐制”提供了严格的同步,确保任务的原子性。SupervisorJob 的“责任隔离”则提供了更高的健壮性,使独立任务可以并行执行,互不影响。理解这两者的工作原理,并结合 CoroutineExceptionHandler 进行异常处理,是编写健壮、高效的 Kotlin 协程代码的关键。