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

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

相关推荐
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android