3. 协程作用域:GlobalScope、CoroutineScope、lifecycleScope、viewModelScope的区别与风险

协程作用域这东西,说白了就是协程的“生命周期容器”。你启动一个协程,总得知道它该跟谁同生共死吧?选错了作用域,轻则内存泄漏,重则应用崩溃。我踩过不少坑,今天咱们一个个说清楚。

核心结论:作用域决定了协程的生命周期边界。用错了,你的协程要么永生(泄漏),要么被意外杀死(崩溃)。

四种协程作用域生命周期对比 作用域 生命周期 风险等级 典型场景 GlobalScope 应用进程级别 永不自动取消 极高 后台常驻任务 CoroutineScope 手动管理 与Job绑定 中等 自定义生命周期 lifecycleScope Activity/Fragment onDestroy时取消 低 UI层协程 viewModelScope ViewModel级别 onCleared时取消 低 ViewModel绑定 ⚠️ 箭头表示生命周期范围缩小: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。它比协程更适合长时间运行的、可持久化的异步任务。