协程作用域与生命周期
协程泄漏这问题,说来也怪——明明写的时候感觉挺好的,一跑几个小时,内存蹭蹭往上飙。我曾经为一个线上泄漏排查了一周,最后发现就是一个没有正确绑定的作用域。所以这章咱们重点聊聊协程的作用域和生命周期,说白了就是搞清楚「协程该什么时候活,什么时候死」。
什么是协程作用域
协程作用域(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()被正确调用。我见过有人忘了调用,结果协程一直挂在后台。嗯,这又是一个泄漏源头。
生命周期绑定与泄漏检测
协程泄漏的核心检查点就是:作用域是否在恰当的时机被取消。我通常用三个方法做检验:
- 看调用栈:协程被
launch时,它所在的协程作用域是哪个? - 看取消回调:
invokeOnCompletion里打印日志,看看协程是什么时候结束的。 - 用LeakCanary:加上协程的泄漏检测库,直接告诉你哪个协程挂了没取消。
泄漏检测小技巧: 在协程里加入协程名,配合Android Profiler的线程视图,能很直观地看到残余协程。
流程图:协程作用域与生命周期关系
这张图其实挺直观的。左边两个作用域(GlobalScope和自定义)很容易漏取消,右边两个(lifecycleScope、viewModelScope)只要用了正确的生命周期绑定,基本上不出问题。但你看那个虚线箭头——lifecycleScope也有泄漏风险,只不过场景更隐蔽。
总结一下我的实战经验
- 能不用GlobalScope就不用——我参与的所有项目中,最后都用自定义作用域替代了它。
- 自定义作用域必须绑定合理的生命周期接口。比如挂到Service的
onDestroy、或者用LifecycleObserver监听。 - 小心函数式作用域:比如
withContext、coroutineScope这些是挂起函数里面的临时作用域,不会泄漏,但如果你在GlobalScope.launch里用它们,那外层的锅还是得外层背。 - 检查协程是否真的被取消了:我会在
finally块里打日志,或者用isActive判断一下。
避坑指南: 我曾经在自定义View里直接CoroutineScope(Dispatchers.Main).launch { ... },结果View被移除后协程还在跑,因为没人取消它。后来我改成用LifecycleOwner的方式,或者干脆用LifecycleCoroutineScope,问题就解决了。