
Kotlin Flow 是一个用于管理异步数据流的强大 API。如果你一直在使用 SharedFlow 或 StateFlow,你可能遇到过两个函数:emit 和 tryEmit。尽管它们看起来很相似,但在底层的行为却截然不同。
在这篇博客中,我们将通过简单的示例,详细分析每个函数的作用、它们之间的区别以及何时使用它们。
在开始之前,请确保你掌握了协程和
Flow的基础知识, 这样你才能理解诸如suspend、收集器、StateFlow、SharedFlow等术语。
如果你想深入了解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 可能会挂起并延迟其他操作。
下面是一个小表格,可以帮助快速了解何时使用 emit 和 tryEmit:
| 场景 | 使用 emit | 使用 tryEmit |
|---|---|---|
| 在挂起函数内部 | 是 | 避免使用(除非非常紧急) |
| UI 点击或生命周期事件 | 是 | 可能(如果需要快速更新) |
| 回调、监听器或后台线程 | 避免使用(不能挂起) | 是 |
| 需要保证数据送达 | 是 | 否(需自行处理失败情况) |
| 发送即忘(fire-and-forget)场景 | 否 | 是 |
两句话概括就是:
- 当你需要可靠的、对协程友好的发送方式时,使用
emit。 - 当你希望实现无阻塞、快速触发且不暂停的操作时,使用
tryEmit。
总结
最后,我们总结一下二者的区别:
| 特性 | emit() | tryEmit() |
|---|---|---|
| 挂起函数 | 是 | 否 |
| 等待收集者 | 是(如果缓冲区已满) | 否 |
| 返回类型 | Unit | Boolean |
| 是否立即发射? | 否(可能会挂起) | 是(如果缓冲区允许) |
| 是否可在协程中使用? | 必须 | 不需要 |
了解如何为合适的任务选择合适的工具,有助于避免事件丢失、程序冻结以及意外的错误。