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。

警告: 直接在 onCreateView/onViewCreated 中调用 collect 而不指定取消时机,collect 会一直运行直到协程作用域被取消。如果作用域是 Fragment 的 lifecycleScope,那它会在 Fragment 真正 destroy 时才能取消,而视图的 destroy 早得多。这就导致了视图持有者泄漏。

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 有可能因为被引用而无法回收——死锁了属于是。

建议: 要么用 viewLifecycleOwner.lifecycleScope,要么用 lifecycleScope 但一定要结合 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 还被其他地方持有引用……那就成了僵尸。

泄漏场景总结

我把这些场景画成了一张图,方便你对照排查:

Fragment Flow订阅泄漏常见场景 Fragment Flow 订阅泄漏路径 Fragment 生命周期 使用 viewLifecycleOwner + repeatOnLifecycle 安全:视图销毁自动取消 错误订阅(常见场景) 具体泄漏场景 • 在 onCreateView 中直接 collect • 使用 GlobalScope 或自定义作用域 • 错误使用 Fragment 自身的 lifecycleScope • shareIn/stateIn 配置不当(Eagerly) 内存泄漏! 排查要点 1. 检查 collect 所在的生命周期作用域 2. 检查是否有视图引用在闭包中

快速自检清单

你在排查泄漏时,可以拿着这张表对照着看:

排查点 正确做法 常见错误
协程作用域 viewLifecycleOwner.lifecycleScope lifecycleScope 或 GlobalScope
生命周期绑定 repeatOnLifecycle(STARTED) 直接在 lifecycleScope 中 launch
共享 Flow 配置 WhileSubscribed + 合理超时 Eagerly 或 Lazily 配合错误 scope
嵌套 Fragment 每个子 Fragment 独立绑定 viewLifecycleOwner 共用父 Fragment 的 scope
我的个人习惯: 只要在 Fragment 里写 collect,我第一时间就会确认三件事——
1. scope 是不是 viewLifecycleOwner 的?
2. 有没有用 repeatOnLifecycle ?
3. collect lambda 里有没有访问外部视图或 Activity 引用?
这三条过关,基本不会出泄漏问题。