文档版本 2026-05-29 · 背景:1300 条 ble_token_renew 来自单个访客、在主人离线的 4 小时内——正是这一点促使我们去寻找其他多余 HTTP 与 WebSocket 流量的来源。
ble_token_renew in pending_host_msgs (在 host_id, dedup_key上的 UNIQUE INDEX)。迁移已应用。GuestConn.requestBleTokenRenew ——每个 bleId 每 60 秒至多一次。onOpen;它要等到至少 60 秒的稳定会话,从而防住“连上—掉线—再连”的风暴。Files: HostService.kt:1424-1432, Vault.kt:213
private val pendingKeysRunnable = object : Runnable {
override fun run() {
if (Prefs.isHostRegistered && Prefs.getPendingHostKeys().isNotEmpty()) {
Thread { syncPendingHostKeys() }.start()
}
pendingKeysHandler.postDelayed(this, 60_000L)
}
}
syncPendingHostKeys 取出全部待处理的主机密钥并调用 Api.keyCreate逐个处理。若 HTTP 调用失败,密钥仍处于待处理状态,60 秒后循环重来。
В Prefs 按 localId 存储:
attempt_at_<localId> ——上次尝试的时间戳attempt_count_<localId> ——尝试计数器每次运行 syncPendingHostKeys:
val waitMs = (30_000L * (1L shl attemptCount)).coerceAtMost(30 * 60_000L)
if (now - attemptAt < waitMs) return@forEach // skip
// otherwise try
val ok = try { Api.keyCreate(...); true } catch (e) { false }
if (ok) { clear attempt_at/count } else { attempt_count++; attempt_at = now }
Progression: 30 秒 → 1 分钟 → 2 分钟 → 4 分钟 → 8 分钟 → 16 分钟 → 30 分钟(封顶)。在持续失败的情况下,就是每 30 分钟一次请求,而不是每分钟一次。 降低到三十分之一。
同样适用于 Vault.retryPendingBundles() ——同样的套路。
snap_url 是摄像机、录像机或 HTTP 源的外部地址。客户端通过 Coil 或 ExoPlayer 直接访问该地址,我们的服务器只是传递加密的 data_cipher 载荷——地址就藏在捆绑包里。画面不经过我们。
若某位主人把 snap_int=1 设成给 10 位访客用,他的录像机每秒就会收到 10 次请求。 那是他自己录像机的负载,与我们的服务器无关。
可选:把界面滑块的下限设为三秒。这是界面问题,不是基础设施问题。
已被 6.61 的修复约束住:
若有十个 BLE 对象,用户会连发十条 WebSocket 消息,服务器连着跑十次 case 'ble_token_renew' 分支。小事一桩。
File: GuestConn.kt ——7 处以上。
AppState.overridesVersion = MutableState<Int>,一个 Compose 信号,含义是“数据变了,重新读取 remember”。它用在 ActionCard:
val ovrVersion = AppState.overridesVersion.value
val (overrideLabel, avatarPath) = remember(action.key, ovrVersion) {
Prefs.getOverride(action.key) // SharedPreferences IO
}
val snapCfg = remember(action.key, ovrVersion) {
if (action.source == ActionSource.HOST_OWN) Prefs.getSnapshot(action.numberId)
else Prefs.getIncomingSnapshot(...)
}
val tpl = remember(action.key, ovrVersion) { loadTpl() } // more IO
每次递增都会让每张卡片从 SharedPreferences中重新读取。一个有 20 张卡片的用户就意味着 80 次 SharedPreferences 读取 (每次递增)。
| Line | Context | 需要吗? |
|---|---|---|
| L193 | 重新接收捆绑包之后(没有 K_obj) | yes |
| L355 | numbers_update 中 parseNumbers 之后 | redundant 当号码并未变化时 |
| L411 | 加载头像之后 | yes |
| L558, L595 | 其他处理器中 parseNumbers 之后 | redundant 当数据完全相同时 |
WebSocket 抖动时,每个 guest_ok → 处理器重新解析号码 → 递增 overridesVersion → 80 次 SharedPreferences 读取 × 20 张卡片 × guest_ok 的频率。
这属于本地 CPU 与 I/O, 并非服务器的直接负载,但是:
递增前,把新的 JSON 列表 numbers 与旧的比较——用哈希,或比较其中的 GuestNumberInfo)。相同 → 不递增。
private var lastNumbersHash: Int = 0
private fun maybeUpdateNumbers(arr: JSONArray?) {
val parsed = parseNumbers(arr)
val newHash = parsed.hashCode() // or a hash of the JSON serialisation
if (newHash == lastNumbersHash) return // nothing changed
lastNumbersHash = newHash
numbers = parsed
publish()
AppState.overridesVersion.value = AppState.overridesVersion.value + 1
}
numbers_update 来得很规律,却很少真的变化——主人不会每分钟都改对象。这项优化去掉了 约 90% 的多余重绘浪潮.