docs(05): 锚点补记三十六 —— W57 通知消费点提到进程级(根因链:连接是进程级/订阅挂 Activity 级 VM/replay=0 丢帧)+ 长跑第3次基线(开关确认是开的,推翻上节那条猜想)+ W57 设备验收步骤与副作用
This commit is contained in:
1 parent
900d191b8d
commit
f9555fba85
1 file changed
+47
@@ -1913,3 +1913,50 @@ adb reverse --remove tcp:80 && adb reverse tcp:80 tcp:10942
|
|||||||
3. 复查那一刻(及现在)App 进程**根本不在**(`pidof` 空、前台是 launcher、`KeepAliveService` 0 条)——**最可能的解释是保活开关当时没开**(没开 ⇒ 没有前台服务 ⇒ App 在后台待了几十分钟被系统正常回收,这正是「不保活」的预期)。⚠️ **这一条我没有实证**(App 已不在,读不到开关状态),**不要当结论**。
|
3. 复查那一刻(及现在)App 进程**根本不在**(`pidof` 空、前台是 launcher、`KeepAliveService` 0 条)——**最可能的解释是保活开关当时没开**(没开 ⇒ 没有前台服务 ⇒ App 在后台待了几十分钟被系统正常回收,这正是「不保活」的预期)。⚠️ **这一条我没有实证**(App 已不在,读不到开关状态),**不要当结论**。
|
||||||
|
|
||||||
**要重做才有效**:得先把 `tcp:80` 切回假后端 `18090`(`adb reverse --remove tcp:80 && adb reverse tcp:80 tcp:18090`),再在真机/模拟器上开保活开关、建立新基线。⚠️ **但切回假后端会打断主人此刻在模拟器上用真后端看真实会话**,所以**没做,等主人定**。
|
**要重做才有效**:得先把 `tcp:80` 切回假后端 `18090`(`adb reverse --remove tcp:80 && adb reverse tcp:80 tcp:18090`),再在真机/模拟器上开保活开关、建立新基线。⚠️ **但切回假后端会打断主人此刻在模拟器上用真后端看真实会话**,所以**没做,等主人定**。
|
||||||
|
|
||||||
|
## ★★ 锚点补记三十六 · W57(通知消费点提到进程级)+ 保活长跑第 3 次基线(2026-10-06 22:25)
|
||||||
|
|
||||||
|
### 1. W57:Activity 被划掉后也能弹通知 —— **根因与修法**(`0e03eba` → 合并 `900d191` = `origin/android`)
|
||||||
|
|
||||||
|
**结论先行**:帧**到得了**连接,是**到了没人消费**。改动面因此很小(没搬连接所有权、没新起连接)。
|
||||||
|
|
||||||
|
根因链(子代理逐条给了行号证据,改前状态):
|
||||||
|
|
||||||
|
1. **连接是进程级的**:`AppContainer.chatSocket`(`AppContainer.kt:282`,`by lazy`)在 `CyreneApplication.onCreate` 建;`ChatSocket.connect()` 把连接循环跑在它自己的 scope 上(`ChatSocket.kt:141,358-359`,`CoroutineScope(SupervisorJob() + Dispatchers.IO)`),**只有 `close()` 会取消它**(`ChatSocket.kt:165-175`)。
|
||||||
|
2. **没有 ViewModel 会关它**:`SessionsViewModel.onCleared()` 是空的,KDoc 明写「刻意不关连接」。
|
||||||
|
3. ⇒ 划掉 Activity 后连接照旧活着、`onMessage` 照旧 `_events.tryEmit(...)`。
|
||||||
|
4. ⚠️ **但 `_events` 是 `MutableSharedFlow(replay = 0, extraBufferCapacity = 512, DROP_OLDEST)`**(`ChatSocket.kt:101-105`)——`replay = 0` 意味着**不为将来的订阅者留任何东西,没有订阅者时 emit 就是当场丢**。
|
||||||
|
5. 而 proactive 帧当时的订阅者是 `SessionsViewModel.kt:232` 的 `viewModelScope.launch { socket.events.collect(::onEvent) }`,`viewModelScope` 随 VM `onCleared` 取消,该 VM 由 Activity 的 `ViewModelStore` 持有 ⇒ **划掉 Activity = 订阅被取消 = 帧被丢**。
|
||||||
|
|
||||||
|
**修法**(`app/ProactiveFrameIntake.kt` 新增):
|
||||||
|
- 进程级帧消费点:订 `ChatSocket.events`,把两类帧(实时 `proactive_message` / 带 `proactive` 的 `message_persisted`)交给 `ProactiveNotifier`;纯函数 `proactiveFrameOf(event)` 决定「哪些帧算她主动说的话、四个入参各取哪个键」。
|
||||||
|
- **构造即订阅,刻意不提供 start/stop**(理由写进 KDoc:唯一会想调 stop 的调用方是 VM,那正好把通路又绑回 Activity;留一个能忘调的 `start()` 只是多一种「通知整个哑掉且不报错」的坏法)。
|
||||||
|
- `AppContainer` 新增 `proactiveFrameIntake`;`SessionsViewModel.onEvent` 里那两处转交**删掉**;`ProactiveNotifier` / `AppPresence` **只改 KDoc**(判据、去重、`post` 签名零改动)。
|
||||||
|
- 登出**不需要**取消订阅:帧只有连接活着才可能到,而连接在 `LoggedOut` 与令牌失效两处都会断;去重账本刻意不清(换账号后同一条再推也不该弹第二遍)。
|
||||||
|
|
||||||
|
**验收**:全量真跑 **113 suites / 1370 tests / 0 failures / 0 errors**(基线 112/1361,±0 其它形状变化)。新增 `ProactiveFrameIntakeTest` 9 条,其中 `her words still notify when no activity is alive` 就是本条的现场。
|
||||||
|
|
||||||
|
**⚠️ 一个需要主人知道的副作用**:`AppContainer` 现在会在 `Application.onCreate` 时**提前把 `chatSocket` 建出来**(改前 `by lazy`,第一次被 VM 取用才建)。代价是进程启动多造一个 `OkHttpClient` 对象(线程池仍懒建)。若不接受,改法是把 intake 的参数换成 `() -> SharedFlow<WsEvent>` 保留懒性——**没做**,因为那会让「订阅时刻」与「连接建出时刻」分开,多一处要解释。
|
||||||
|
**另一处必要项**:`deliver` 里接住了落地异常(改前异常会打死 VM 那条 collect,但 VM 重建会自愈;现在它是唯一消费点,异常逃出去就是**进程崩溃 + 通知永久哑掉**,所以必须接住,只 DEBUG 打日志)。
|
||||||
|
|
||||||
|
**设备验收步骤(等长跑结束再做;⚠️ 装包必然杀进程)**:
|
||||||
|
```
|
||||||
|
adb root && adb reverse tcp:80 tcp:18090 # 注意顺序:adb root 会冲掉 reverse,要放在前面
|
||||||
|
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
# 打开 App → localhost 登录 → 授通知权限 → 设置页开「后台保持连接」
|
||||||
|
# 进主会话 → Home 退后台 → 最近任务里把卡片划掉(等 2~3s)
|
||||||
|
pidof top.yeij.cyrene # 记下 PID
|
||||||
|
dumpsys notification --noredact | grep keep_alive # 保活通知还在 = 进程确实被撑着
|
||||||
|
python devtools/fake-backend/proactive_inject.py --port 18090 --text "划掉之后也要弹"
|
||||||
|
dumpsys notification --noredact | grep 划掉之后 # 期望:有「昔涟 / 划掉之后也要弹」
|
||||||
|
pidof top.yeij.cyrene # 期望:与前面同一个 PID(证明是同一进程,不是重启后补弹)
|
||||||
|
```
|
||||||
|
**判据前提**(不满足则本次无效):注入**之前** PID 未变 **且** 保活通知还在;通知权限已授予。
|
||||||
|
|
||||||
|
### 2. 保活长跑第 3 次(假后端,基线 `22:16:08`)
|
||||||
|
|
||||||
|
主人 2026-10-06 晚授权「允许你切」,于是把 `tcp:80` 切回假后端 `18090` 重做。基线:前台=launcher(App 在后台)、**pid `31092`**、`KeepAliveService` **3 条**(`isForeground=true` / `foregroundId=1` / `channel=keep_alive` / `ONGOING_EVENT|NO_CLEAR|SILENT`)、假后端 `wsClients=1`。挂了后台任务:**30 分钟抽查**(22:46)+ **2 小时复查**(00:16,含注入一条 proactive 看后台弹不弹)。
|
||||||
|
|
||||||
|
**★ 顺带推翻了一条猜想**:锚点三十五 §7 猜「上次复查时进程不在」可能是**保活开关没开**。这次 App 一启动我就查了——**服务立刻就在跑**(开关是开的)。所以那条猜想**是错的**,上次进程消失是**别的**原因(可能是系统低内存强杀,也可能是被手动划掉——**仍未定**,别当结论)。
|
||||||
|
|
||||||
|
**当前转发**:`tcp:80 → tcp:18090`(假后端)、`tcp:10942 → tcp:10942`(真后端)。要回真后端改这一条即可。
|
||||||
Reference in new issue
Block a user