在Kotlin协程的世界里,Job
和SupervisorJob
是两个核心概念,它们在协程异常处理和父子关系上有着关键的区别。作为Android开发者或Kotlin后端工程师,理解这两者的差异对于构建健壮的异步应用至关重要。
Job:严格的父子关系
Job
是Kotlin协程中的基本构建块,代表一个可取消的、有生命周期的工作单元。在默认的父子Job关系中:
- 异常传播:当子协程抛出异常时,异常会向上传播到父Job
- 自动取消:父Job会取消所有其他子协程
- 级联效应:这种取消会继续向上传播,可能导致整个协程层次结构的崩溃
fun main() = runBlocking {
val parentJob = Job()
CoroutineScope(parentJob).launch {
// 子协程1
launch {
delay(100)
println("子协程1完成")
}
// 子协程2 - 会抛出异常
launch {
delay(50)
throw RuntimeException("子协程2失败!")
}
}
parentJob.join()
}
// 输出:什么都不会打印,因为异常导致所有协程被取消
SupervisorJob:宽松的父子关系
SupervisorJob
是Job
的一种特殊变体,它改变了异常传播的行为:
- 隔离异常:子协程的失败不会影响其他子协程
- 局部化失败:只有抛出异常的协程本身会被取消
- 手动处理:需要开发者显式处理子协程的异常
fun main() = runBlocking {
val supervisorJob = SupervisorJob()
CoroutineScope(supervisorJob).launch {
// 子协程1
launch {
delay(100)
println("子协程1完成") // 这个会正常执行
}
// 子协程2 - 会抛出异常
launch {
delay(50)
throw RuntimeException("子协程2失败!")
}
}
delay(200)
supervisorJob.cancel() // 记得清理资源
}
// 输出:
// 子协程1完成
关键区别总结
特性 | Job | SupervisorJob |
---|---|---|
异常传播 | 向上传播,取消所有子协程 | 不传播,仅取消失败协程 |
使用场景 | 需要原子性操作 | 需要独立运行的子任务 |
子协程独立性 | 低 | 高 |
异常处理方式 | 集中处理 | 分散处理 |
注意:安卓的viewModelScope和lifecycleScope都默认使用了SupervisorJob
1. viewModelScope源码分析
viewModelScope
的实现可以在androidx.lifecycle:lifecycle-viewmodel-ktx
库中找到。
核心源码路径:
androidx/lifecycle/ViewModel.kt
androidx/lifecycle/ViewModelKt.kt
关键代码片段:
// androidx/lifecycle/ViewModel.kt
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}
从这段代码可以清楚地看到:
viewModelScope
使用SupervisorJob()
作为根Job- 它结合了
Dispatchers.Main.immediate
调度器 - 通过
CloseableCoroutineScope
包装,在ViewModel清除时自动取消
2. lifecycleScope源码分析
lifecycleScope
的实现可以在androidx.lifecycle:lifecycle-runtime-ktx
库中找到。
核心源码路径:
androidx/lifecycle/Lifecycle.kt
androidx/lifecycle/LifecycleController.kt
关键代码片段:
// androidx/lifecycle/Lifecycle.kt
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
val Lifecycle.coroutineScope: LifecycleCoroutineScope
get() {
while (true) {
val existing = getTag(LifecycleCoroutineScopeImpl.JOB_KEY) as LifecycleCoroutineScopeImpl?
if (existing != null) {
return existing
}
val newScope = LifecycleCoroutineScopeImpl(
this,
SupervisorJob() + Dispatchers.Main.immediate
)
if (compareAndSetTag(LifecycleCoroutineScopeImpl.JOB_KEY, null, newScope)) {
newScope.register()
return newScope
}
}
}
从这段代码可以看到:
lifecycleScope
同样使用SupervisorJob()
作为根Job- 也使用了
Dispatchers.Main.immediate
调度器 - 通过
LifecycleCoroutineScopeImpl
实现与生命周期的自动绑定
3. 为什么使用SupervisorJob?
从源码分析可以确认两者都使用了SupervisorJob
,这种设计选择有几个重要原因:
- 隔离子协程的失败:在UI相关的Scope中,我们不希望一个子协程的失败导致整个Scope被取消
- 更符合UI开发模式:不同UI操作通常是独立的,一个操作的失败不应影响其他操作
- 更灵活的异常处理:允许开发者对每个子协程单独处理异常
4. 验证实验
我们可以通过一个小实验来验证这个行为:
class MyViewModel : ViewModel() {
fun testJobs() {
viewModelScope.launch {
// 子协程1 - 会失败
launch {
delay(500)
throw RuntimeException("子协程1失败")
}
// 子协程2 - 应该继续执行
launch {
repeat(5) {
delay(1000)
println("子协程2仍在运行: ${it + 1}")
}
}
}
}
}
运行结果:
子协程2仍在运行: 1
子协程2仍在运行: 2
子协程2仍在运行: 3
子协程2仍在运行: 4
子协程2仍在运行: 5
这个实验证明了即使一个子协程失败,其他子协程仍能继续运行,这正是SupervisorJob
的行为特征。
5. 总结
通过源码分析和实际验证,我们可以确认:
- viewModelScope:确实使用了
SupervisorJob
+Dispatchers.Main.immediate
- lifecycleScope:同样使用了
SupervisorJob
+Dispatchers.Main.immediate
- 设计意图:这种设计使Android的协程Scope更适合UI开发场景,提供了更合理的默认行为