现象:大厅 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
172 lines
7.1 KiB
Swift
172 lines
7.1 KiB
Swift
//
|
||
// 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 array(WVJB 支持批处理)
|
||
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]) {
|
||
// 优先判 responseId(JS 响应 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) 不存在")
|
||
}
|
||
}
|
||
}
|
||
}
|