3、Flow 的发射与收集:emit() 与 collect() 的机制,背压问题初探

好,咱们今天来聊聊 Flow 最核心的两个操作——emit()collect()

说白了,Flow 就是个数据管道。一头往里面塞数据,另一头把数据拿出来处理。emit() 就是那个塞的动作,collect() 就是那个拿的动作。听起来很简单对吧?但这里面的门道,比你想象的要深。

3.1 emit():数据的发射端

emit() 是 Flow 的挂起函数,它负责把数据发射出去。我刚开始用的时候,总觉得它跟 RxJava 的 onNext() 差不多。但实际用起来,差别还挺大的。

来看个最基础的例子:

fun simpleFlow(): Flow<Int> = flow {
    println("Flow 开始发射数据")
    for (i in 1..3) {
        delay(100) // 模拟耗时操作
        emit(i)    // 发射数据
        println("发射了: $i")
    }
    println("Flow 发射完毕")
}

这里有个关键点——emit() 是挂起函数。这意味着什么呢?

嗯,它不会像 RxJava 那样一股脑把所有数据都扔出去。它会等下游处理完一个,再发射下一个。这就是 Flow 和 RxJava 在背压处理上的本质区别。

核心要点:emit() 是挂起函数,它会等待 collect() 消费完当前数据后,才继续发射下一个数据。这是 Flow 天然的背压处理机制。

3.2 collect():数据的收集端

collect() 是 Flow 的终端操作符,它订阅了 Flow 并开始接收数据。我习惯把它理解成「水龙头」——你拧开它,水才开始流。

看个完整的例子:

fun main() = runBlocking {
    val flow = simpleFlow()
    
    println("准备收集数据")
    flow.collect { value ->
        println("收集到: $value")
        delay(200) // 模拟处理耗时
    }
    println("收集完毕")
}

运行结果会是什么?你想想看:

准备收集数据
Flow 开始发射数据
发射了: 1
收集到: 1
发射了: 2
收集到: 2
发射了: 3
收集到: 3
Flow 发射完毕
收集完毕

注意看顺序。发射一个,收集一个,再发射下一个。这就是 Flow 的「冷流」特性——数据是按需生产的。

个人经验:我在项目中遇到过一个问题,用 RxJava 做网络请求时,如果下游处理慢,上游会一直往内存里塞数据,最后 OOM 了。换成 Flow 后,这个问题自然就解决了。因为 emit() 会等 collect() 处理完。

3.3 背压问题初探

背压,说白了就是「生产者太快,消费者太慢」的问题。

在 RxJava 里,背压是个大麻烦。你需要用 FlowableBackpressureStrategy 这些策略来处理。稍有不慎,就会抛出 MissingBackpressureException

但在 Flow 里,情况简单得多。

Flow 的背压策略是——天然支持背压。为什么?因为 emit() 是挂起函数,它会自动等待 collect() 消费完。

来看个对比的例子:

// 快速发射,慢速收集
fun fastProducer(): Flow<Int> = flow {
    var i = 0
    while (true) {
        emit(i++)
        delay(10) // 每 10ms 发射一个
    }
}

fun main() = runBlocking {
    fastProducer().collect { value ->
        delay(100) // 每 100ms 处理一个
        println("处理了: $value")
    }
}

这段代码会出问题吗?

不会。因为 emit() 会等 collect() 处理完。发射端每 10ms 准备发射,但收集端每 100ms 才消费一个。所以实际效果是——每 100ms 发射并收集一个数据。

注意:虽然 Flow 天然支持背压,但如果你在 emit() 里做了大量计算,或者在 collect() 里做了耗时操作,还是会导致性能问题。我曾经在一个项目里,collect() 里做了网络请求,结果整个 Flow 被卡住了。后来用 buffer() 操作符才解决。

3.4 实战中的注意事项

在实际项目中,我总结了几个要点:

  • emit() 必须在协程上下文中调用——因为它是个挂起函数。不能在普通线程里直接调用。
  • collect() 是终端操作——一个 Flow 只能被 collect 一次。如果你想多次收集,需要用 shareIn 或 stateIn。
  • 取消协程会自动取消 Flow——Flow 是结构化的,父协程取消,子协程也会取消。

来看个常见的错误用法:

// ❌ 错误用法
fun badFlow(): Flow<Int> = flow {
    // 这里不能直接调用挂起函数
    // 因为 flow builder 已经提供了协程上下文
    withContext(Dispatchers.IO) { // 不要这样做!
        emit(1)
    }
}

// ✅ 正确用法
fun goodFlow(): Flow<Int> = flow {
    // 直接在 flow builder 里调用
    emit(1)
    // 或者用 flowOn 切换上下文
}.flowOn(Dispatchers.IO)

避坑指南:我曾经在 flow builder 里用 withContext 切换线程,结果 emit() 报错了。后来才知道,flow builder 已经提供了协程上下文,你只需要用 flowOn 操作符来切换调度器。

3.5 小结

好了,咱们来总结一下:

概念 说明
emit() 挂起函数,发射数据,会等待下游消费
collect() 终端操作符,订阅并消费数据
背压 Flow 天然支持,emit() 会等待 collect()
协程上下文 用 flowOn 切换,不要在 flow builder 里用 withContext

嗯,这就是 Flow 最基础的发射与收集机制。说白了,就是「你发一个,我收一个」的简单模式。但正是这种简单,让 Flow 在处理背压时比 RxJava 优雅得多。

下一章,咱们会深入聊聊 Flow 的各种操作符,看看怎么对数据流进行变换、过滤、合并。到时候你会发现,Flow 的威力远不止于此。