协程作用域与生命周期

协程泄漏这问题,说来也怪——明明写的时候感觉挺好的,一跑几个小时,内存蹭蹭往上飙。我曾经为一个线上泄漏排查了一周,最后发现就是一个没有正确绑定的作用域。所以这章咱们重点聊聊协程的作用域和生命周期,说白了就是搞清楚「协程该什么时候活,什么时候死」。

什么是协程作用域

协程作用域(CoroutineScope)定义了协程的生存范围。你可以把它理解成一个「容器」。容器里的协程,要么自己执行完,要么被容器取消掉。容器没了,里面的协程必须全部终止。

核心原则: 协程的生命周期不能超过它的作用域。一旦作用域被销毁,内部所有协程都应该自动取消。

以前我遇到一个项目,Activity销毁之后还发着网络请求,结果回调里要更新UI,直接NullPointerException。这就是典型的作用域没管好。

Kotlin 内置的几种作用域

作用域类型 生命周期 典型使用场景 泄漏风险
GlobalScope 应用进程级别 极少用,后台守护任务 ⚠️ 高,极易泄漏
lifecycleScope Activity/Fragment生命周期 UI层协程 低(自动取消)
viewModelScope ViewModel生命周期 业务逻辑、数据加载 低(自动取消)
自定义CoroutineScope 手动控制 Service、自定义组件 视绑定情况而定
MainScope 手动绑定到主线程 旧版、View相关 需要手动取消

为什么 GlobalScope 是个陷阱

GlobalScope 的生命周期跟应用一样长。你想想看,如果在一个Activity里启动了GlobalScope的协程,Activity退出了,协程还在跑。它持有Activity的引用吗?不一定。但它可能持有Activity内部的回调、View引用、或者其他资源。这就产生了泄漏。

避坑指南: 我曾经在一个项目里看到同事用GlobalScope去封装网络请求工具类,结果每调用一次请求就泄漏一个Activity。后来全部改成传给调用者的作用域,泄漏问题当场解决。

所以,除非你是真的需要一个「不死」的后台任务(比如心跳检测),否则别碰GlobalScope。

lifecycleScope 和 viewModelScope 的正确姿势

这两个是Jetpack官方提供的「开箱即用」方案。它们已经绑定了对应的生命周期。

  • lifecycleScope:当Activity进入DESTROYED状态时,自动取消所有协程。
  • viewModelScope:当ViewModel的onCleared()被调用时,自动取消。

用它们的时候,你基本不用担心泄漏问题。但有个小坑——如果你在Fragment的onCreateView里面用lifecycleScope启动了一个协程,而Fragment视图被销毁但Fragment本身还活着(比如添加到返回栈),这时候协程并不会取消。为什么?因为lifecycleScope绑定的是LifecycleOwner的生命周期,而Fragment的生命周期包括onDestroyView和onDestroy两个阶段。视图销毁不代表整个Fragment销毁。

我的习惯: 如果协程只跟视图相关(比如更新UI),我会用Lifecycle.State.STARTED做限制:lifecycleScope.whenStarted { ... }。这样视图不可见时协程就暂停了。

自定义作用域:手动控制生命周期

当内置作用域不够用的时候,你就得自己创建了。比如我们封装一个网络请求管理器,它需要在整个App运行期间管理一些长连接协程,但又不希望它们像GlobalScope那样完全不受控。

class NetworkManager {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    
    fun connect() {
        scope.launch {
            // 长连接处理
        }
    }
    
    fun onDestroy() {
        scope.cancel() // 手动取消
    }
}

重点在于,你要确保onDestroy()被正确调用。我见过有人忘了调用,结果协程一直挂在后台。嗯,这又是一个泄漏源头。

生命周期绑定与泄漏检测

协程泄漏的核心检查点就是:作用域是否在恰当的时机被取消。我通常用三个方法做检验:

  1. 看调用栈:协程被launch时,它所在的协程作用域是哪个?
  2. 看取消回调:invokeOnCompletion里打印日志,看看协程是什么时候结束的。
  3. 用LeakCanary:加上协程的泄漏检测库,直接告诉你哪个协程挂了没取消。

泄漏检测小技巧: 在协程里加入协程名,配合Android Profiler的线程视图,能很直观地看到残余协程。

流程图:协程作用域与生命周期关系

协程作用域与生命周期关系图 应用进程生命周期 GlobalScope 与进程同生同死 Activity / Fragment 生命周期 lifecycleScope 销毁时自动取消 viewModelScope ViewModel销毁时取消 自定义作用域 需手动调用 cancel() 泄漏高风险地带 GlobalScope + 持有外部引用 自定义作用域忘记 cancel() lifecycleScope 在 onDestroyView 后未暂停 最佳实践 优先使用 lifecycleScope / viewModelScope;自定义作用域务必在 onDestroy 中 cancel()

这张图其实挺直观的。左边两个作用域(GlobalScope和自定义)很容易漏取消,右边两个(lifecycleScope、viewModelScope)只要用了正确的生命周期绑定,基本上不出问题。但你看那个虚线箭头——lifecycleScope也有泄漏风险,只不过场景更隐蔽。

总结一下我的实战经验

  • 能不用GlobalScope就不用——我参与的所有项目中,最后都用自定义作用域替代了它。
  • 自定义作用域必须绑定合理的生命周期接口。比如挂到Service的onDestroy、或者用LifecycleObserver监听。
  • 小心函数式作用域:比如withContext、coroutineScope这些是挂起函数里面的临时作用域,不会泄漏,但如果你在GlobalScope.launch里用它们,那外层的锅还是得外层背。
  • 检查协程是否真的被取消了:我会在finally块里打日志,或者用isActive判断一下。

避坑指南: 我曾经在自定义View里直接CoroutineScope(Dispatchers.Main).launch { ... },结果View被移除后协程还在跑,因为没人取消它。后来我改成用LifecycleOwner的方式,或者干脆用LifecycleCoroutineScope,问题就解决了。