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

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

相关推荐
好好学习啊天天向上4 小时前
Android Studio 撕开安卓手机投屏
android·智能手机·android studio
Android-Flutter5 小时前
android - JPG图片转换HDR图片,heic格式
android
诸神黄昏EX13 小时前
Android Build系列专题【篇四:编译相关语法】
android
雨白15 小时前
优雅地处理协程:取消机制深度剖析
android·kotlin
leon_zeng015 小时前
更改 Android 应用 ID (ApplicationId) 后遭遇记
android·发布
2501_9160074717 小时前
iOS 混淆工具链实战,多工具组合完成 IPA 混淆与加固(iOS混淆|IPA加固|无源码混淆|App 防反编译)
android·ios·小程序·https·uni-app·iphone·webview
Jeled19 小时前
Retrofit 与 OkHttp 全面解析与实战使用(含封装示例)
android·okhttp·android studio·retrofit
ii_best21 小时前
IOS/ 安卓开发工具按键精灵Sys.GetAppList 函数使用指南:轻松获取设备已安装 APP 列表
android·开发语言·ios·编辑器
2501_9159090621 小时前
iOS 26 文件管理实战,多工具组合下的 App 数据访问与系统日志调试方案
android·ios·小程序·https·uni-app·iphone·webview