From 09f071e38f842375921dd8254aaf227e133996cc Mon Sep 17 00:00:00 2001 From: joywayer Date: Mon, 22 Jun 2026 19:02:35 +0800 Subject: [PATCH] =?UTF-8?q?Phase=202.C=EF=BC=9AExternalSubscriptions=20?= =?UTF-8?q?=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E7=AE=A1=E7=90=86=EF=BC=88?= =?UTF-8?q?=E9=98=B2=E5=8F=8C=E5=8F=91=E9=A2=84=E5=A4=87=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 Design §2.4.2 把 battery / network / appservice 三组外部事件源的订阅 挂钩与 VC 生命周期配对:viewWillAppear → setup,viewWillDisappear → teardown。配套约束:未来 Phase 6 子游戏 push 上来时大厅自动 teardown, 避免桥事件双发(栈深 ≤ 2,§2.4.3)。 WebContainerViewController 改造: - 新增 setupExternalSubscriptions(): * 幂等 start 3 个 monitor(NetworkMonitor / BatteryMonitor / AppLifecycleObserver) * 设 BatteryMonitor.onChange → evaluateJavaScript("window.app_getbattery=N") + bridge.call("getBattery", "%.2f") * 设 NetworkMonitor.onChange → evaluateJavaScript("window.app_getnetwork=N") + bridge.call("getnetwork", "1"/"2"/"3") * 设 AppLifecycleObserver.onBackground/Foreground → bridge.call("appservice", "1"/"2") - 新增 teardownExternalSubscriptions(): * 解绑 onChange / on{Background,Foreground} = nil * 释放对 bridge/webView 的 closure 引用避免子游戏 push 后双发 * monitor 不停(保持后台 currentXxx 状态新鲜),下次 setup 直接 resume - viewWillAppear / viewWillDisappear override 调 setup / teardown 配对 - writeAppDataFiles 精简:只写首次值 + 启动 NetworkMonitor/BatteryMonitor (需要 currentXxx 读首次值);AppLifecycleObserver 启动挪到 setup 至此 Phase 2 全部完成(2.A 8 个 handler + 2.B 5 项反向 callback + 2.C 生命周期管理)。 BuildProject 通过 Plan §5 Phase 2.13 勾选 Co-Authored-By: Claude Opus 4.7 --- docs/Development-Plan.md | 8 ++- .../WebView/WebContainerViewController.swift | 67 ++++++++++++++----- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/docs/Development-Plan.md b/docs/Development-Plan.md index aef0ab2..f51dcb9 100644 --- a/docs/Development-Plan.md +++ b/docs/Development-Plan.md @@ -372,9 +372,11 @@ Contract Design Plan(本文档) ##### 2.C 外部订阅生命周期 -- [ ] **2.13** 实现 Design §2.4.2 的 `ExternalSubscriptions` 模式 - - `viewWillAppear` resume / `viewWillDisappear` suspend - - 防双发:栈深 ≥ 2 时下层不发桥事件 +- [x] **2.13** 实现 Design §2.4.2 的外部订阅生命周期管理 + - `setupExternalSubscriptions()`(viewWillAppear 调):幂等启动 3 个 monitor + 设 onChange 闭包(battery / network 走 evaluateJavaScript + bridge.call 双轨;appservice 仅 bridge.call) + - `teardownExternalSubscriptions()`(viewWillDisappear 调):解绑 onChange / on{Background,Foreground} = nil,释放 bridge/webView 引用避免子游戏 push 后双发 + - `writeAppDataFiles` 精简为只写首次值 + 启动 NetworkMonitor/BatteryMonitor(读 currentXxx 用),AppLifecycleObserver 启动挪到 setup + - 单例 monitor 全局只有一个 closure 引用,未来 Phase 6 子游戏 setup 时会覆盖(大厅已 teardown 不会冲突,符合 §2.4.3 栈深 ≤ 2 约束) #### 验收 diff --git a/ylgamehall/Source/WebView/WebContainerViewController.swift b/ylgamehall/Source/WebView/WebContainerViewController.swift index 36fad07..5832054 100644 --- a/ylgamehall/Source/WebView/WebContainerViewController.swift +++ b/ylgamehall/Source/WebView/WebContainerViewController.swift @@ -75,11 +75,22 @@ public final class WebContainerViewController: UIViewController { public override var canBecomeFirstResponder: Bool { true } + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // 外部订阅生命周期:栈顶时挂钩,避免子游戏 push 上来后双发(§2.4.2) + setupExternalSubscriptions() + } + public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) becomeFirstResponder() } + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + teardownExternalSubscriptions() + } + public override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { shakeHandler.handleMotionEnded(motion) } @@ -216,10 +227,10 @@ public final class WebContainerViewController: UIViewController { ) } - /// 写 4 个 app_*.js 文件,并挂钩 battery / network / appservice 三组钩子: - /// 每次变化时同时(1)重写对应 .js 文件(下次 reload 读到新值) - /// 和(2)`bridge.call` 反向推送给当前页 H5(§3.2 [2][3][4])。 - /// 与原 msext NewRootVC.initJSdata + changebattery + changenetstate + appservice 等价。 + /// 写 4 个 app_*.js 文件首次值(loadFileURL 之前调一次,monitor 已启动)。 + /// 业务期变化的桥事件挂钩由 `setupExternalSubscriptions` / `teardownExternalSubscriptions` + /// 配对管理(详见 Design §2.4.2 ExternalSubscriptions 生命周期),避免大厅 push + /// 子游戏后两个 VC 都在监听导致 H5 收到双倍桥事件。 private func writeAppDataFiles(resolved: ResolvedVersion) throws { let writer = AppDataWriter( bundleConfig: .shared, @@ -227,27 +238,40 @@ public final class WebContainerViewController: UIViewController { containerRole: .lobby ) - // 启动 3 个 monitor(幂等:内部 started 标记) + // 启动 2 个 currentXxx 类 monitor(幂等)以便读首次值; + // AppLifecycleObserver 不需要 currentXxx,挪到 setup 启动 NetworkMonitor.shared.start() BatteryMonitor.shared.start() - AppLifecycleObserver.shared.start() // 首次写入:4 个文件全部落盘 try writer.writeInitial() try writer.writeBattery(BatteryMonitor.shared.currentLevel) try writer.writeNetwork(NetworkMonitor.shared.currentCode) + } + + // MARK: - 外部订阅生命周期(Phase 2.C / Design §2.4.2) + // + // 把 battery / network / appservice 三个外部事件源的订阅挂钩与 VC 生命周期 + // 配对:viewWillAppear → setup,viewWillDisappear → teardown。 + // 配套约束:未来 Phase 6 子游戏 push 上来时,大厅的 viewWillDisappear 会自动 + // 触发 teardown,避免桥事件双发(参 §2.4.3 栈深 ≤ 2)。 + // + // 业务期变化时**不重写文件**,改为直接 evaluateJavaScript 重新赋值 window.app_* + // 全局变量(H5 已加载完,