第3章 协程作用域与结构化并发
3.1 结构化并发核心概念
结构化并发是 Kotlin 协程的基石,它确保所有子协程的生命周期绑定到父作用域,避免协程泄漏。核心组件包括:
- CoroutineScope —— 协程运行的上下文容器,管理子协程的生命周期。
- Job —— 协程句柄,可追踪状态(New、Active、Completing、Completed、Cancelling、Cancelled),支持父子层级。
- SupervisorJob —— 特殊 Job,子协程失败不会传播取消给兄弟或父协程。
- 取消机制 —— 通过 job.cancel() 触发 CancellationException,协程内部可通过 isActive 或 suspend 函数检查取消。
- onCancellation —— 在 finally 块中通过 isCancelled 判断,或使用 invokeOnCompletion 回调处理释放资源。
| 组件 | 职责 | 取消行为 |
|---|---|---|
| CoroutineScope | 创建协程的入口,提供 Job 和 Dispatcher | 取消 scope 会取消所有子协程 |
| Job | 控制单个协程生命周期 | 取消 job 本身及其子 job |
| SupervisorJob | 允许子协程独立失败 | 子协程失败不会取消兄弟或父协程 |
在 ViewModel 或 Fragment 中使用 viewModelScope 或 lifecycleScope,它们内部使用 SupervisorJob,确保单个数据请求失败不会影响其他请求。
3.2 结构化并发流程图
3.3 创建与取消协程作用域
两种常用方式:
// 方式1:使用 CoroutineScope 工厂
val scope = CoroutineScope(Dispatchers.IO + Job())
scope.launch {
delay(1000); println("协程运行")
}
scope.cancel() // 取消所有子协程
// 方式2:使用 SupervisorJob 允许独立失败
val supervisorScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
supervisorScope.launch {
throw RuntimeException("子任务A失败")
}
supervisorScope.launch {
delay(500); println("子任务B仍然执行")
}
// 即使A失败,B仍正常完成
注意:在创建 scope 时,如果使用 SupervisorJob() ,必须将其作为参数传入,否则默认使用普通的 Job。SupervisorJob 默认不会影响父协程,但需要显式放在作用域中。
3.4 onCancellation 处理与资源清理
协程取消时会抛出 CancellationException,在 finally 块中通过 isCancelled 或 invokeOnCompletion 进行处理:
val job = scope.launch {
try {
repeat(100) { i ->
delay(1000)
println("处理第 $i 项")
}
} finally {
// 不管正常结束还是取消都会执行
if (isActive) {
println("正常完成")
} else {
println("协程被取消,执行清理...")
// 注意:在 finally 中调用 suspend 函数会触发取消异常,需用 withContext(NonCancellable)
withContext(NonCancellable) {
releaseResources()
}
}
}
}
// 取消触发
delay(2500); job.cancel()
3.5 常见错误与调试
- 错误1:未处理 CancellationException
在 catch 中捕获Exception而不是CancellationException,导致协程无法正常响应取消。应使用catch (e: CancellationException) { throw e }重新抛出。 - 错误2:SupervisorJob 误用为最外层作用域
使用runBlocking(SupervisorJob())会导致 runBlocking 本身在子协程失败时立即结束,不符合预期。SupervisorJob 只适用于 launch/async 的子协程。 - 错误3:在 finally 中调用 suspend 函数
协程取消后 finally 块中调用 delay、withContext 等 suspend 函数会再次抛出 CancellationException,需用withContext(NonCancellable)包裹。
调试技巧:在协程的
invokeOnCompletion 回调中打印状态及异常信息,便于追踪取消链。使用 CoroutineExceptionHandler 全局捕获未处理的异常。3.6 性能与稳定性优化建议
- 作用域复用:避免在每次请求时创建新的 CoroutineScope,应复用 Activity/Fragment 的生命周期作用域 (lifecycleScope, viewModelScope),减少创建开销。
- SupervisorJob 分离故障域:在 UI 层使用 SupervisorJob,确保单个网络请求失败不会导致所有协程取消;在后台数据同步层可以使用普通 Job 以保证原子性。
- 取消敏感协程使用 NonCancellable:对于必须完成的关键操作(如数据库写入),用
withContext(NonCancellable)保证执行完成。 - 避免泄漏:在作用域取消时,子协程仍可能持有资源,务必在 finally 或 invokeOnCompletion 中释放网络连接、文件句柄等。