Flow 基础概念与生命周期归纳总结

说实话,我刚开始接触 Kotlin Flow 的时候,第一反应是:「这不就是 RxJava 换了个马甲吗?」后来踩了几个坑才明白,Flow 在设计上比 RxJava 更懂 Android 开发者的痛点——尤其是生命周期管理。

这一章我们先打基础。把 Flow 到底是什么、它的生命周期长什么样说清楚。后面所有泄漏排查的技巧,都基于这些基础概念。

Flow 是什么?

Flow 是 Kotlin 协程库提供的一种冷数据流(Cold Stream)。说白了,它就像一根水管:有人打开水龙头(收集)时,水才开始流;没人接水,水管就是空的,不会浪费一滴水。

我个人习惯把 Flow 理解成一个「带协程的生产者-消费者模型」:

  • 生产者:emit 发出数据
  • 消费者:collect 收集数据
  • 协程作用域:决定了这条水管什么时候该关

我在项目中遇到过有人把 Flow 当成 LiveData 的替代品,直接往 globalScope 里塞。结果页面退出了,后台还在 emit,最终内存泄漏。切记:Flow 不是 LiveData,它没有自带的生命周期感知能力。

Flow 的两种基本类型

类型特点典型场景
冷流(Cold Flow) 每个 collect 都独立执行 producer,无共享 网络请求、数据库查一次
热流(Hot Flow) 多个 collect 共享生产者,独立消费 状态管理、事件总线

为什么这个区分对 Fragment 里的泄漏排查很重要?你想想看:热流如果被多个 Fragment 实例共享,而其中一个 Fragment 销毁时没取消收集,整个热流的生产者还会继续往这个已销毁的 Fragment 里传数据——这不就泄漏了么?

Flow 的生命周期三要素

要避免泄漏,首先得搞清楚 Flow 的生命周期到底受什么控制。我把它总结为三个要素:

  1. 协程作用域(CoroutineScope)——Flow 的收集必须跑在一个作用域里,作用域取消,收集就停止。
  2. 终端操作(Terminal Operator)——比如 collect、toList、first,这些触发了实际的订阅。
  3. 协程上下文(CoroutineContext)——Dispatchers 决定了生产者跑在哪个线程,但生命周期本身不受线程影响。

核心公式:Flow 的生命周期 = 收集它的协程作用域的生命周期

换句话说:你在哪个 scope 里 collect,Flow 就在哪个 scope 里活。

Fragment 里常见的 Flow 订阅模式

我排查过的泄漏案例里,90% 都源自下面这种「无意识」的订阅:

class MyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // ❌ 典型错误:使用 viewLifecycleOwner.lifecycleScope 却忘了管理
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.someFlow.collect { value ->
                textView.text = value
            }
        }
    }
}

这段代码的问题在哪?lifecycleScope 确实会在 Fragment 的 onDestroy 时取消,但如果你用的是 Lifecycle.State.STARTED 以上的收集(比如 repeatOnLifecycle),并且 Flow 是热流,就有可能出问题。

我建议:在 Fragment 里订阅 Flow,永远使用 repeatOnLifecycle。这是官方推荐的写法:

class MyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // ✅ 正确:生命周期感知,自动重启
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.someFlow.collect { value ->
                    textView.text = value
                }
            }
        }
    }
}

⚠️ 注意:我曾经见过团队把 repeatOnLifecycle 和 Flow.flowWithLifecycle 混用导致重复订阅。简单记:repeatOnLifecycle 作用于收集的 外部协程作用域,flowWithLifecycle 是给 Flow 本身加生命周期限定符。建议只用一个模式,全团队统一。

Flow 生命周期与 Fragment 生命周期的映射

把两者放在一起看,就一目了然了:

Flow 生命周期与 Fragment 生命周期的映射 Fragment 状态 onCreate → onStart onResume(可见) onPause(不可见) onStop → onDestroy Flow 收集状态 flow.collect 开始 正常收集更新 UI flowWithLifecycle 暂停 协程取消,Flow 停止 说明 • Fragment onStart 时开始收集 • 可见期间正常接收数据 • onPause 后 flowWithLifecycle 会暂停收集(不取消Flow) • onStop 或者 onDestroy 协程 scope 取消,Flow 停止 • 泄漏常发生在:未正确处理 onDestroy 后的协程取消 ⚠️ 未用 repeatOnLifecycle 时可能泄漏

你需要在 Fragment 里记住的生命周期关键点

  • onCreate → onDestroy:这是 Fragment 实例的生命周期。在这期间创建或绑定的协程,必须在 onDestroy 时取消,否则引用泄露。
  • onStart → onStop:这是与用户交互的生命周期。Flow 收集最好只在这段时间里活跃,因为 UI 更新只有在可见时才有意义。
  • onResume → onPause:如果 ui 对实时性要求高(比如倒计时),才需要在 resumed 时收集。

我个人习惯在项目里定一条铁律:所有 Fragment 里的 Flow 订阅,必须绑定到 viewLifecycleOwner.lifecycleScope 并且使用 repeatOnLifecycle(STARTED)。这样即使有热流、有共享,至少不会因为 Fragment 销毁而泄漏。

总结

Flow 是冷的,但它的收集行为是由协程作用域决定的。Fragment 的生命周期天然会销毁协程作用域,关键是你要正确地绑定它们。