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
是否立即发射? 否(可能会挂起) 是(如果缓冲区允许)
是否可在协程中使用? 必须 不需要

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

相关推荐
stevenzqzq9 小时前
Android Hilt 入门教程_传统写法和Hilt写法的比较
android
wuwu_q9 小时前
用通俗易懂方式,详细讲讲 Kotlin Flow 中的 map 操作符
android·开发语言·kotlin
_李小白10 小时前
【Android FrameWork】第五天:init加载RC文件
android
2501_9160074710 小时前
手机使用过的痕迹能查到吗?完整查询指南与步骤
android·ios·智能手机·小程序·uni-app·iphone·webview
黄毛火烧雪下11 小时前
React Native (RN)项目在web、Android和IOS上运行
android·前端·react native
下位子11 小时前
『OpenGL学习滤镜相机』- Day7: FBO(帧缓冲对象)
android·opengl
從南走到北11 小时前
JAVA国际版同城外卖跑腿团购到店跑腿多合一APP系统源码支持Android+IOS+H5
android·java·ios·微信小程序·小程序
空白格9711 小时前
组件化攻略
android
岸芷漫步11 小时前
android框架层弹出对话框的分析
android
Android疑难杂症11 小时前
鸿蒙Media Kit媒体服务开发快速指南
android·harmonyos·音视频开发