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。这意味着协程里的所有资源(比如网络请求的挂起函数)都会被释放。

🎯 我的建议: 如果你在收集一个冷 Flow(比如从 Room 或网络来的),用 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 的交互

Flow 与生命周期交互流程 Activity/Fragment onCreate → onStart → onResume onPause → onStop → onDestroy Flow 收集 collect { 更新UI } 暂停 / 取消收集 生命周期到达 生命周期离开 结果 UI 正常更新 ✅ 无内存泄漏 ✅ 🔑 关键点: • repeatOnLifecycle() 会取消协程,适合冷 Flow • flowWithLifecycle() 只暂停 Flow,适合热 Flow • 不要用 launchWhenXxx() 收集 Flow,容易泄漏

实际项目中的选择

说了这么多,到底该用哪个?我分享一下我的经验:

  • 如果你在 ViewModel 里暴露的是 StateFlow,用 flowWithLifecycle()。因为 StateFlow 是热 Flow,它不依赖协程的连续性,暂停后恢复不会丢失最新值。
  • 如果你在收集 Room 的 Flow 或者网络请求的 Flow,用 repeatOnLifecycle()。因为这些是冷 Flow,每次收集都会重新执行,取消后重新启动是合理的。
  • 如果你不确定,就用 repeatOnLifecycle()。它更安全,虽然多了一点协程创建的开销,但这点开销可以忽略不计。
⚠️ 避坑指南: 我曾经在一个项目里看到有人用 lifecycleScope.launchWhenResumed { } 来收集 Flow,结果页面切到后台再回来,收集了两次。因为 launchWhenResumed 只是暂停,不会取消之前的协程。如果你在 collect 里做了副作用操作(比如写数据库),就会重复执行。

总结

生命周期管理是 Android 开发里绕不开的话题。Flow 本身不感知生命周期,但通过 LifecycleOwner 提供的扩展函数,我们可以轻松地让 Flow 的收集与页面同生共死。

记住一句话:页面不可见时,不要更新 UI;页面销毁时,不要持有引用。 做到这两点,内存泄漏就离你远去了。


公众号:蓝海资料掘金营,微信deep3321