第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 订阅泄漏的根源:

协程作用域与结构化并发关系图 协程任务 GlobalScope ViewModelScope lifecycleScope 应用进程 ViewModel onCleared Activity onDestroy 说明: • GlobalScope 绑定应用进程,生命周期最长,最容易泄漏 • ViewModelScope 绑定 ViewModel 清除时机,适合数据层异步 • lifecycleScope 绑定 Activity/Fragment 生命周期,适合 UI 相关任务

图中很直观:最上层是协程任务,它必须在某个作用域下启动。不同作用域绑定不同的生命周期节点,生命周期一旦结束,协程就被强制取消。结构化并发的「取消传递」机制,保证了这个链条的可靠性。