Fragment生命周期与协程作用域
很多同学找我排查内存泄漏时,第一句话就是:“我在 Fragment 里 collect 了一个 Flow,然后回不来了”。
其实大部分情况,问题都出在「生命周期」和「协程作用域」没对齐。今天我就把这对冤家的关系彻底理清楚。
Fragment 生命周期的特殊之处
Fragment 的生命周期比 Activity 复杂得多。Activity 只有前后台切换和销毁重建,而 Fragment 多了 onCreateView / onDestroyView、onAttach / onDetach 等额外状态。
我强调一个关键点:视图层销毁不代表 Fragment 实例被销毁。比如用户按下返回键时,onDestroyView 会先调用,但 onDestroy 可能还没跑。如果你在 onCreateView 里启动了协程,且作用域绑定的是 viewLifecycleOwner,那视图销毁时协程就会自动取消。可如果你绑定的是 this(即 Fragment 本身),协程会一直活到 Fragment 彻底销毁——这中间就埋下了泄漏的种子。
| 生命周期事件 | 视图是否存活 | Fragment 实例存活 | 典型泄漏场景 |
|---|---|---|---|
onCreateView → onViewCreated |
✅ 有 | ✅ | 无 |
onDestroyView 之后 |
❌ 无 | ✅ | 持有 View 引用、协程未取消 |
onDestroy 之后 |
❌ | ❌ 变为 destroyed | 协程作用域未与 Fragment 绑定 |
viewLifecycleOwner.lifecycleScope。别用 Fragment 本身的 lifecycleScope——它和 Fragment 实例共存亡,跨越了视图范围。
协程作用域的三种绑定方式
Kotlin 协程在 Fragment 里通常有三种启动方式。我按照「推荐程度」排了序:
- viewLifecycleOwner.lifecycleScope —— 最安全,视图销毁即取消。
- lifecycleScope —— 绑定 Fragment 本身,视图销毁不取消,需手动管理。
- GlobalScope / 自定义 CoroutineScope —— 极易泄漏,不推荐。
我曾经在一个电商项目里接手了一个崩溃单:用户反复切换 Tab,每个 Fragment 里的网络请求堆积起来,最后 OOM。查了半天,发现所有协程用的都是 GlobalScope.launch。嗯,这种写法比 `lifecycleScope` 更危险,因为它根本不会随任何组件销毁而取消。
onDestroyView 里打一个日志,打印 coroutineContext[Job]?.isActive。看到 true 且 Fragment 视图已销毁,那就要警惕了。
Flow 订阅时的常见错误
Flow 本身是冷流,但一旦你用 .collect 或 .launchIn 启动它,就会变成「活跃的协程任务」。下面这种写法,十有八九会引发泄漏:
// ❌ 错误示例
class MyFragment : Fragment() {
private val viewModel: MyViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 泄漏根源:协程绑定在 lifecycleScope,但 collect 操作未被主动取消
lifecycleScope.launch {
viewModel.someFlow.collect { data ->
// 更新 UI —— 这里引用了 view
textView.text = data.toString()
}
}
}
}
为什么泄漏?因为 lifecycleScope 是 Fragment 实例级别的。当用户离开页面,视图被销毁,textView 被置为 null,但协程里还在持有 textView 的引用(lambda 捕获了外部变量)。虽然协程在 onDestroy 时会被取消,但如果 Fragment 被加回回退栈而没走到 onDestroy,那么这个协程会一直运行,导致内存泄漏。
正确做法是使用 viewLifecycleOwner.lifecycleScope,并且最好配合 repeatOnLifecycle:
// ✅ 正确示例
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
// 当视图处于至少 STARTED 状态时才 collect
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.someFlow.collect { data ->
textView.text = data.toString()
}
}
}
}
repeatOnLifecycle 的好处很直观:当生命周期低于指定状态时自动取消 collect,重新回到该状态时再重新订阅。这正好解决了「回退栈回来后,Flow 订阅状态不一致」的问题。
flowWithLifecycle 操作符,也要确保它运行在正确的 LifecycleCoroutineScope 里。我见过有人把 flowWithLifecycle 直接写在 ViewModel 里,那是没用的——它绑定了错误的生命周期。
一张图理清关系
实战排查步骤
如果现在有一个泄漏的 Fragment,我建议按下面的顺序排查:
- 检查协程启动位置 —— 是在
onViewCreated里吗?还是onCreate?后者往往更危险。 - 确认使用的 scope —— 看是
lifecycleScope还是viewLifecycleOwner.lifecycleScope。肉眼看不出来的话,加断点走一遍。 - 查看 collect 内部是否有 View 引用 —— adapter、textView、甚至
requireView()都算。用 LeakCanary 验证最直接。 - 验证是否使用了 repeatOnLifecycle —— 没有的话,加上基本就解决问题了。
viewLifecycleOwner.lifecycleScope 但还是泄漏了。最后发现,是因为 ViewPager 在销毁子 Fragment 前先销毁了其视图,导致 viewLifecycleOwner 对象变成 detached 状态,协程虽然取消了但回调还在队列里。解决方案是配合 lifecycle.whenResumed 做二次保护。细节很多,遇到类似情况时可以往这个方向查。
小结
Fragment 的生命周期是 Android 里最复杂的组件之一,不当的协程作用域选择会直接导致内存泄漏。
记住一句话:Flow 订阅的「生」与「死」要对齐视图的「生」与「死」。
用 viewLifecycleOwner.lifecycleScope + repeatOnLifecycle 是经过行业验证的最佳实践。