首次推送
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
# 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 统一响应清理。
|
||||
Reference in New Issue
Block a user