常用作用域:GlobalScope、viewModelScope、lifecycleScope 的使用场景与区别

说起协程作用域,我最早接触的时候也是一头雾水。三个作用域看起来都能启动协程,但什么时候用哪个,很多人搞混。我踩过几次坑之后,算是摸清了它们的脾气。

先看一张全景图

协程作用域全景图 GlobalScope 应用级 · 不自动取消 viewModelScope ViewModel 生命周期 lifecycleScope Activity/Fragment 生命周期 协程存活 直到进程死亡 onCleared() 自动取消 onDestroy() 自动取消 ⚠ 混合泄漏高风险 OkHttp 回调不被取消 ✅ 配合 OkHttp 安全 取消后自动释放 ✅ 更细粒度控制 配合 Lifecycle 事件 📌 最佳实践 日常开发优先使用 viewModelScope / lifecycleScope,避免用 GlobalScope

GlobalScope —— 慎用的“全球视野”

GlobalScope 是一个单例,它的生命周期跟整个应用一样长。说白了,你启动的协程如果不主动取消,它会一直跑下去,直到进程被干掉。

我记得刚接触协程那会儿,有个后台日志上报的需求,图省事直接用 GlobalScope.launch。结果页面关掉了,协程还在死循环上报,导致内存泄漏。后来排查才发现,OkHttp 的 Response 回调持有 Activity 引用,GC 都救不了。

核心特点:
  • 生命周期:应用级别,进程存活则协程存活
  • 取消机制:需要手动调用 cancel(),忘记就泄漏
  • 典型场景:极少数需要全局单次任务(如初始化 SDK),而且必须保证不持有 UI 引用
警告:不要在 Activity/Fragment 中使用 GlobalScope 启动协程去更新 UI。我见过同事因为这样做,页面销毁后协程还在执行,然后 onSuccess 回调里更新了一个已经死掉的 TextView,直接崩了。
// 错误示范:全局作用域持有 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 的请求也会被一并取消(前提是挂起函数正确实现了取消响应)。

个人习惯:我大部分网络请求都放在 ViewModel 里,使用 viewModelScope.launch。配合 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()
    }
}

三大作用域对比总结

把它们放在一起比一比,就很容易做决定了:

对比维度GlobalScopeviewModelScopelifecycleScope
生命周期范围应用进程ViewModel 存活Activity/Fragment 存活
自动取消否是(onCleared)是(onDestroy)
典型使用场景全局一次性任务(慎用)UI 相关的数据加载界面可见性依赖的任务
与 OkHttp 混合泄漏风险高(需手动管理引用)低(ViewModel 持有引用安全)低(Lifecycle 自动管理)
一句话决策:能用 lifecycleScope 就用 lifecycleScope,否则用 viewModelScope,永远别碰 GlobalScope,除非你真的懂它的后果。

避坑指南

曾经有一次,我在 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% 的问题都出在作用域选择不当上。好好想想你的协程该绑在哪个生命周期上,比后续排查要省力得多。