From ce3fbb7106f52afdac6ed282aba08ff7acfd20a0 Mon Sep 17 00:00:00 2001 From: joywayer Date: Tue, 23 Jun 2026 23:17:39 +0800 Subject: [PATCH] =?UTF-8?q?getWebdata=20=E5=8F=8D=E5=90=91=E8=B0=83?= =?UTF-8?q?=E7=94=A8=EF=BC=9A=E5=A4=A7=E5=8E=85=20observer=20=E6=94=B9?= =?UTF-8?q?=E6=B0=B8=E4=B9=85=E6=8C=82=E9=92=A9=EF=BC=8C=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=20msext=20=E5=8F=8C=E5=8F=91=E8=A1=8C=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前大厅 / 子游戏 observer 都 lifecycle-tied(viewWillAppear setup / viewWillDisappear teardown),sub-game 内 overlay backgameData pop 时 只有 sub-game H5 收到 getWebdata(大厅 observer 已 teardown)。这与 msext "NewRootVC observer 永久 + gameController observer 永久" 的双发行为不一致。 大厅 H5 可能依赖"sub-game 内 overlay 操作触发自身刷新",破坏契约。 修复: - WebContainerViewController.subGameReturnObserver 改"永久挂钩" - 加 subGameReturnObserverInstalled 标志,首次 setupExternalSubscriptions 时 install 一次,后续永不解绑(msext NewRootVC.m:593 等价) - teardownExternalSubscriptions 不再触碰它 - SubGameViewController.subGameReturnObserver 保持 lifecycle-tied - 子游戏自身 backgameData pop:sub-game 已 teardown → 仅大厅触发(单发,msext 等价) - 子游戏内 overlay backgameData pop:sub-game.viewWillAppear 重新 setup → 大厅 + sub-game 双触发(双发,msext 等价) Co-Authored-By: Claude Opus 4.7 --- .../WebView/SubGameViewController.swift | 9 +++- .../WebView/WebContainerViewController.swift | 53 +++++++++++++------ 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/ylgamehall/Source/WebView/SubGameViewController.swift b/ylgamehall/Source/WebView/SubGameViewController.swift index cc1d34a..d30c19a 100644 --- a/ylgamehall/Source/WebView/SubGameViewController.swift +++ b/ylgamehall/Source/WebView/SubGameViewController.swift @@ -342,8 +342,13 @@ public final class SubGameViewController: UIViewController { bridge.call("phonestate", data: .string(state), callback: nil) } - // overlay(Phase 7)pop 回子游戏时反向 callback getWebdata。挂在子游戏栈顶 - // 期间生效;overlay push 上来时 viewWillDisappear → teardown 自动解绑,避免双发。 + // overlay(Phase 7)pop 回子游戏时反向 callback getWebdata。**lifecycle-tied**: + // 子游戏栈顶时挂钩,overlay push / 子游戏自身 pop 时由 viewWillDisappear 解绑。 + // 这一行为与大厅 observer 的"永久挂钩"配合,复现 msext 双观察者行为: + // - 子游戏自身 backgameData pop 回大厅:sub-game observer 在 viewWillDisappear + // 已解绑 → 仅大厅 observer 触发 → 大厅 H5 收到 getWebdata(msext 单发等价) + // - 子游戏内 overlay backgameData pop:sub-game.viewWillAppear 时 observer 重挂 + // → 大厅 + sub-game 双触发 → 大厅 H5 + sub-game H5 双发收到(msext 双观察者等价) subGameReturnObserver = NotificationCenter.default.addObserver( forName: .subGameDidReturn, object: nil, diff --git a/ylgamehall/Source/WebView/WebContainerViewController.swift b/ylgamehall/Source/WebView/WebContainerViewController.swift index 217893b..27556df 100644 --- a/ylgamehall/Source/WebView/WebContainerViewController.swift +++ b/ylgamehall/Source/WebView/WebContainerViewController.swift @@ -23,8 +23,12 @@ public final class WebContainerViewController: UIViewController { private let shakeHandler = ShakeHandler() // MARK: - .subGameDidReturn 观察者(子游戏 backgameData pop 后回大厅,反向 callback getWebdata) + // + // 与 msext NewRootVC.m:593 等价:永久挂钩,viewDidLoad 期间一次性 install, + // 不随 viewWillDisappear teardown。详见 setupExternalSubscriptions 注释。 private var subGameReturnObserver: NSObjectProtocol? + private var subGameReturnObserverInstalled = false // MARK: - Boot pipeline 内部错误分类 // @@ -447,18 +451,35 @@ public final class WebContainerViewController: UIViewController { bridge.call("appservice", data: .string("2"), callback: nil) } - // §3.2 [12] getWebdata — 子游戏 pop 回大厅,反向把 backgameData 入参透传给 H5 - // (msext NewRootVC 监听 "backgameDatatwo" 通知 → callHandler:"getWebdata" 等价) - // 大厅栈顶时挂钩,pop 进子游戏时由 viewWillDisappear teardown 解绑,避免双发。 - subGameReturnObserver = NotificationCenter.default.addObserver( - forName: .subGameDidReturn, - object: nil, - queue: .main - ) { note in - // queue=.main 保证 closure 在主线程;显式 hop 让 Swift 6 actor 推断通过 - let data = (note.userInfo?["data"] as? String) ?? "" - MainActor.assumeIsolated { - bridge.call("getWebdata", data: .string(data), callback: nil) + // §3.2 [12] getWebdata — 反向把 backgameData / 弹层 data 入参透传给大厅 H5。 + // + // 与 msext NewRootVC.m:593 等价:observer 在 viewDidLoad 期间一次性挂钩, + // **永久 listen,不随 viewWillDisappear teardown**。这样: + // + // 1. 子游戏 backgameData 自己 pop 回大厅: + // BackGameDataHandler → AppCoordinator.popSubGame → popViewController + // sub-game.viewWillDisappear → sub-game 的 observer 解绑(sub-game 走 lifecycle-tied) + // 再 post → 仅大厅 observer 触发 → 大厅 H5 收到 getWebdata(msext 单发等价) + // + // 2. 子游戏内弹层 backgameData pop(弹层 push 自 sub-game): + // OverlayViewController → AppCoordinator.popOverlay → popViewController + // overlay.viewWillDisappear → sub-game.viewWillAppear → sub-game observer 重新挂钩 + // 再 post → 大厅 + sub-game observer 同时触发 → 大厅 H5 + sub-game H5 双发收到 + // (msext NewRootVC + gameController 双观察者等价行为) + // + // 用 installed 标志保证多次 viewWillAppear 不重复挂钩(避免重复触发)。 + if !subGameReturnObserverInstalled { + subGameReturnObserverInstalled = true + subGameReturnObserver = NotificationCenter.default.addObserver( + forName: .subGameDidReturn, + object: nil, + queue: .main + ) { note in + // queue=.main 保证 closure 在主线程;显式 hop 让 Swift 6 actor 推断通过 + let data = (note.userInfo?["data"] as? String) ?? "" + MainActor.assumeIsolated { + bridge.call("getWebdata", data: .string(data), callback: nil) + } } } } @@ -469,15 +490,13 @@ public final class WebContainerViewController: UIViewController { // 单例 monitor 全局只有一个 closure 引用,子游戏 setup 时会覆盖; // 大厅 viewWillDisappear 时 teardown 释放对当前 bridge/webView 的引用 // 避免双发。 + // + // subGameReturnObserver 是永久挂钩(与 msext NewRootVC observer 等价), + // **不在此 teardown** —— 详见 setupExternalSubscriptions 注释。 BatteryMonitor.shared.onChange = nil NetworkMonitor.shared.onChange = nil AppLifecycleObserver.shared.onBackground = nil AppLifecycleObserver.shared.onForeground = nil - - if let token = subGameReturnObserver { - NotificationCenter.default.removeObserver(token) - subGameReturnObserver = nil - } } // MARK: - Alerts (terminal / retry states)