Files
youle_app_ios_v2/ylgamehall/Source/Bridge/BridgeBus.swift
T
joywayerandClaude Opus 5 3f8b382ad3 加桥/定位边界诊断日志,排查子游戏收不到 getlocationinfo
现象:大厅 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMPCLfsuxvwgMotzsb67QH
2026-08-07 01:54:18 +08:00

172 lines
7.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// BridgeBus.swift
// ylgamehall
//
// BridgeProtocol 的具体实现 + WKScriptMessageHandler 适配。
// 详见 docs/H5-Native-Implementation-Design.md §3.3。
//
import Foundation
import WebKit
import os.log
/// 桥诊断日志。文件级 `let`Logger 是 Sendable)以便在 evaluateJavaScript
/// 完成回调等非 MainActor 上下文里也能直接用。
private let bridgeLog = Logger(subsystem: "ylgamehall", category: "Bridge")
/// H5 ↔ Native 消息总线。
///
/// **运行时约束**:必须挂在 WKWebView 上、@MainActor 单线程访问。
/// **生命周期**:构造时自动向 `WKUserContentController` 注册 `WVJBHandler` 通道,
/// 销毁时由 WebView 链路自然释放(无需手动 remove)。
@MainActor
public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
/// JS → Native 的 WKScriptMessageHandler 通道名。
/// JS 端通过 `window.webkit.messageHandlers.WVJBHandler.postMessage({...})` 发送。
public static let scriptMessageHandlerName = "WVJBHandler"
/// 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,
label: String = "webview") {
self.webView = webView
self.label = label
super.init()
controller.add(self, name: Self.scriptMessageHandlerName)
}
// MARK: - BridgeProtocol
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?) {
var payload: [String: Any] = ["handlerName": name]
if let data {
payload["data"] = data.jsonObject
}
if let callback {
nativeCallbackCounter += 1
let cbId = "objc_cb_\(nativeCallbackCounter)"
pendingCallbacks[cbId] = callback
payload["callbackId"] = cbId
}
bridgeLog.debug("[\(self.label, privacy: .public)] → H5 callHandler '\(name, privacy: .public)'")
sendToJS(payload: payload)
}
// MARK: - WKScriptMessageHandler
public func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
// body 可能是单条 dict 或一批 dict arrayWVJB 支持批处理)
if let batch = message.body as? [[String: Any]] {
for msg in batch { handleIncoming(msg) }
} else if let single = message.body as? [String: Any] {
handleIncoming(single)
} else {
print("[BridgeBus] unrecognized message body type: \(type(of: message.body))")
}
}
// MARK: - 私有
private func handleIncoming(_ msg: [String: Any]) {
// 优先判 responseIdJS 响应 Native 早前的 callHandler
if let responseId = msg["responseId"] as? String {
let respData = msg["responseData"].flatMap { BridgeData(jsonObject: $0) }
if let cb = pendingCallbacks.removeValue(forKey: responseId) {
cb(respData)
} else {
print("[BridgeBus] stale responseId: \(responseId)")
}
return
}
// 否则是 JS → Native handler 调用
guard let name = msg["handlerName"] as? String else {
print("[BridgeBus] message missing handlerName: \(msg)")
return
}
let data = msg["data"].flatMap { BridgeData(jsonObject: $0) }
let callbackId = msg["callbackId"] as? String
// 构造一个回调闭包,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 {
bridgeLog.error("[\(self.label, privacy: .public)] no NATIVE handler registered for '\(name, privacy: .public)'")
responseCallback?(nil)
return
}
// handler 是 async,独立 Task 内 await;本方法保持 sync 返回
Task {
await handler(data, responseCallback)
}
}
private func makeResponseCallback(for callbackId: String?) -> BridgeCallback? {
guard let id = callbackId else { return nil }
return { [weak self] respData in
Task { @MainActor in
self?.sendResponseToJS(responseId: id, data: respData)
}
}
}
private func sendResponseToJS(responseId: String, data: BridgeData?) {
var payload: [String: Any] = ["responseId": responseId]
if let data {
payload["responseData"] = data.jsonObject
}
sendToJS(payload: payload)
}
private func sendToJS(payload: [String: Any]) {
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 {
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()
// 返回 '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) 不存在")
}
}
}
}