协程作用域与生命周期管理:viewModelScope、lifecycleScope、自定义作用域、取消与异常
协程作用域决定了协程的存活范围与取消传播方式。在Android组件中,将协程绑定到组件的生命周期上,可以避免内存泄漏和无效的后台操作。本章深入剖析viewModelScope、lifecycleScope以及自定义作用域的原理,并详细讲解取消与异常处理的工程实践。
核心原则:协程作用域的取消应当自动且及时——当组件销毁时,所有在该作用域内启动的协程都应被取消,以释放资源和避免泄露。
2.1 内置作用域架构
Kotlin协程提供了两个与Android生命周期深度整合的内置作用域:
| 作用域 | 绑定的生命周期 | 取消时机 | 典型使用场景 |
|---|---|---|---|
viewModelScope |
ViewModel | ViewModel的onCleared()被调用时 |
负责数据加载、网络请求、数据库操作等与UI无关的后台任务 |
lifecycleScope |
LifecycleOwner(Activity/Fragment) | Lifecycle到达DESTROYED状态时 |
直接需要在UI层面执行的协程,如收集Flow、更新界面 |
区分用法:如果协程需要在配置变更(如屏幕旋转)后继续,请使用
viewModelScope;如果协程需要随当前Activity/Fragment的生命周期同步取消,请使用lifecycleScope。
2.2 生命周期与协程取消的绑定机制
下面是Android组件生命周期与协程作用域取消的对应关系图:
2.3 代码实现流程
2.3.1 viewModelScope 使用示例
class MyViewModel : ViewModel() {
init {
// 在viewModelScope中启动协程,协程随ViewModel生命周期绑定
viewModelScope.launch {
val data = repository.fetchData()
_uiState.value = UiState.Success(data)
}
}
override fun onCleared() {
super.onCleared()
// 自动取消所有viewModelScope内的协程,不需要手动调用
}
}
2.3.2 lifecycleScope 使用示例
class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 在lifecycleScope中启动,当Fragment进入DESTROYED时自动取消
lifecycleScope.launch {
viewModel.uiState.collect { state ->
// 更新UI
textView.text = state.toString()
}
}
// 也支持在指定的Lifecycle状态时启动
lifecycleScope.launchWhenResumed {
// 仅在RESUMED状态下活跃,离开RESUMED时挂起,重新RESUMED时恢复
}
}
}
2.3.3 自定义作用域实现
当内置作用域无法满足需求时(例如:后台服务、跨组件共享),可以使用CoroutineScope配合SupervisorJob手动管理生命周期。
class MyBackgroundService {
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun startWork() {
serviceScope.launch {
// 长期运行的后台任务
while (isActive) {
doPolling()
delay(5000)
}
}
}
fun stop() {
serviceScope.cancel() // 取消所有子协程
}
}
注意:自定义作用域必须显式调用
cancel(),否则协程会一直存活,可能导致内存泄漏或资源耗尽。推荐使用SupervisorJob而非默认的Job,以便一个子协程失败不会连带取消其他子协程。
2.4 取消与异常处理
2.4.1 取消机制原理
- 取消是通过
Job.cancel()触发的,协程会在下一个挂起点(如delay()、withContext等)抛出CancellationException。 - 协程体必须配合
isActive检查,或使用ensureActive()来响应取消,否则长时间运行的CPU密集型任务可能忽略取消信号。 - 挂起函数如
withContext(Dispatchers.Default) { ... }会自动检查取消状态。
2.4.2 异常传播与处理
| 异常类型 | 传播行为 | 处理方式 |
|---|---|---|
CancellationException |
不会触发CoroutineExceptionHandler,只用于取消流程 |
通常不必捕获;若需清理资源可使用try/catch/finally |
普通异常(如IOException) |
向父Job传播,会取消兄弟协程(除非使用SupervisorJob) |
使用try/catch局部处理,或通过CoroutineExceptionHandler全局处理 |
2.4.3 异常处理实践
// 使用SupervisorJob防止子协程异常影响兄弟协程
viewModelScope.launch(SupervisorJob() + CoroutineExceptionHandler { _, throwable ->
Log.e("TAG", "协程异常:${throwable.message}")
}) {
// 子协程1
launch {
throw RuntimeException("出现异常")
}
// 子协程2 — 不受子协程1异常影响
launch {
delay(100)
Log.d("TAG", "继续执行")
}
}
最佳实践:在
viewModelScope和lifecycleScope中启动协程时,尽量在每个协程内部使用try/catch捕获业务异常,避免未捕获异常传播到顶层导致应用崩溃。也可以使用CoroutineExceptionHandler作为兜底。
2.5 调试技巧
- Android Studio Coroutines Debugger:在Debug模式下,点击“Coroutines”标签可查看所有活跃协程的作用域、状态和堆栈。
- 日志输出:在协程启动和取消处添加日志,观察生命周期事件。
- 使用
onCleared()断点:验证ViewModel销毁时viewModelScope是否被取消。
viewModelScope.launch {
Log.d("TAG", "协程启动,viewModelScope的isActive = ${coroutineContext[Job]?.isActive}")
try {
delay(3000)
} catch (e: CancellationException) {
Log.d("TAG", "协程被取消:${e.message}")
throw e // 重新抛出是惯例
}
}
2.6 常见错误与避坑
- 未取消自定义作用域:忘记在
onDestroy()或onStop()中调用scope.cancel(),导致协程持续运行,引发内存泄漏。 - 在错误的作用域启动IO操作:在
viewModelScope中使用Dispatchers.Main执行网络请求,会阻塞主线程,应显式指定Dispatchers.IO。 - 捕获
CancellationException后不重新抛出:导致协程无法正常取消,可能产生僵尸任务。 - 使用
GlobalScope启动协程:生命周期不受控制,极易造成泄漏,应坚决避免。
性能优化建议:
- 使用
SupervisorJob避免单个子协程异常导致整个作用域被取消,减少不必要的重启开销。 - 在
lifecycleScope中长时间收集Flow时,优先使用flowWithLifecycle或repeatOnLifecycle来利用生命周期感知收集,只在可见状态下消耗资源。 - 取消不必要的协程:在ViewModel启动多个网络请求时,使用
Job().cancel()或viewModelScope.launch { job.cancelAndJoin() }主动取消已过时的请求。
2.7 小结
理解协程作用域与生命周期管理的本质,能够帮助你写出稳定、无泄漏的Android协程代码。掌握viewModelScope与lifecycleScope的绑定规则,合理使用SupervisorJob和自定义作用域,并正确处理取消与异常,是生产级应用的基础保障。