4. 常见泄漏场景:在Fragment中使用collect
说实话,Fragment 里的 Flow 订阅泄漏,我见得太多了。很多开发者觉得「不就是个 collect 嘛,有啥大不了的」,结果上线后 OOM 崩溃率直接飙升。这一章我带你看看那些最容易踩坑的场景,每个都是我亲手排查过的真实案例。
4.1 场景一:直接在 onCreateView 里 collect
这是我遇到最频繁的问题。你看这段代码:
class MyFragment : Fragment() {
override fun onCreateView(inflater, container, savedInstanceState): View? {
// 错误示范!
someFlow.collect { value ->
textView.text = value
}
return inflater.inflate(R.layout.fragment_my, container, false)
}
}
错在哪?onCreateView 执行的时候,Fragment 的视图还没完全创建好,但 collect 却启动了一个挂起协程。这个协程属于 Fragment 的 lifecycleScope,可问题是——视图被销毁时,collect 并没有一起取消。你想想看,用户旋转屏幕,Fragment 重建,旧的 collect 还活着,新的又来了,double kill。
4.2 场景二:使用 GlobalScope 或自定义协程作用域
有些朋友觉得「简单点,用 GlobalScope 直接开干」——我每次看到这种代码血压就上来。GlobalScope 的生命周期跟整个应用一样长,Fragment 都销毁了它还在后台默默 collect,Activity 引用被死死拽着,内存泄漏几乎是必然的。
// 错误!千万不要这样写
GlobalScope.launch {
someFlow.collect { value ->
updateUI(value)
}
}
还有种更隐蔽的:在 Fragment 里自己创建了 CoroutineScope,比如:
class MyFragment : Fragment() {
private val myScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun startObserving() {
myScope.launch {
someFlow.collect { ... }
}
}
}
这种scope如果不手动调用 myScope.cancel(),它就会一直活到 Fragment 被 GC 回收。但 Fragment 有可能因为被引用而无法回收——死锁了属于是。
Lifecycle.repeatOnLifecycle。我个人习惯统一用 viewLifecycleOwner.lifecycleScope,省心。
4.3 场景三:错误地使用了 LifecycleOwner
Fragment 有两个 LifecycleOwner:
- this(即 Fragment 本身)——生命周期与 Fragment 整体一致,包括视图销毁后仍存活。
- viewLifecycleOwner——生命周期与 Fragment 的 View 绑定,视图销毁即结束。
很多人习惯写成 lifecycleScope.launch { ... },但其实在 ViewPager 或嵌套 Fragment 场景下,Fragment 可能因为被缓存而保持活动,但它的视图已经被移除了。这时候 collect 还在跑,UI 更新的 lambda 里持有了旧的视图引用——嗯,泄漏也就来了。
// 错误:使用了 Fragment 本身的 lifecycleScope
lifecycleScope.launch {
someFlow.collect { value ->
// 这里持有 textView,但 textView 可能已经被销毁
textView.text = value
}
}
正确做法是绑定到 viewLifecycleOwner:
// 正确:视图销毁时自动取消
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
someFlow.collect { value ->
textView.text = value
}
}
}
核心原则: 只要你的 collect 里访问了视图(TextView、ImageView、RecyclerView 等),就必须用 viewLifecycleOwner。这是 Fragment 内存泄漏的第一道防线。
4.4 场景四:嵌套 Fragment 与 ViewPager 中的陷阱
ViewPager 会预加载相邻 Fragment,但它们的视图可能已经被销毁。这时候如果你在 child Fragment 里用 Fragment 自己的 lifecycleScope 做 collect,预加载的 Fragment 即使视图已被回收,collect 却还在运行。
我曾经排查过一个 case:用户快速滑动 ViewPager,导致十几个 Fragment 的 collect 都活着,每个都持有 Activity 引用(因为 Fragment 内部访问了 requireActivity()),内存直接暴涨 40MB。解决方法很简单:统一改成 viewLifecycleOwner.lifecycleScope + repeatOnLifecycle,并且确保 collect 块里只访问 viewLifecycleOwner 相关的视图。
4.5 场景五:Flow 的 shareIn / stateIn 配置不当
相信很多人在用 Flow.stateIn 或 shareIn 把冷流转热流,但忘了指定正确的 started 策略。比如:
val hotFlow = someColdFlow
.stateIn(
scope = viewLifecycleOwner.lifecycleScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
initialValue = null
)
这里 stopTimeoutMillis = 5000 表示订阅者全部取消后,上游还会保持 5 秒。如果你的 Fragment 被快速重建(比如屏幕旋转),5 秒内旧 Fragment 的 collect 已经取消了,但新 Fragment 的 collect 又来了——上游不会被取消,看起来没问题。但如果是 SharingStarted.Eagerly 或 Lazily 配合错误的 scope,那就可能造成永久活动。
// 高危:scope 用了 Fragment 本身的 lifecycleScope,又配了 Eagerly
val badFlow = someColdFlow
.stateIn(
scope = lifecycleScope, // Fragment 的 scope
started = SharingStarted.Eagerly, // 立即启动,永不停止
initialValue = null
)
这个 badFlow 会在 Fragment 创建时启动,然后永远运行下去,直到 Fragment 被彻底销毁。但即使 Fragment 没了,如果这个 flow 还被其他地方持有引用……那就成了僵尸。
泄漏场景总结
我把这些场景画成了一张图,方便你对照排查:
快速自检清单
你在排查泄漏时,可以拿着这张表对照着看:
| 排查点 | 正确做法 | 常见错误 |
|---|---|---|
| 协程作用域 | viewLifecycleOwner.lifecycleScope | lifecycleScope 或 GlobalScope |
| 生命周期绑定 | repeatOnLifecycle(STARTED) | 直接在 lifecycleScope 中 launch |
| 共享 Flow 配置 | WhileSubscribed + 合理超时 | Eagerly 或 Lazily 配合错误 scope |
| 嵌套 Fragment | 每个子 Fragment 独立绑定 viewLifecycleOwner | 共用父 Fragment 的 scope |
1. scope 是不是 viewLifecycleOwner 的?
2. 有没有用 repeatOnLifecycle ?
3. collect lambda 里有没有访问外部视图或 Activity 引用?
这三条过关,基本不会出泄漏问题。