Kotlin Flow 的 emit 和 tryEmit 有什么区别

Kotlin Flow 是一个用于管理异步数据流的强大 API。如果你一直在使用 SharedFlowStateFlow,你可能遇到过两个函数:emittryEmit。尽管它们看起来很相似,但在底层的行为却截然不同。

在这篇博客中,我们将通过简单的示例,详细分析每个函数的作用、它们之间的区别以及何时使用它们。

在开始之前,请确保你掌握了协程和 Flow 的基础知识, 这样你才能理解诸如 suspend、收集器、StateFlowSharedFlow 等术语。
如果你想深入了解 Flow,我也给你准备了一万字

emit

emit 是一个挂起函数,当你使用它的时候,它会挂起协程的执行,直到值被收集:

Kotlin 复制代码
fun main(args: Array<String>): Unit = runBlocking {

    val flow = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 0, onBufferOverflow = BufferOverflow.SUSPEND) 

    val cJob = launch(start = CoroutineStart.ATOMIC) {
        timeLog("start a collect")
        flow.collect {
            timeLog("collected: $it")
            delay(2000L)
        }
    }

    delay(1000L) // 这里很重要,我们必须要确保在 emit 之前有 collector

    val emitJob = launch(Dispatchers.IO) {
        repeat(5) {
            timeLog("emit $it")
            flow.emit(it)
        }
    }

    emitJob.join()
    delay(1000L)
    cJob.cancel()
}

我们来看看上面这段代码,emit 会一直等待下游的处理。

来看看输出结果:

makefile 复制代码
22:38:1.982 start a collect
22:38:2.991 emit 0
22:38:2.993 emit 1
22:38:2.993 collected: 0
22:38:5.0 collected: 1
22:38:5.0 emit 2
22:38:7.7 collected: 2
22:38:7.7 emit 3
22:38:9.13 emit 4
22:38:9.13 collected: 3
22:38:11.15 collected: 4

日志因为时序问题,可能跟我们想象的不太一样,但是我们仔细看 emit 相关日志,就会发现,在第一次收集之后,后续的 emit 每次都会间隔 2 秒左右,这就证明了 emit 在等待下游收集后才会继续发送新的值。

tryEmit

tryEmit 是非挂起函数。如果值成功发送,它将返回 true,否则返回 false

同样的代码,我们只是将 emit 改成 tryEmit,同时稍微加点日志:

Kotlin 复制代码
//...
timeLog("emit $it")
val res = flow.tryEmit(it)
timeLog("emit $it result: $res")
//...

各位可以先猜测一下输出。

这个输出可能各位想不到,我们直接看结果:

sql 复制代码
22:44:25.119 start a collect
22:44:26.125 emit 0
22:44:26.129 emit 0 result: false
22:44:26.129 emit 1
22:44:26.129 emit 1 result: false
22:44:26.129 emit 2
22:44:26.129 emit 2 result: false
22:44:26.129 emit 3
22:44:26.129 emit 3 result: false
22:44:26.129 emit 4
22:44:26.129 emit 4 result: false

你会发现根本没有收集到任何值。

问题出在构造函数这里:extraBufferCapacity = 0。这句话的意思是不要缓存保留值。这在使用 tryEmit 时,每次都会丢弃该值。

我们将其改为 1,测试看看。

Kotlin 复制代码
val flow = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.SUSPEND)
//...

输出如下:

Kotlin 复制代码
22:47:57.199 start a collect
22:47:58.217 emit 0
22:47:58.219 collected: 0
22:47:58.220 emit 0 result: true
22:47:58.220 emit 1
22:47:58.220 emit 1 result: true
22:47:58.220 emit 2
22:47:58.221 emit 2 result: false
22:47:58.221 emit 3
22:47:58.221 emit 3 result: false
22:47:58.221 emit 4
22:47:58.221 emit 4 result: false

我们确实能收集到值了,不过只能收集到一个。

查看 emit 的日志,tryEmit 根本不会等待下游的收集。

当你不在协程中,或者不想挂起时,可以使用 tryEmit

没错,tryEmit 是一个普通函数,你可以在任何地方使用。

对于那些可以容忍发送失败或需要以不同方式处理发送失败的即发即弃的情况,它非常适用。

常见注意事项

tryEmit 可能会悄无声息地失败

如果你不检查 tryEmit 的返回值,可能会错过发送失败的情况:

Kotlin 复制代码
if (!sharedFlow.tryEmit(1)) {
    // 处理失败情况
}

当然,如果 tryEmit 的成功与否并不重要,那么就不需要关心返回值。

缓冲区大小很重要

正如上面的代码那样,在缓冲区为 0 的时候,tryEmit 会失败。

emit 可能会阻塞你的协程

如果收集器运行缓慢或不可用,emit 可能会挂起并延迟其他操作。

下面是一个小表格,可以帮助快速了解何时使用 emittryEmit

场景 使用 emit 使用 tryEmit
在挂起函数内部 避免使用(除非非常紧急)
UI 点击或生命周期事件 可能(如果需要快速更新)
回调、监听器或后台线程 避免使用(不能挂起)
需要保证数据送达 否(需自行处理失败情况)
发送即忘(fire-and-forget)场景

两句话概括就是:

  • 当你需要可靠的、对协程友好的发送方式时,使用 emit
  • 当你希望实现无阻塞、快速触发且不暂停的操作时,使用 tryEmit

总结

最后,我们总结一下二者的区别:

特性 emit() tryEmit()
挂起函数
等待收集者 是(如果缓冲区已满)
返回类型 Unit Boolean
是否立即发射? 否(可能会挂起) 是(如果缓冲区允许)
是否可在协程中使用? 必须 不需要

了解如何为合适的任务选择合适的工具,有助于避免事件丢失、程序冻结以及意外的错误。

相关推荐
xiaolizi5674898 小时前
安卓远程安卓(通过frp与adb远程)完全免费
android·远程工作
阿杰100019 小时前
ADB(Android Debug Bridge)是 Android SDK 核心调试工具,通过电脑与 Android 设备(手机、平板、嵌入式设备等)建立通信,对设备进行控制、文件传输、命令等操作。
android·adb
梨落秋霜9 小时前
Python入门篇【文件处理】
android·java·python
zFox10 小时前
四、ViewModel + StateFlow + 状态持久化
kotlin·stateflow·viewmodel
遥不可及zzz11 小时前
Android 接入UMP
android
Coder_Boy_13 小时前
基于SpringAI的在线考试系统设计总案-知识点管理模块详细设计
android·java·javascript
冬奇Lab14 小时前
【Kotlin系列03】控制流与函数:从if表达式到Lambda的进化之路
android·kotlin·编程语言
冬奇Lab14 小时前
稳定性性能系列之十二——Android渲染性能深度优化:SurfaceFlinger与GPU
android·性能优化·debug
冬奇Lab15 小时前
稳定性性能系列之十一——Android内存优化与OOM问题深度解决
android·性能优化