常用作用域:GlobalScope、viewModelScope、lifecycleScope 的使用场景与区别
说起协程作用域,我最早接触的时候也是一头雾水。三个作用域看起来都能启动协程,但什么时候用哪个,很多人搞混。我踩过几次坑之后,算是摸清了它们的脾气。
先看一张全景图
GlobalScope —— 慎用的“全球视野”
GlobalScope 是一个单例,它的生命周期跟整个应用一样长。说白了,你启动的协程如果不主动取消,它会一直跑下去,直到进程被干掉。
我记得刚接触协程那会儿,有个后台日志上报的需求,图省事直接用 GlobalScope.launch。结果页面关掉了,协程还在死循环上报,导致内存泄漏。后来排查才发现,OkHttp 的 Response 回调持有 Activity 引用,GC 都救不了。
- 生命周期:应用级别,进程存活则协程存活
- 取消机制:需要手动调用
cancel(),忘记就泄漏 - 典型场景:极少数需要全局单次任务(如初始化 SDK),而且必须保证不持有 UI 引用
// 错误示范:全局作用域持有 Activity 引用
GlobalScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO) { networkCall() } // OkHttp 请求
textView.text = result // 如果协程还没结束,Activity 就已经销毁,textView 泄露
}
viewModelScope —— ViewModel 的最佳搭档
这个作用域是 Jetpack 为 ViewModel 量身定做的。我建议你在 MVVM 架构里无脑用它,只要 ViewModel 活着,协程就活着;ViewModel 清理了,协程自动取消。
实际项目中我用它来处理所有业务数据加载。比如用户打开页面请求接口,如果用户迅速返回,onCleared() 一触发,协程就被取消,OkHttp 的请求也会被一并取消(前提是挂起函数正确实现了取消响应)。
class MyViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
val response = withContext(Dispatchers.IO) {
OkHttpClient().newCall(request).execute()
}
// 处理结果,此时如果 ViewModel 已清理,协程自动取消
}
}
}
lifecycleScope —— 细粒度到 Activity/Fragment 级别
lifecycleScope 是 AndroidX Lifecycle 提供的扩展,直接绑定到 Activity 或 Fragment 的生命周期。你想想看,当页面不可见时,你其实不需要继续执行某些协程,比如动画、轮询。用它正好。
我曾经遇到过一个直播聊天室项目,消息轮询写在 lifecycleScope 里,配合 repeatOnLifecycle 达到“前台才轮询,后台暂停”的效果。后来发现内存泄漏率直降为零。
| 启动方式 | 触发时机 | 取消时机 |
|---|---|---|
| lifecycleScope.launch | 任意生命周期时刻 | onDestroy |
| lifecycleScope.launchWhenCreated | 至少 Created 状态 | onDestroy |
| lifecycleScope.launchWhenStarted | 至少 Started 状态 | onStop 时暂停,onStart 恢复 |
| lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) | 每次进入 Started 状态 | 离开状态自动取消重启 |
// 更优雅的写法:只在界面可见时执行
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// 这里的协程会在 onStart -> onStop 之间重复执行
loadChatMessages()
}
}
三大作用域对比总结
把它们放在一起比一比,就很容易做决定了:
| 对比维度 | GlobalScope | viewModelScope | lifecycleScope |
|---|---|---|---|
| 生命周期范围 | 应用进程 | ViewModel 存活 | Activity/Fragment 存活 |
| 自动取消 | 否 | 是(onCleared) | 是(onDestroy) |
| 典型使用场景 | 全局一次性任务(慎用) | UI 相关的数据加载 | 界面可见性依赖的任务 |
| 与 OkHttp 混合泄漏风险 | 高(需手动管理引用) | 低(ViewModel 持有引用安全) | 低(Lifecycle 自动管理) |
避坑指南
曾经有一次,我在 RecyclerView 的 Adapter 里直接用了 GlobalScope.launch 加载图片。结果滑动飞快时,协程被大量创建,且不被取消,导致图片加载还在跑,但 ViewHolder 已经被回收。崩溃率一度飙升。后来换成 lifecycleScope 才解决。
嗯,这里要注意:如果你不得不使用 GlobalScope(比如在一些纯工具类库中),一定要确保它不持有任何 Activity/Context 引用,或者使用弱引用包装。我一般会封装一个自定义的协程作用域,配合超时机制,比如:
val appScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// 使用 appScope.launch { ... } 代替 GlobalScope
// 并在 Application.onTerminate() 或特定时机手动 appScope.cancel()
记住,选择作用域不只是 API 调用的问题,它直接决定了你的应用会不会泄漏。OkHttp 混合泄漏案例里,90% 的问题都出在作用域选择不当上。好好想想你的协程该绑在哪个生命周期上,比后续排查要省力得多。