协程作用域与生命周期:GlobalScope、CoroutineScope、SupervisorJob 的区别

说实话,绝大多数协程泄漏,根子都出在「作用域没管好」上。你想想看,一个协程跑起来了,但它所属的作用域——也就是它的「生命周期容器」——如果比它期望的活得更久,或者根本没被正确关闭,那么这个协程就会像断了线的风筝,永远飘在后台。我最早在线上排查泄漏问题时,十次有八次都绕不开这几个关键词:GlobalScope、CoroutineScope、SupervisorJob。

搞清楚它们三者的区别,说白了就是搞懂「协程的出生证明」和「死亡条件」。咱们一个一个来。

1. GlobalScope —— 全局的「野猫」

GlobalScope 是 Kotlin 官方提供的一个顶层协程作用域。它的生命周期和应用进程一样长——只要进程不挂,它就能一直跑。听起来很方便是吧?但这就是万恶之源。

⚠️ 核心危险:GlobalScope 启动的协程,不会因为你 onDestroy() 或者 onStop() 就自动取消。你在 Activity 里用 GlobalScope 启动一个网络请求,界面已经关了,那个请求依然在跑,甚至可能更新一个已经不存在的 UI → 直接闪退。

我曾在项目中遇到过这个坑:一个同事图省事,所有异步任务都用 GlobalScope.launch,结果用户快速退出重进,后台攒了十几个协程同时执行,最后 OOM 了。排查时候看堆栈全指向同一个 GlobalScope 启动的代码块,头都大了。

所以我的建议是:生产代码里尽量不要出现 GlobalScope。除非你真的需要「进程级单例任务」——比如一些极其稳定的后台监控,但这种情况我也会用自定义的 ApplicationScope。

2. CoroutineScope —— 你的「私家作用域」

正确的做法是创建自己的 CoroutineScope 实例,并绑定到组件的生命周期上。你可以把它理解成一个「容器」,容器销毁时,里面所有协程都自动取消。

class MyActivity : AppCompatActivity() {
    // 绑定 Activity 生命周期
    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

    override fun onDestroy() {
        super.onDestroy()
        scope.cancel()  // 取消所有子协程
    }

    fun fetchData() {
        scope.launch {
            // 网络请求
        }
    }
}

注意这里我传了一个 SupervisorJob()。为什么?因为默认是 Job(),如果一个子协程抛异常,整个作用域下所有协程都会被取消。这在很多业务场景下不是你想要的。

🚀 经验之谈:我个人习惯把 scope 声明为 lateinit 或者在 onCreate 里初始化,然后在 onDestroy 里取消。对于 Fragment,就绑定到 viewLifecycleOwner 上,用 Lifecycle 自带的 scope。

3. SupervisorJob —— 容错的「安全阀门」

SupervisorJob 和普通 Job 最大的区别在于:子协程的失败不会向上传播。如果兄弟协程 A 崩了,兄弟协程 B 不会被牵连取消。

val supervisor = SupervisorJob()
val scope = CoroutineScope(supervisor + Dispatchers.IO)

scope.launch {
    delay(100)
    throw RuntimeException("子任务1挂了")
}

scope.launch {
    delay(200)
    println("我还能正常执行")  // 这条会打印,因为 SuperVisorJob 保护了它
}

// 但注意:supervisor 本身如果取消了,所有子协程还是会取消。

我建议你在大多数 UI 层或者业务层使用 `SupervisorJob`——你想想看,如果一个网络请求超时,你肯定不希望另一个同步刷新的任务也被一起取消。当然,如果你要的是「任意一个失败就全部回滚」的原子性场景,那用普通 Job 更合适。

三者对比表

特性 GlobalScope 自定义 CoroutineScope SupervisorJob(配合 scope)
生命周期 应用进程 自己管理(手动 cancel) 同 scope,但子异常不传染
泄漏风险 极高 低(正确取消时) 低,注意父 Job 取消会传染
子协程异常默认行为 取消兄弟 取消兄弟(普通 Job) 不取消兄弟
推荐使用场景 极少,仅极稳定后台 所有组件事务 并行独立任务

一张图理清作用域关系

下面这张图概括了协程作用域的层次、生命周期绑定以及泄漏风险点:

协程作用域与生命周期关系图 GlobalScope(应用进程级) 生命周期 = 应用启动到结束,极易泄漏 自定义 CoroutineScope(绑组件生命周期) 例如:ActivityScope / FragmentScope / ViewModelScope Job(默认) 子协程失败 → 所有协程取消 原子性场景使用 SupervisorJob 子协程失败 → 不传染兄弟 独立并行任务场景 子协程 1: 网络请求 子协程 2: 本地缓存 说明:GlobalScope 是全局根,自定义 Scope 在其下绑定组件生命周期;选择 Job 或 SupervisorJob 决定异常传播策略。

避坑指南

  • 我曾经在 Fragment 里用 lifecycleScope(Android 官方),觉得万无一失。结果在 onViewCreated 里协程还没跑完,Fragment 就已经 onDestroyView 了,但 lifecycleScope 默认绑定的是 LifecycleOwner 整个生命周期,如果 Activity 还活着,协程会继续跑——所以后来我改用 viewLifecycleOwner.lifecycleScope 才真正绑定到视图生命周期上。
  • 记住了:SupervisorJob 并不能防止父 Job 取消导致的连锁反应。如果你手动调用了 scope.cancel(),就算你用的是 SupervisorJob,所有子协程依然会全部取消。它只管「子异常不传染」。
  • 一个小技巧:在单元测试里,可以用 TestCoroutineScope 替换自定义作用域,方便控制时间。我一般会在测试工程里写一个扩展函数,快速创建绑定测试生命周期的 scope。
💡 日常推荐组合:
UI 层 → viewModelScope(ViewModel 自带) 或 lifecycleScope + SupervisorJob;
非 UI 的业务层 → 自定义 CoroutineScope(SupervisorJob() + Dispatchers.Default) 并暴露 cancel() 方法;
Data 层 → 普通 Job 配合 withContext 即可,不轻易创建外部 scope。