From 3f8b382ad33b82118b0b7f1ed902466d9ab90e1f Mon Sep 17 00:00:00 2001 From: joywayer Date: Fri, 7 Aug 2026 01:54:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E6=A1=A5/=E5=AE=9A=E4=BD=8D=E8=BE=B9?= =?UTF-8?q?=E7=95=8C=E8=AF=8A=E6=96=AD=E6=97=A5=E5=BF=97=EF=BC=8C=E6=8E=92?= =?UTF-8?q?=E6=9F=A5=E5=AD=90=E6=B8=B8=E6=88=8F=E6=94=B6=E4=B8=8D=E5=88=B0?= =?UTF-8?q?=20getlocationinfo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:大厅 H5 能正确拿到定位,子游戏拿不到。已静态排除「子游戏未注册 handler」与「回调错发给大厅」两个猜测(BridgeBus 是 per-WebView 实例、 handlers 是实例状态,反向 call 捕获各自的 bridge,物理上不可能串台; 下载真实子游戏 zip 比对后确认 H5 侧大厅/子游戏逻辑完全对称)。 为定位真正的失败层,在各组件边界加日志(纯诊断,不改任何行为): - BridgeBus 加 label(lobby / subGame)区分来源,记录 ← H5 调用、 → H5 反向 call、native handler 未注册、evaluateJavaScript 失败 - sendToJS 改为返回 'ok'/'no-bridge',H5 侧 bridge 未就绪导致的静默丢包 现在会打错误日志(原实现 `if (window.X)` 直接丢弃,完全不可见) - LocationService 记录 requestOnce 序号 / shared manager id / requestLocation 的 BOOL 返回值 / completionBlock 是否回来 / stop() 调用 —— 高德文档明确 requestLocation 返回 NO 时 completionBlock 永不调用, 当前代码忽略该返回值会让 continuation 永挂,H5 连 errorCode 12 都收不到 - H5ErrorRelay 补 console.warn 中继,让 WVJB「H5 侧无对应 handler」的 warn 能出现在 Xcode console 契约影响:无。仅新增日志与可选 label 参数,桥接接口名 / 字段名 / 数据结构 均未变动(docs/H5-Native-Contract.md §3.1[20]/ §3.2[6]不变)。 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UMPCLfsuxvwgMotzsb67QH --- ylgamehall/Source/Bridge/BridgeBus.swift | 43 ++++++++++++++++--- ylgamehall/Source/Bridge/H5ErrorRelay.swift | 14 ++++++ .../Bridge/Handlers/BackGameDataHandler.swift | 2 +- .../Handlers/StartLocationHandler.swift | 17 ++++++-- .../Source/Location/LocationService.swift | 25 +++++++++-- .../Source/WebView/BridgedWebView.swift | 7 ++- .../WebView/SubGameViewController.swift | 4 +- .../WebView/WebContainerViewController.swift | 4 +- 8 files changed, 97 insertions(+), 19 deletions(-) diff --git a/ylgamehall/Source/Bridge/BridgeBus.swift b/ylgamehall/Source/Bridge/BridgeBus.swift index bd9c57a..8c5479b 100644 --- a/ylgamehall/Source/Bridge/BridgeBus.swift +++ b/ylgamehall/Source/Bridge/BridgeBus.swift @@ -8,6 +8,11 @@ import Foundation import WebKit +import os.log + +/// 桥诊断日志。文件级 `let`(Logger 是 Sendable)以便在 evaluateJavaScript +/// 完成回调等非 MainActor 上下文里也能直接用。 +private let bridgeLog = Logger(subsystem: "ylgamehall", category: "Bridge") /// H5 ↔ Native 消息总线。 /// @@ -24,14 +29,21 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler { /// JS 端注入的全局桥对象名(约定 `window.WebViewJavascriptBridge`)。 public static let jsBridgeName = "WebViewJavascriptBridge" + /// 容器标识(`lobby` / `subGame` / …),仅用于诊断日志区分是哪个 WebView 的桥。 + /// 不参与任何契约行为。 + public let label: String + private weak var webView: WKWebView? private var handlers: [String: BridgeHandler] = [:] private var pendingCallbacks: [String: BridgeCallback] = [:] private var nativeCallbackCounter: UInt64 = 0 /// 构造时绑定 WebView + 用户内容控制器,自动注册 `WVJBHandler` 消息处理器。 - public init(webView: WKWebView, controller: WKUserContentController) { + public init(webView: WKWebView, + controller: WKUserContentController, + label: String = "webview") { self.webView = webView + self.label = label super.init() controller.add(self, name: Self.scriptMessageHandlerName) } @@ -40,6 +52,7 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler { public func register(_ name: String, handler: @escaping BridgeHandler) { handlers[name] = handler + bridgeLog.debug("[\(self.label, privacy: .public)] register handler '\(name, privacy: .public)'") } public func call(_ name: String, data: BridgeData?, callback: BridgeCallback?) { @@ -53,6 +66,7 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler { pendingCallbacks[cbId] = callback payload["callbackId"] = cbId } + bridgeLog.debug("[\(self.label, privacy: .public)] → H5 callHandler '\(name, privacy: .public)'") sendToJS(payload: payload) } @@ -95,8 +109,10 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler { // 构造一个回调闭包,handler 调用即把 responseData 异步派回 JS let responseCallback: BridgeCallback? = makeResponseCallback(for: callbackId) + bridgeLog.debug("[\(self.label, privacy: .public)] ← H5 call '\(name, privacy: .public)' data=\(String(describing: msg["data"]), privacy: .public)") + guard let handler = handlers[name] else { - print("[BridgeBus] no handler registered for '\(name)'") + bridgeLog.error("[\(self.label, privacy: .public)] no NATIVE handler registered for '\(name, privacy: .public)'") responseCallback?(nil) return } @@ -125,16 +141,31 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler { } private func sendToJS(payload: [String: Any]) { - guard let webView else { return } + guard let webView else { + bridgeLog.error("[\(self.label, privacy: .public)] sendToJS 丢弃:webView 已释放") + return + } guard let data = try? JSONSerialization.data(withJSONObject: payload), let json = String(data: data, encoding: .utf8) else { - print("[BridgeBus] JSON encode failed for payload: \(payload)") + bridgeLog.error("[\(self.label, privacy: .public)] JSON encode failed for payload: \(String(describing: payload), privacy: .public)") return } // base64 包装避免 JSON 内单引号 / 反斜杠扰乱 JS 字符串字面量 let base64 = Data(json.utf8).base64EncodedString() - let js = "if (window.\(Self.jsBridgeName)) window.\(Self.jsBridgeName)._handleMessageFromObjC('\(base64)');" - webView.evaluateJavaScript(js, completionHandler: nil) + // 返回 'ok' / 'no-bridge' 以便诊断「消息被静默丢弃」(H5 侧 bridge 未就绪) + let js = """ + (function(){ if (!window.\(Self.jsBridgeName)) { return 'no-bridge'; } \ + window.\(Self.jsBridgeName)._handleMessageFromObjC('\(base64)'); return 'ok'; })() + """ + let tag = label + let what = (payload["handlerName"] as? String) ?? (payload["responseId"] as? String) ?? "?" + webView.evaluateJavaScript(js) { result, error in + if let error { + bridgeLog.error("[\(tag, privacy: .public)] sendToJS '\(what, privacy: .public)' evaluateJavaScript 失败: \(error.localizedDescription, privacy: .public)") + } else if (result as? String) != "ok" { + bridgeLog.error("[\(tag, privacy: .public)] sendToJS '\(what, privacy: .public)' 被丢弃:H5 侧 window.\(Self.jsBridgeName) 不存在") + } + } } } diff --git a/ylgamehall/Source/Bridge/H5ErrorRelay.swift b/ylgamehall/Source/Bridge/H5ErrorRelay.swift index 0624363..1e4a2e5 100644 --- a/ylgamehall/Source/Bridge/H5ErrorRelay.swift +++ b/ylgamehall/Source/Bridge/H5ErrorRelay.swift @@ -48,6 +48,11 @@ public final class H5ErrorRelay: NSObject { case "console.error": let args = (dict["args"] as? [String]) ?? [] print("[H5 console.error]", args.joined(separator: " ")) + case "console.warn": + // 桥诊断需要:WebViewJavascriptBridge.js 在「Native 发来的 handlerName + // H5 侧没注册」时走 console.warn,之前不上报导致这类丢包完全不可见。 + let args = (dict["args"] as? [String]) ?? [] + print("[H5 console.warn]", args.joined(separator: " ")) case "onerror": let msg = (dict["msg"] as? String) ?? "" let src = (dict["src"] as? String) ?? "" @@ -80,6 +85,15 @@ public final class H5ErrorRelay: NSObject { } catch(e){} if (origErr) { origErr.apply(console, arguments); } }; + var origWarn = console.warn; + console.warn = function(){ + try { + var args = []; + for (var i=0; i LocationPayload { + public func requestOnce(caller: String = "?") async throws -> LocationPayload { #if canImport(AMapLocationKit) + requestSeq += 1 + let seq = requestSeq + locLog.debug("[\(caller, privacy: .public)] requestOnce #\(seq, privacy: .public) 发起(shared manager id=\(String(describing: ObjectIdentifier(self.manager)), privacy: .public))") return try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in - manager.requestLocation(withReGeocode: true) { loc, regeo, err in + let accepted = manager.requestLocation(withReGeocode: true) { loc, regeo, err in + locLog.debug("[\(caller, privacy: .public)] requestOnce #\(seq, privacy: .public) completionBlock 回来:loc=\(loc != nil, privacy: .public) regeo=\(regeo != nil, privacy: .public) err=\(err?.localizedDescription ?? "nil", privacy: .public)") if let err = err as NSError? { // msext 行为:权限相关错误归一到 12 errorCode(handler 转字段) if err.code == AMapLocationErrorCode.locateFailed.rawValue { @@ -89,15 +100,23 @@ public final class LocationService { street: regeo.street ?? "" )) } + // ⚠️ AMapLocationManager.h:「是否成功添加单次定位Request」。返回 NO 时 + // completionBlock 永远不会被调用 → continuation 永挂 → H5 收不到任何 + // getlocationinfo(连 errorCode 12 都没有)。诊断阶段先只记录。 + locLog.debug("[\(caller, privacy: .public)] requestOnce #\(seq, privacy: .public) requestLocation 返回 accepted=\(accepted, privacy: .public)") } #else + locLog.error("[\(caller, privacy: .public)] requestOnce: AMapLocationKit 未链接,抛 sdkNotLinked") throw LocationError.sdkNotLinked #endif } /// 停止:msext gameController.m:2473-2478 cleanUpAction 等价 - public func stop() { + public func stop(caller: String = "?") { #if canImport(AMapLocationKit) + // ⚠️ AMapLocationManager.h:stopUpdatingLocation「会 cancel 掉所有的单次定位请求」。 + // 本类是 shared 单例、manager 唯一,因此这里会连带取消**其它容器**在飞的单次定位。 + locLog.debug("[\(caller, privacy: .public)] stop():stopUpdatingLocation + delegate=nil(会取消所有在飞的单次定位)") manager.stopUpdatingLocation() manager.delegate = nil #endif diff --git a/ylgamehall/Source/WebView/BridgedWebView.swift b/ylgamehall/Source/WebView/BridgedWebView.swift index 7670d86..d218463 100644 --- a/ylgamehall/Source/WebView/BridgedWebView.swift +++ b/ylgamehall/Source/WebView/BridgedWebView.swift @@ -17,7 +17,9 @@ public final class BridgedWebView: UIView { public let webView: WKWebView public let bridge: BridgeBus - public init() { + /// - Parameter label: 容器标识(`lobby` / `subGame`),仅用于桥诊断日志区分来源, + /// 不参与任何契约行为。 + public init(label: String = "webview") { // ── WKWebViewConfiguration(契约 §4.1)───────────────── let configuration = WKWebViewConfiguration() configuration.defaultWebpagePreferences.allowsContentJavaScript = true @@ -71,7 +73,8 @@ public final class BridgedWebView: UIView { } #endif let bridge = BridgeBus(webView: webView, - controller: configuration.userContentController) + controller: configuration.userContentController, + label: label) self.webView = webView self.bridge = bridge diff --git a/ylgamehall/Source/WebView/SubGameViewController.swift b/ylgamehall/Source/WebView/SubGameViewController.swift index 8e1a29a..e987427 100644 --- a/ylgamehall/Source/WebView/SubGameViewController.swift +++ b/ylgamehall/Source/WebView/SubGameViewController.swift @@ -33,7 +33,7 @@ public final class SubGameViewController: UIViewController { // MARK: - UI - private let bridgedWebView = BridgedWebView() + private let bridgedWebView = BridgedWebView(label: "subGame") private let splash = SplashOverlay() // MARK: - Handlers @@ -88,7 +88,7 @@ public final class SubGameViewController: UIViewController { DeviceInfoHandler.register(on: bridge) BrowserHandler.register(on: bridge) OpenSaomaHandler.register(on: bridge) - StartLocationHandler.register(on: bridge) + StartLocationHandler.register(on: bridge, label: "subGame") // assetsRoot 闭包 = 子游戏 H5 根目录(msext gameController.m:364 等价)。 // 用 closure 是因为 effectiveGameDir 在 boot pipeline 升级路径下会变化, diff --git a/ylgamehall/Source/WebView/WebContainerViewController.swift b/ylgamehall/Source/WebView/WebContainerViewController.swift index e975d72..a849fe9 100644 --- a/ylgamehall/Source/WebView/WebContainerViewController.swift +++ b/ylgamehall/Source/WebView/WebContainerViewController.swift @@ -15,7 +15,7 @@ public final class WebContainerViewController: UIViewController { // MARK: - UI - private let bridgedWebView = BridgedWebView() + private let bridgedWebView = BridgedWebView(label: "lobby") private let splash = SplashOverlay() // MARK: - Handlers (有状态的 handler 注册器持有,无状态的走 enum 静态 register) @@ -77,7 +77,7 @@ public final class WebContainerViewController: UIViewController { DeviceInfoHandler.register(on: bridge) // §3.1 [21] + §3.2 [1] BrowserHandler.register(on: bridge) // §3.1 [16] OpenSaomaHandler.register(on: bridge) // §3.1 [22]空 stub - StartLocationHandler.register(on: bridge) // §3.1 [20]Phase 5 完整实现,Phase 2 stub + StartLocationHandler.register(on: bridge, label: "lobby") // §3.1 [20]Phase 5 完整实现,Phase 2 stub // Phase 3.A 本地音频 + 3.C/3.D stub // assetsRoot 闭包 = 大厅 H5 根目录(lobbyIndex 父目录),lobby 不做升级