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()
相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
小羊没烦恼!3 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
伞伞悦读3 天前
【第38期】Python 模块与包详解:import、from、模块搜索路径、包结构和 __init__
开发语言·python
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
C语言小火车3 天前
C/C++ 为什么需要编译器?
开发语言·c++
霍霍的袁3 天前
【C++】map 和 set 的使用 | 从用法到底层
开发语言·c++·学习·visual studio