第1章 Kotlin协程基础:协程概念、GlobalScope与ViewModelScope、结构化并发
开始讲Flow订阅泄漏之前,咱们得先把地基打牢。协程这东西,我刚接触时也觉得玄乎——不就是个轻量线程吗?后来写多了才发现,真正吃透它的生命周期管理才是关键。这一章我会把协程的核心概念、作用域选择,以及结构化并发为什么能拯救你于火海,掰开了说清楚。
1.1 协程到底是什么?
协程说白了,就是一个可挂起的计算单元。它不像线程那样依赖操作系统调度,而是在用户态切换,所以开销极小。你写个网络请求,suspend 一下,等结果回来再接着跑——嗯,就是这个意思。
举个例子,以前用回调嵌套时代码能写成「圣诞树」:
api.fetchUser { user ->
api.fetchPosts(user.id) { posts ->
updateUI(posts)
}
}
换成协程呢?
suspend fun loadData() {
val user = api.fetchUser() // 挂起,不阻塞线程
val posts = api.fetchPosts(user.id)
updateUI(posts)
}
看着是不是清爽多了?刚才那段代码里,fetchUser 和 fetchPosts 都是挂起函数。挂起就是「暂停当前协程,但不阻塞底层线程」。线程该干嘛干嘛,等结果就绪再回来继续。
个人经验:我刚开始写协程时总在挂起函数里偷偷写 Thread.sleep()——那是线程的阻塞语法,会整条线程都卡住。正确的做法是用 delay(),它是挂起式的,不占线程。这一点很多人翻过车。
1.2 协程作用域:GlobalScope vs ViewModelScope
作用域决定了协程的「生死范围」。你想想看,如果协程没有明确的生命周期绑定,它可能会在 Activity 销毁后还在后台偷偷跑——这不就是泄漏吗?
来看看两个最常用的作用域:
| 作用域 | 生命周期 | 典型场景 | 泄漏风险 |
|---|---|---|---|
| GlobalScope | 整个应用进程 | 极少用,除非是全局性的一次性任务 | 极高 |
| ViewModelScope | 与 ViewModel 绑定,ViewModel 清除时自动取消 | ViewModel 内的异步操作 | 低 |
| lifecycleScope | 与 Activity/Fragment 生命周期绑定 | UI 相关的协程,例如 LiveData 发送 | 低 |
GlobalScope 为什么危险? 因为它的生命周期是 app 级别的。你在 Activity 里开启一个 GlobalScope 协程,Activity 销毁了,协程还在。如果协程持有 View 的引用,就形成泄漏。我以前在一个遗留项目里看到过这种写法:
GlobalScope.launch(Dispatchers.IO) {
// 做一些耗时操作,然后更新UI
withContext(Dispatchers.Main) {
textView.text = result // Activity 已销毁,但 textView 还在被引用
}
}
结果就是内存泄漏 + 潜在崩溃。虽说 GlobalScope 也有自己的适用场景,但在 Android 开发中,99% 的情况下应该用 ViewModelScope 或 lifecycleScope。
我曾经踩过一个坑:接手一个项目,全局搜索 GlobalScope.launch 找到了 23 处。其中一半都是直接在 Activity 或 Fragment 里启动的后台任务。重构时我花了一整天把它们改成 ViewModelScope,内存占用直接降了 40%。
1.3 结构化并发——避免泄漏的基石
结构化并发是 Kotlin 协程最核心的设计理念。说白了就是:每个协程都必须在一个明确的作用域内运行,作用域结束,所有子协程自动取消。
这会带来几个好处:
- 父子联动:父协程取消,子协程全取消。反过来,子协程异常也会传递到父协程(取决于策略)。
- 避免泄漏:生命周期结束,作用域取消,协程自然终止,不再持有任何引用。
- 错误处理统一:异常可以在作用域层面捕获,不用每个协程都写 try-catch。
看个对比:
// ❌ 非结构化:各协程各自为政,生命周期不受控
GlobalScope.launch {
delay(1000)
Log.d("TAG", "still running after activity destroyed")
}
// ✅ 结构化:lifecycleScope 与 Activity 绑定
lifecycleScope.launch {
delay(1000)
Log.d("TAG", "will be cancelled if activity destroyed")
}
结构化并发的另一层意思是「代码结构与执行结构一致」。你写出来的大括号嵌套就对应着协程的父子关系,不像回调那样执行路径乱飞。
关键点:协程的泄漏与普通线程泄漏本质一样——对象被长时间持有的引用。结构化并发通过作用域自动取消,从源头切断了这个引用链。
最后我用一张图来梳理这三者的关系,也方便后续章节理解 Flow 订阅泄漏的根源:
图中很直观:最上层是协程任务,它必须在某个作用域下启动。不同作用域绑定不同的生命周期节点,生命周期一旦结束,协程就被强制取消。结构化并发的「取消传递」机制,保证了这个链条的可靠性。