322 lines
10 KiB
Markdown
322 lines
10 KiB
Markdown
# 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 状态
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、线程模型全景
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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 密封类
|
|||
|
|
|
|||
|
|
```kotlin
|
|||
|
|
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 核心结构
|
|||
|
|
|
|||
|
|
```kotlin
|
|||
|
|
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 合并:
|
|||
|
|
|
|||
|
|
```kotlin
|
|||
|
|
// 在 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 回调注册
|
|||
|
|
|
|||
|
|
```kotlin
|
|||
|
|
// 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 成员变化事件流(合并)
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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 连接事件流(立即)
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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 与现有组件的关系
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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 统一响应清理。
|