3. 协程作用域:GlobalScope、CoroutineScope、lifecycleScope、viewModelScope的区别与风险
协程作用域这东西,说白了就是协程的“生命周期容器”。你启动一个协程,总得知道它该跟谁同生共死吧?选错了作用域,轻则内存泄漏,重则应用崩溃。我踩过不少坑,今天咱们一个个说清楚。
核心结论:作用域决定了协程的生命周期边界。用错了,你的协程要么永生(泄漏),要么被意外杀死(崩溃)。
1. GlobalScope —— 全局打工人,没人管你死活
GlobalScope 是 Kotlin 内置的全局协程作用域。它的生命周期跟应用进程一样长——你启动的协程,除非手动 cancel 或者进程被杀,否则永远跑着。
我曾经在一个项目里用 GlobalScope 启动了一个网络请求,结果用户切到后台后 Activity 销毁了,但协程还在跑。回传结果时更新 UI 直接崩了。嗯,那是个血泪教训。
为什么会这样?因为 GlobalScope 不绑定任何 Android 组件。你只管发射协程,它压根不管谁在监听结果。说白了就是「启动后忘掉」模式。
// 危险示例 —— 不要这么写
GlobalScope.launch(Dispatchers.IO) {
val result = doNetworkCall()
// 此时可能 Activity 已经销毁
withContext(Dispatchers.Main) {
textView.text = result // 崩溃:View 已销毁
}
}
| 特性 | GlobalScope |
|---|---|
| 生命周期 | 应用进程 |
| 自动取消 | 否 |
| 推荐场景 | 极少场景,如:应用全局数据预加载 |
| 风险 | 内存泄漏、空指针、资源浪费 |
我建议:除了非常明确的后台常驻任务(比如统计 SDK 上报),不要在 Android 应用里用 GlobalScope 做 UI 相关的事。
2. CoroutineScope —— 手动挡,自己把控方向盘
CoroutineScope 是一个接口,你可以自己创建实例并绑定一个 Job。生命周期完全由你控制——你可以在需要的时候调用 scope.cancel() 来结束所有子协程。
我习惯在自定义 View 或者某些生命周期不明确的组件里用 CoroutineScope。举个例子:
class MyCustomView(context: Context) : View(context) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
fun startHeavyWork() {
scope.launch {
// 做一些异步操作
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
scope.cancel() // 记得取消!
}
}
注意这里的 SupervisorJob()。它的作用是:如果一个子协程失败了,不会影响其他兄弟协程。这在 UI 层非常实用,你想想看,网络请求失败了,总不能把整个列表刷新协程都干掉吧?
避坑指南:CoroutineScope 最怕忘记 cancel。我见过有人创建了 scope 后忘了在销毁时取消,结果协程泄漏导致 OOM。建议使用 lifecycleScope 或 viewModelScope 代替手动管理,除非你明确需要自定义生命周期。
3. lifecycleScope —— Activity/Fragment 的最佳搭档
lifecycleScope 是 AndroidX 提供的扩展属性,直接绑定组件的 Lifecycle。当 Activity 或 Fragment 进入 DESTROYED 状态时,所有协程自动取消。
我个人觉得这是绝大多数 UI 操作的默认选择。你不需要写任何 cancel 逻辑,框架帮你兜底。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launch {
// 这个协程在 Activity 销毁时自动取消
val data = fetchData()
updateUI(data)
}
}
}
这里有个小细节:lifecycleScope 默认使用 Dispatchers.Main。如果你需要 IO 操作,记得切换调度器:
lifecycleScope.launch(Dispatchers.IO) {
val result = heavyIoWork()
withContext(Dispatchers.Main) {
// 更新 UI
}
}
我建议:当协程需要跟 Activity/Fragment 同生共死时,无脑选 lifecycleScope。但注意,如果配置变更导致 Activity 重建,lifecycleScope 也会被取消——新 Activity 会创建一个全新的 scope。
4. viewModelScope —— ViewModel 的专属领域
viewModelScope 绑定 ViewModel 的 onCleared() 方法。当 ViewModel 不再被使用(比如 Activity 被 finish)时,协程自动取消。
它和 lifecycleScope 最大的区别是什么?生命周期边界不同。ViewModel 的生命周期比 Activity 更宽——即使 Activity 因为配置变更重建,ViewModel 仍然存活。所以 viewModelScope 中的协程在屏幕旋转时不会中断,非常适合加载数据。
class MyViewModel : ViewModel() {
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state
fun loadData() {
viewModelScope.launch {
try {
val data = repository.fetchData()
_state.value = UiState.Success(data)
} catch (e: Exception) {
_state.value = UiState.Error(e)
}
}
}
}
| 作用域 | 生命周期终点 | 配置变更后是否重建 | 使用频率 |
|---|---|---|---|
| lifecycleScope | onDestroy | 是(新建 scope) | 极高(UI 层) |
| viewModelScope | onCleared | 否(保留 scope) | 极高(ViewModel 层) |
最佳实践:ViewModel 层面的业务逻辑、数据加载用 viewModelScope;Activity/Fragment 里的 UI 操作(如动画、弹窗)用 lifecycleScope。各司其职,互不干涉。
5. 风险对比与选择建议
我总结了四句话,帮你快速决策:
- 不要用 GlobalScope —— 除非你明确知道自己在干嘛,并且愿意手动管理所有协程的生命周期。
- 谨慎用 CoroutineScope —— 适用于自定义生命周期对象,但别忘记 cancel。
- 大胆用 lifecycleScope —— Activity/Fragment 内首选,自动取消最省心。
- 必用 viewModelScope —— ViewModel 中数据加载的默认选择,旋转屏幕也不中断。
我曾经接手过一个老项目,里面大量使用 GlobalScope 做网络请求。结果每次页面切换后,后台协程还在跑,回调时拿着过时的 context 更新 UI,空指针崩溃报表一片红。后来我们逐个替换成 viewModelScope 和 lifecycleScope,崩溃率直接降了 80%。
另外说一句,如果你用的是 lifecycle-runtime-ktx 2.4.0+ 版本,注意 lifecycleScope 在 Lifecycle 处于 STARTED 状态时才会启动协程。如果你在后台启动了协程并且需要一直执行,可以考虑 lifecycleScope.launchWhenResumed 等细化控制,但那是另一堂课的内容了。
小技巧:如果你实在需要全局后台任务,可以考虑用 WorkManager 而不是 GlobalScope。它比协程更适合长时间运行的、可持久化的异步任务。