Kotlin MutableSharedFlow: emit vs tryEmit 详解

一、核心区别

特性 emit tryEmit
挂起函数 ✅ 是 suspend 函数 ❌ 不是挂起函数
缓冲区满时行为 挂起等待直到有空间 立即返回 false,丢弃数据
返回值 无(Unit) Boolean(true=成功,false=失败)
使用场景 协程中 普通函数/回调中
线程安全

二、emit 详细说明

kotlin 复制代码
// emit 是 suspend 函数,只能在协程或挂起函数中调用
public suspend fun emit(value: T)

工作原理:

  • 当缓冲区有空间时,立即发射值并返回
  • 当缓冲区满时,挂起调用者,等待缓冲区有可用空间
  • 保证了数据的不丢失(背压支持)
kotlin 复制代码
val sharedFlow = MutableSharedFlow<Int>(
    replay = 0,     // 新订阅者不会收到历史值
    extraBufferCapacity = 2,  // 额外缓冲容量为2
    onBufferOverflow = BufferOverflow.SUSPEND  // 默认:缓冲满时挂起
)


// ✅ 正确:在协程中使用 emit
viewModelScope.launch {
    sharedFlow.emit(1)  // 缓冲区有空间,立即返回
    sharedFlow.emit(2)  // 缓冲区有空间,立即返回
    sharedFlow.emit(3)  // 缓冲区满,挂起等待消费者读取后才能继续
}

三、tryEmit 详细说明

kotlin 复制代码
// tryEmit 不是 suspend 函数,可在任何地方调用
public fun tryEmit(value: T): Boolean

工作原理:

  • 尝试立即发射值,不挂起
  • 如果缓冲区有空间 → 发射成功,返回 true
  • 如果缓冲区满 → 丢弃数据,返回 false
  • 不保证数据一定被发送
kotlin 复制代码
val sharedFlow = MutableSharedFlow<Int>(
    replay = 0,
    extraBufferCapacity = 2,
    onBufferOverflow = BufferOverflow.SUSPEND
)

// ✅ 在普通函数/回调中使用 tryEmit
fun onClick() {
    val success = sharedFlow.tryEmit(1)  // true,缓冲区有空间
    val failed = sharedFlow.tryEmit(99)  // 可能返回 false(缓冲区满时)
}

四、tryEmit 成功条件

tryEmit 在以下情况返回 true:

  1. replay != 0 时(有重放缓存)
  2. extraBufferCapacity > 0 且缓冲区未满
  3. 有活跃订阅者且其缓冲区未满

关键陷阱: 默认配置下 tryEmit 极易失败!

kotlin 复制代码
// ⚠️ 默认配置:replay=0, extraBufferCapacity=0
val defaultFlow = MutableSharedFlow<String>()

// 这种情况下 tryEmit 几乎总是返回 false!
// 因为没有缓冲区,且没有活跃订阅者时无处存放数据
defaultFlow.tryEmit("hello")  // 大概率返回 false,数据丢失!

五、完整代码示例

kotlin 复制代码
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

class EventViewModel : ViewModel() {

    // ============ 方案1:使用 emit(推荐,数据不丢失)============
    private val _events = MutableSharedFlow<String>(
        replay = 0,
        extraBufferCapacity = 10,
        onBufferOverflow = BufferOverflow.SUSPEND
    )
    val events = _events.asSharedFlow()

    // 在协程中使用 emit ------ 数据安全
    fun sendDataWithEmit(data: String) {
        viewModelScope.launch {
            _events.emit(data)  // 挂起函数,保证数据发送
        }
    }

    // ============ 方案2:使用 tryEmit(适合回调场景)============
    private val _uiEvents = MutableSharedFlow<UiEvent>(
        replay = 0,
        extraBufferCapacity = 1,  // ⚠️ 必须设置缓冲区!
        onBufferOverflow = BufferOverflow.DROP_OLDEST  // 满时丢弃最旧的
    )
    val uiEvents = _uiEvents.asSharedFlow()

    // 在非协程回调中使用 tryEmit
    fun onButtonClick() {
        // 不需要协程,适合在普通函数/回调中调用
        val success = _uiEvents.tryEmit(UiEvent.ShowToast("Clicked!"))
        if (!success) {
            Log.w(TAG, "Event dropped!")
        }
    }

    // ============ 方案3:ChannelFlow 替代(1对1场景)============
    private val _navigation = Channel<NavEvent>()
    val navigation = _navigation.receiveAsFlow()

    fun navigate(event: NavEvent) {
        _navigation.trySend(event)  // Channel 的 trySend
    }
}

// ============ 使用示例 ============
class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            viewModel.events.collect { event ->
                // 处理事件
                println("Received: $event")
            }
        }
    }
}

六、理解误区

1、如果没有collect执行,emit发射数据时,会不会挂起等待?直到collect收集。

在 Kotlin 协程的 MutableSharedFlow中,emit不会因为"没有 Collector(收集者)"而挂起 。它只会因为"有 Collector 但处理不过来(背压)"而挂起。

只有当存在活跃的 Collector ,且数据生产速度 > 消费速度 时,emit才会挂起。

打个比方:

没 Collector:就像对着空气喊话。喊完就完事了,不需要等谁回应。

有 Collector 但没挂起:就像对着一群人演讲,手里拿了个麦克风(缓冲区),还能装得下。

挂起等待:就像你拿着麦克风(缓冲区满了),台下的人还在记笔记没听完,你只能暂停演讲,等他们记完了你再继续。

你感觉到的"挂起",通常是用了默认配置 (replay=0, extraBufferCapacity=0) 且有慢速 Collector 时发生的。

六、最佳实践总结

  1. 优先使用 emit:在协程环境中始终优先使用 emit,它保证数据不丢失
  2. tryEmit 必须配合缓冲区:如果使用 tryEmit,务必设置 extraBufferCapacity > 0 或 onBufferOverflow = DROP_OLDEST
  3. 默认配置是陷阱:MutableSharedFlow() 默认 extraBufferCapacity=0,此时 tryEmit 在无订阅者时必定失败
  4. 回调场景用 tryEmit:当无法使用协程时(如第三方库回调),tryEmit 是唯一选择
  5. 1对1事件用 Channel:如果事件只需一个消费者处理,考虑使用 Channel + receiveAsFlow()
相关推荐
喜欢踢足球的老罗1 小时前
Chrome MV3 插件架构深度解析:Service Worker 生命周期与 Token 管理的三层博弈
前端·chrome·架构
buxiangshui_cd1 小时前
Conda命令
开发语言·python·conda
踏着七彩祥云的小丑1 小时前
Go学习第2天:程序结构+基础语法+数据类型
开发语言·学习·golang·go
小李云雾1 小时前
Pinia:Vue3 全局状态管理从入门到精通
前端·javascript·vue.js
隔窗听雨眠1 小时前
VMware迁移上云的十个关键关卡
开发语言·php·vmware
caimouse1 小时前
Reactos 第 5 章 进程与线程 — 5.4 系统调用 NtCreateThread()
服务器·开发语言
吴梓穆1 小时前
Python 基础语法2 if 运算符 循环
android·开发语言·python
如竟没有火炬1 小时前
整数拆分——动态规划
开发语言·数据结构·python·算法·leetcode·动态规划
Upsy-Daisy1 小时前
Hermes Agent 学习笔记 03:CLI 与 TUI 使用体验,让 Agent 真正进入终端工作流
服务器·前端·数据库