20、Flow与生命周期:LifecycleOwner的Flow扩展、自动取消收集、避免内存泄漏
说实话,Flow 本身是个好东西,但如果你在 Activity 或 Fragment 里直接收集它,很容易踩坑。我见过不少项目,Flow 收集了但没在页面销毁时取消,结果协程还在跑,视图已经没了——轻则报空指针,重则内存泄漏。
这一章我们就来聊聊,怎么让 Flow 跟 Android 生命周期好好相处。
为什么需要生命周期感知?
你想想看,一个 Activity 走到 onDestroy 了,它里面的 TextView 已经不可见了。但如果你之前用 viewModelScope.launch 或者 lifecycleScope.launch 收集了一个 Flow,并且没有手动取消,那这个协程会继续运行。它可能还在做网络请求、数据库查询,甚至尝试更新 UI。
嗯,这就是问题所在。协程本身是轻量的,但它的回调里如果持有 View 引用,那这个 View 就无法被 GC 回收。久而久之,内存就涨上去了。
lifecycleScope 就万事大吉了。它确实会在 onDestroy 时取消协程,但前提是你用的是 launch 而不是 launchWhenXxx。后者有它自己的行为逻辑,后面会讲。
LifecycleOwner 的 Flow 扩展
Google 在 lifecycle-runtime-ktx 库里给我们提供了几个非常实用的扩展函数。我个人习惯把它们分成两类:
- 自动取消类:
flowWithLifecycle() - 生命周期状态绑定类:
launchWhenCreated()、launchWhenStarted()、launchWhenResumed()
先看一个最简单的例子:
// 在 Fragment 或 Activity 中
lifecycleScope.launch {
someFlow.collect { value ->
// 更新 UI
textView.text = value
}
}
这段代码有什么问题?它会在 Fragment 销毁后继续运行。正确的做法是:
lifecycleScope.launch {
someFlow
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { value ->
textView.text = value
}
}
flowWithLifecycle() 的作用是:当生命周期低于指定状态时,暂停 Flow 的收集;当回到该状态时,恢复收集。说白了,它帮你做了「自动暂停和恢复」这件事。
flowWithLifecycle() 不会取消协程,它只是暂停了 Flow 的发射。协程本身还在运行,只是 collect 里的代码不执行了。
自动取消收集:repeatOnLifecycle
我在项目中遇到过一个问题:用 flowWithLifecycle() 虽然能暂停收集,但如果页面被销毁了,协程本身还是活的。有没有办法让协程也自动取消?
有,就是 repeatOnLifecycle()。
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
someFlow.collect { value ->
textView.text = value
}
}
}
这段代码的行为是:
- 当生命周期到达 STARTED 时,启动一个新的协程来收集 Flow
- 当生命周期低于 STARTED 时,取消这个协程
- 当再次回到 STARTED 时,重新启动收集
你想想看,这跟 flowWithLifecycle() 有什么区别?区别在于:repeatOnLifecycle() 是真的把协程取消了,而不仅仅是暂停 Flow。这意味着协程里的所有资源(比如网络请求的挂起函数)都会被释放。
repeatOnLifecycle() 更安全。如果你在收集一个热 Flow(比如 StateFlow),用 flowWithLifecycle() 就够了,因为热 Flow 不依赖协程的连续性。
避免内存泄漏的最佳实践
我曾经在一个项目里看到这样的代码:
// ❌ 错误示例
lifecycleScope.launchWhenStarted {
viewModel.uiState.collect { state ->
binding.textView.text = state.text
}
}
这段代码的问题在于:launchWhenStarted() 只是暂停了协程的执行,并没有取消它。如果页面被销毁了,协程仍然持有 binding 的引用,导致内存泄漏。
正确的做法是:
// ✅ 正确示例
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
binding.textView.text = state.text
}
}
}
或者更简洁的写法:
// ✅ 使用扩展函数
lifecycleScope.launch {
viewModel.uiState
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { state ->
binding.textView.text = state.text
}
}
这两种写法都能保证:页面不可见时,不会更新 UI;页面销毁时,不会泄漏内存。
三种方式的对比
| 方式 | 协程是否取消 | 生命周期状态 | 适用场景 |
|---|---|---|---|
launchWhenStarted() |
否(仅暂停) | STARTED | 不推荐使用,容易泄漏 |
flowWithLifecycle() |
否(仅暂停 Flow) | 可指定 | 热 Flow(StateFlow、SharedFlow) |
repeatOnLifecycle() |
是 | 可指定 | 冷 Flow、需要彻底释放资源的场景 |
SVG 流程图:生命周期与 Flow 的交互
实际项目中的选择
说了这么多,到底该用哪个?我分享一下我的经验:
- 如果你在 ViewModel 里暴露的是 StateFlow,用
flowWithLifecycle()。因为 StateFlow 是热 Flow,它不依赖协程的连续性,暂停后恢复不会丢失最新值。 - 如果你在收集 Room 的 Flow 或者网络请求的 Flow,用
repeatOnLifecycle()。因为这些是冷 Flow,每次收集都会重新执行,取消后重新启动是合理的。 - 如果你不确定,就用
repeatOnLifecycle()。它更安全,虽然多了一点协程创建的开销,但这点开销可以忽略不计。
lifecycleScope.launchWhenResumed { } 来收集 Flow,结果页面切到后台再回来,收集了两次。因为 launchWhenResumed 只是暂停,不会取消之前的协程。如果你在 collect 里做了副作用操作(比如写数据库),就会重复执行。
总结
生命周期管理是 Android 开发里绕不开的话题。Flow 本身不感知生命周期,但通过 LifecycleOwner 提供的扩展函数,我们可以轻松地让 Flow 的收集与页面同生共死。
记住一句话:页面不可见时,不要更新 UI;页面销毁时,不要持有引用。 做到这两点,内存泄漏就离你远去了。
公众号:蓝海资料掘金营,微信deep3321