Files
ts-mobile-go/docs/流程/09_EventBus架构.md
T
2026-07-20 19:01:03 +08:00

10 KiB
Raw Blame History

EventBus 架构

本文档描述 TeamSpeak 事件总线(EventBus)的设计,将 TS 事件线程与 UI 渲染线程分离。 EventBus 是 TSBridge 回调与 ViewModel 状态更新之间的中间层,负责事件收集、合并、节流和分发。


一、设计目的

1.1 问题

当前架构中,Go JNI 回调直接触发 ViewModel 状态更新:

Go goroutine → JNI callback → ServerViewModel (直接写 StateFlow)

存在的问题:

  1. 线程不一致JNI 回调在 Go goroutine 线程上执行,部分状态更新直接赋值(_state.value = xxx),部分通过 viewModelScope.launch 切到 Main,缺乏统一的线程策略。
  2. 事件风暴:服务器连续推送多个 onClientEnter / onClientMoved 事件时,每次都触发 refreshClientList() 全量查询,造成不必要的命令开销和 UI 频繁重组。
  3. 关注点耦合TSBridge 回调直接依赖 ViewModel 实现,无法独立调试、日志记录或重放事件。

1.2 目标

Go goroutine → JNI callback → TSBridge → [EventBus] → ViewModel → StateFlow → UI
                                     ↑
                                 这一层负责:
                                 - 统一线程调度(全部切到 Main)
                                 - 合并高频事件(debounce
                                 - 事件日志与调试
                                 - TS 线程完全不碰 UI 状态

二、线程模型全景

flowchart TB
    subgraph GO[Go Runtime]
        NET[Network goroutines<br/>receiveLoop / processLoop / resendLoop / pingLoop]
        EVT[事件消费 goroutine<br/>串行调用 JNI 回调]
        NET --> EVT
    end

    subgraph KOTLIN[Kotlin / Android]
        subgraph IO[Dispatchers.IO 线程池]
            CAP[麦克风采集循环]
            PLAY[语音解码与播放]
            FETCH[JSON 解析<br/>fetchChannels / fetchClients]
        end

        subgraph MAIN[Main Thread]
            VM[ViewModel 业务逻辑]
            SF[StateFlow 状态更新]
            UI[Compose UI 渲染]
            VM --> SF --> UI
        end

        EB[EventBus<br/>事件收集 / 合并 / 分发]
    end

    EVT -->|JNI 回调<br/>Go goroutine 线程| EB
    EB -->|Dispatchers.Main| VM
    CAP -->|Dispatchers.IO| EB
    FETCH -->|Dispatchers.IO| EB

线程职责划分

线程 / 调度器 职责 说明
Go goroutines 网络收发、协议处理、事件队列消费 双层事件队列保证 JNI 回调串行执行
JNI 回调线程 仅负责"把事件扔进 EventBus" 不做任何状态更新或 UI 操作
Dispatchers.IO 麦克风采集、语音播放、JSON 解析 阻塞 IO 操作
Dispatchers.Main ViewModel 逻辑、StateFlow 更新、Compose 渲染 所有 UI 状态变更在主线程

三、事件定义

3.1 TSEvent 密封类

sealed class TSEvent {
    // 连接生命周期
    object Connected : TSEvent()
    data class Disconnected(val message: String) : TSEvent()
    data class Kicked(val reason: String) : TSEvent()

    // 成员变化
    data class ClientEnter(val client: Client) : TSEvent()
    data class ClientLeave(val id: Long, val reasonMsg: String) : TSEvent()
    data class ClientMoved(val id: Long, val targetChannelID: String) : TSEvent()

    // 消息
    data class TextMessage(val msg: TextMsg) : TSEvent()

    // 语音
    data class VoiceData(val clientID: Long, val data: ByteArray, val codec: Long) : TSEvent()

    // 通知
    data class Poked(val event: TSBridge.PokeEventData) : TSEvent()
}

3.2 事件分类

类别 事件 处理策略
连接生命周期 Connected / Disconnected / Kicked 立即分发 — 影响全局状态,不可延迟
成员变化 ClientEnter / ClientLeave / ClientMoved 合并分发 — debounce 后触发一次 refreshClientList
消息 TextMessage 立即分发 — 用户期望实时看到新消息
语音 VoiceData IO 线程直接处理 — 不经过 EventBus,延迟敏感
通知 Poked 立即分发 — 需要弹出通知

四、EventBus 实现

4.1 核心结构

object EventBus {
    private val _events = MutableSharedFlow<TSEvent>(
        replay = 0,
        extraBufferCapacity = 64,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )
    val events: SharedFlow<TSEvent> = _events.asSharedFlow()

    /** 发送事件(可从任意线程调用) */
    fun emit(event: TSEvent) {
        _events.tryEmit(event)
    }
}

4.2 事件合并策略

对于高频成员变化事件,使用 debounce 合并:

// 在 ViewModel 中收集事件时
viewModelScope.launch {
    EventBus.events
        .filterIsInstance<TSEvent.ClientEnter>()
        .debounce(300) // 300ms 内的多次 ClientEnter 合并为一次
        .collect {
            Repository.refreshClientList()
        }
}

合并规则:

事件 合并策略 延迟 说明
ClientEnter debounce 300ms 连续多人进入时只刷新一次
ClientLeave debounce 300ms 连续多人离开时只刷新一次
ClientMoved debounce 300ms 连续多人移动时只刷新一次
Connected 不合并 0 立即处理
Disconnected 不合并 0 立即处理
Kicked 不合并 0 立即处理
TextMessage 不合并 0 立即归档
Poked 不合并 0 立即弹出

4.3 TSBridge 回调注册

// TSBridge.connect() 内部
val callbackProxy = object : EventCallback {
    override fun onConnected() {
        EventBus.emit(TSEvent.Connected)
    }
    override fun onDisconnected(message: String) {
        EventBus.emit(TSEvent.Disconnected(message))
    }
    override fun onTextMessage(msg: TextMsg?) {
        msg?.let { EventBus.emit(TSEvent.TextMessage(it)) }
    }
    override fun onClientEnter(client: Client?) {
        client?.let { EventBus.emit(TSEvent.ClientEnter(it)) }
    }
    override fun onClientLeave(id: Long, reasonMsg: String) {
        EventBus.emit(TSEvent.ClientLeave(id, reasonMsg))
    }
    override fun onClientMoved(id: Long, targetChannelID: String) {
        EventBus.emit(TSEvent.ClientMoved(id, targetChannelID))
    }
    override fun onKicked(reason: String) {
        EventBus.emit(TSEvent.Kicked(reason))
    }
    override fun onVoiceData(clientID: Long, data: ByteArray?, codec: Long) {
        // 语音数据延迟敏感,不经过 EventBus,直接回调 VoiceService
        data?.let { callbacks.onVoiceData(clientID, it, codec) }
    }
    override fun onPoked(event: TSBridge.PokeEventData?) {
        event?.let { EventBus.emit(TSEvent.Poked(it)) }
    }
}

五、事件流时序

5.1 成员变化事件流(合并)

sequenceDiagram
    participant TS as TeamSpeak 服务器
    participant GO as Go 事件消费 goroutine
    participant JNI as JNI 回调
    participant EB as EventBus
    participant VM as ViewModel (Main)
    participant UI as Compose UI

    TS-->>GO: notifycliententerview (客户端 A)
    GO->>JNI: OnClientEnter(A)
    JNI->>EB: emit(ClientEnter(A))

    TS-->>GO: notifycliententerview (客户端 B)
    GO->>JNI: OnClientEnter(B)
    JNI->>EB: emit(ClientEnter(B))

    TS-->>GO: notifyclientmoved (客户端 C)
    GO->>JNI: OnClientMoved(C)
    JNI->>EB: emit(ClientMoved(C))

    Note over EB: debounce 300ms 合并

    EB->>VM: collect → refreshClientList()
    VM->>UI: StateFlow 更新 → 一次性重组

5.2 连接事件流(立即)

sequenceDiagram
    participant GO as Go 事件消费 goroutine
    participant JNI as JNI 回调
    participant EB as EventBus
    participant VM as ServerViewModel (Main)
    participant UI as Compose UI

    GO->>JNI: OnConnected()
    JNI->>EB: emit(Connected)
    EB->>VM: collect → 处理连接成功
    VM->>VM: 执行首次同步
    VM->>UI: 连接状态更新

5.3 语音数据流(绕过 EventBus

sequenceDiagram
    participant GO as Go 事件消费 goroutine
    participant JNI as JNI 回调
    participant VS as VoiceService (IO)
    participant SP as 扬声器

    GO->>JNI: OnVoiceData(clientID, data, codec)
    JNI->>VS: handleVoiceData() 直接回调
    VS->>VS: Opus 解码 (Dispatchers.IO)
    VS->>SP: AudioTrack.write()

六、EventBus 与现有组件的关系

flowchart TB
    subgraph 现有架构
        TSB[TSBridge<br/>JNI 回调注册]
        SVM[ServerViewModel<br/>连接 / 断开 / 重连]
        CVM[ChannelViewModel<br/>频道列表 / 切换]
        CHVM[ChatViewModel<br/>消息归档]
        VVM[VoiceViewModel<br/>语音控制]
        REPO[Repository<br/>状态仓库]
    end

    subgraph 新增
        EB[EventBus<br/>事件收集 / 合并 / 分发]
        EVT[TSEvent 密封类<br/>事件类型定义]
    end

    TSB -->|JNI 回调| EB
    EB -->|SharedFlow| SVM
    EB -->|SharedFlow| CVM
    EB -->|SharedFlow| CHVM
    EB -->|SharedFlow| VVM
    SVM --> REPO
    CVM --> REPO
    CHVM --> REPO

    EB -.->|语音绕过| VVM

各 ViewModel 监听的事件

ViewModel 监听事件 处理方式
ServerViewModel Connected / Disconnected / Kicked 立即更新连接状态
ChannelViewModel ClientEnter / ClientLeave / ClientMoved debounce 后 refreshClientList
ChatViewModel TextMessage 立即归档消息
VoiceViewModel 语音数据由 VoiceService 直接处理

七、与统一实现原则的关系

EventBus 架构强化了以下实现原则:

  1. 事件通过 EventBus 分发,不在回调中直接更新 UI 状态。 JNI 回调只负责 emit()ViewModel 通过 collect() 响应。
  2. 列表命令建立基线,服务端事件维护增量。 EventBus 的 debounce 机制确保增量事件不会导致过度刷新。
  3. 成员表按 ClientID 幂等更新。 debounce 后的 refreshClientList 是全量刷新,天然幂等。
  4. 断开时统一清理当前会话资源。 Disconnected 事件通过 EventBus 统一分发,各 ViewModel 统一响应清理。