AskaEth bbc47dbd31 feat(chat): 常驻 ChatViewModel 加 LRU 回收(S4)—— 一格一份 ViewModelStore,最近 3 格之外整格交出去
S4 的病根:每个访问过的会话各留一个常驻 VM(都订阅那条共享连接 ⇒ 每次重连各自发一轮
after 补拉)、各留一份消息列表(含内联 base64 图片),而删会话也不回收。此前**没有按会话
交出一格的手段**:所有 VM 塞在 Activity 级那一个 ViewModelStore 里,而它只有整体 clear(),
W48 只能做到「让 key 换一代、旧实例继续留着」—— 旧实例照样活着、照样收帧。

这次把「哪几格留着」这件事单独拎出来:

- 新增 `core/session/ChatSessionSlots.kt`(进程级,挂在 AppContainer 上,与 GoneSessions
  同一层):一格一份 ViewModelStore。`ChatScreen` 改成
  `viewModel(viewModelStoreOwner = <槽位>, key = chatViewModelKey(...))`,所以「交出去一格」
  就是那一份 store 自己的 `clear()` ⇒ 那一格的 `onCleared()` + `viewModelScope` 取消
  (订阅、补拉、在飞的请求一起停)。共享连接的生死一个字节都没碰:主人仍是
  SessionsViewModel,`ChatViewModel.onCleared` 照旧**不** close(现在它会真的被调到,
  所以这句注释也一并写成事实)。
- 新增判据 `core/session/chatSessionEvictions`(**纯函数**,VM 与测试共用):
  「最近」的口径 = **最后一次成为当前会话的时刻**(由 ChatScreen 在组合到某个会话时写入);
  候选要同时满足「不是主会话 ∧ 不是屏幕上那一格 ∧ 没有没交代完的事」,按最旧的先走,
  保留最近 `keep` = 3 格(主人口径,含被豁免的那些占的名额——不然「最多 3 格」会变成
  3 + 豁免数)。两条豁免名额之外的豁免各自都有硬理由:屏幕那一格死了会「不再更新、连
  发送都静默不发」,还有没交代完的事那一格(草稿 / 排队等连接的发送)死了内容**回不来**。
- 删会话时那一格当场交出去(`SessionsViewModel.deleteSession`,与 W51 的
  `goneSessions.mark` 相邻:一个是「别发注定 404 的补拉」,一个是「这一格本来就不该留」);
  登出 / 换账号时 `clear()` 一格不留(与快照 / 已读游标 / GoneSessions 同档纪律)。
- 「还有没交代完的事」收的是**现读的探针**而不是打开那一刻的快照:回收发生在别的会话被
  打开的那一刻,而那正是用户可能刚在前一格打了草稿、还没切走的时候。

刻意不动的红线:W48 的 `chatViewModelKey` 拼法一个字没改(新槽位的 key 里同样带账号指纹,
两道闸门各管一段);W49 的节流判据、W50 的 ProactiveIngest 判据、W51 的 reconnectPull
判据都没碰,也没有把「连接恢复」做成整表重拉。

测试:新增 `ChatSessionEvictionTest`(14 例,穷举判据的每一条边界:N=3、最旧先走、主会话 /
屏幕上那一格 / 有草稿的豁免、豁免占名额、并列退回账本顺序、结论是带账号指纹的槽位 key)
与 `ChatSessionSlotsTest`(9 例,钉住「没被收的一格还是原来那一格(切回来状态还在)」与
「被收的一格下次拿到的是新的一格」、空槽位会被下一次回收扫掉、探针现读的效果)。

同批把已经说错的注释改成事实:GoneSessions / AppContainer.chatSocket / ChatHomeScreen /
ChatScreen 里那几处「VM 活在 Activity 级 ViewModelStore 里、切走 / 删会话都不回收」。
2026-10-06 15:56:13 +08:00
S
Description
昔涟在安卓设备上的载体。
Apache-2.0
19 MiB
0 Stars 1 Watchers 0 Forks
2026-05-29 22:15:56 +08:00