修复定位的回调问题
This commit is contained in:
@@ -14,6 +14,70 @@ import os.log
|
||||
/// 完成回调等非 MainActor 上下文里也能直接用。
|
||||
private let bridgeLog = Logger(subsystem: "ylgamehall", category: "Bridge")
|
||||
|
||||
/// ── 临时诊断输出(排查「子游戏收不到 getlocationinfo」,结束后整体移除)──
|
||||
///
|
||||
/// 真机上 `Logger.debug` 会被系统日志级别过滤掉:devicectl console / Xcode
|
||||
/// console 都只能看到 `.error` 及以上。`print` 直写 stderr 必达,所以诊断期
|
||||
/// 统一走这里;带毫秒时间戳以便与 CoreLocation 的 os_log 行对齐时序。
|
||||
/// 同时落盘到 `Documents/diag.log`:用 Xcode 跑真机时 print 只进 Xcode console,
|
||||
/// 外部拿不到;落盘后可用
|
||||
/// xcrun devicectl device copy from --domain-type appDataContainer \
|
||||
/// --domain-identifier com.skyapp.ylgamehall --source Documents/diag.log ...
|
||||
/// 把整轮日志取出来分析。
|
||||
nonisolated func diagLog(_ message: String) {
|
||||
let line = "[DIAG \(diagTimestamp())] \(message)"
|
||||
print(line)
|
||||
diagFileSink.append(line)
|
||||
}
|
||||
|
||||
private nonisolated let diagFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "HH:mm:ss.SSS"
|
||||
return f
|
||||
}()
|
||||
|
||||
private nonisolated func diagTimestamp() -> String {
|
||||
diagFormatter.string(from: Date())
|
||||
}
|
||||
|
||||
/// 串行写入 `Documents/diag.log`。每个进程首次写入前插一行 session 分隔,
|
||||
/// 便于区分多次启动。纯诊断设施,排查结束随 diagLog 一并移除。
|
||||
// 工程默认 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor,需显式 nonisolated 才能在
|
||||
// @Sendable 桥回调 / evaluateJavaScript 完成块里同步调用。
|
||||
private nonisolated final class DiagFileSink: @unchecked Sendable {
|
||||
private let queue = DispatchQueue(label: "ylgamehall.diag.log")
|
||||
private let url: URL
|
||||
private var wroteHeader = false
|
||||
|
||||
init() {
|
||||
let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
url = dir.appendingPathComponent("diag.log")
|
||||
}
|
||||
|
||||
func append(_ line: String) {
|
||||
queue.async { [self] in
|
||||
var text = line + "\n"
|
||||
if !wroteHeader {
|
||||
wroteHeader = true
|
||||
let stamp = ISO8601DateFormatter().string(from: Date())
|
||||
text = "\n===== session \(stamp) pid=\(ProcessInfo.processInfo.processIdentifier) =====\n" + text
|
||||
}
|
||||
guard let data = text.data(using: .utf8) else { return }
|
||||
let fm = FileManager.default
|
||||
if !fm.fileExists(atPath: url.path) {
|
||||
fm.createFile(atPath: url.path, contents: nil)
|
||||
}
|
||||
guard let handle = try? FileHandle(forWritingTo: url) else { return }
|
||||
handle.seekToEndOfFile()
|
||||
handle.write(data)
|
||||
try? handle.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated let diagFileSink = DiagFileSink()
|
||||
|
||||
|
||||
/// H5 ↔ Native 消息总线。
|
||||
///
|
||||
/// **运行时约束**:必须挂在 WKWebView 上、@MainActor 单线程访问。
|
||||
@@ -38,6 +102,18 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
|
||||
private var pendingCallbacks: [String: BridgeCallback] = [:]
|
||||
private var nativeCallbackCounter: UInt64 = 0
|
||||
|
||||
/// 桥就绪前的消息暂存队列。等价 msext `WebViewJavascriptBridgeBase.startupMessageQueue`:
|
||||
/// `_queueMessage` 在 bridge 尚未 loaded 时入队,`injectJavascriptFile` 时按序 flush。
|
||||
///
|
||||
/// 早前的实现在 `window.WebViewJavascriptBridge` 不存在时**直接丢弃**消息 ——
|
||||
/// 真机日志里每次启动都有一条 `sendToJS 'getnetwork' 未送达 H5:no-bridge`,
|
||||
/// msext 里这条是会补发到 H5 的。子游戏 WebView 是全新建的、H5 很早就调
|
||||
/// `startlocation`,反向 `getlocationinfo` 落在这个窗口里就会被静默吞掉。
|
||||
private var startupMessageQueue: [[String: Any]] = []
|
||||
|
||||
/// H5 侧 WVJB.js 初始化完成后会 postMessage `{bridgeReady:true}`,收到即置 true 并 flush。
|
||||
private var bridgeReady = false
|
||||
|
||||
/// 构造时绑定 WebView + 用户内容控制器,自动注册 `WVJBHandler` 消息处理器。
|
||||
public init(webView: WKWebView,
|
||||
controller: WKUserContentController,
|
||||
@@ -52,7 +128,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)'")
|
||||
diagLog("[\(self.label)] register handler '\(name)'")
|
||||
}
|
||||
|
||||
public func call(_ name: String, data: BridgeData?, callback: BridgeCallback?) {
|
||||
@@ -66,7 +142,10 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
|
||||
pendingCallbacks[cbId] = callback
|
||||
payload["callbackId"] = cbId
|
||||
}
|
||||
bridgeLog.debug("[\(self.label, privacy: .public)] → H5 callHandler '\(name, privacy: .public)'")
|
||||
// 诊断:打出实际序列化后的字节,确认字段集合 / 类型 / 引号与 msext 一致
|
||||
let wire = (try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]))
|
||||
.flatMap { String(data: $0, encoding: .utf8) } ?? "<encode-failed>"
|
||||
diagLog("[\(self.label)] → H5 callHandler '\(name)' wire=\(wire)")
|
||||
sendToJS(payload: payload)
|
||||
}
|
||||
|
||||
@@ -87,6 +166,27 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
|
||||
// MARK: - 私有
|
||||
|
||||
private func handleIncoming(_ msg: [String: Any]) {
|
||||
// H5 侧 WVJB.js 初始化完成信号(等价 msext 的 `__bridge_loaded__`)。
|
||||
// 每次导航都会重新注入 user script,因此会重复收到;每次都要重新 flush。
|
||||
if msg["bridgeReady"] as? Bool == true {
|
||||
bridgeReady = true
|
||||
let queued = startupMessageQueue
|
||||
startupMessageQueue.removeAll()
|
||||
if !queued.isEmpty {
|
||||
diagLog("[\(self.label)] 桥就绪,flush \(queued.count) 条暂存消息")
|
||||
}
|
||||
for payload in queued { dispatchToJS(payload: payload) }
|
||||
return
|
||||
}
|
||||
|
||||
// 诊断上报:H5 侧派发结果(有没有对应 handler、handler 有没有抛错)
|
||||
if let dispatched = msg["diagDispatched"] as? String {
|
||||
let had = msg["hadHandler"] as? Bool ?? false
|
||||
let threw = msg["threw"] as? String
|
||||
diagLog("[\(self.label)] H5 派发 '\(dispatched)':hadHandler=\(had) threw=\(threw ?? "nil")")
|
||||
return
|
||||
}
|
||||
|
||||
// 优先判 responseId(JS 响应 Native 早前的 callHandler)
|
||||
if let responseId = msg["responseId"] as? String {
|
||||
let respData = msg["responseData"].flatMap { BridgeData(jsonObject: $0) }
|
||||
@@ -109,11 +209,19 @@ 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)")
|
||||
diagLog("[\(self.label)] ← H5 call '\(name)' data=\(String(describing: msg["data"]))")
|
||||
|
||||
guard let handler = handlers[name] else {
|
||||
bridgeLog.error("[\(self.label, privacy: .public)] no NATIVE handler registered for '\(name, privacy: .public)'")
|
||||
responseCallback?(nil)
|
||||
// msext `WebViewJavascriptBridgeBase.flushMessageQueue`:
|
||||
// if (!handler) { NSLog(@"WVJBNoHandlerException, ..."); continue; }
|
||||
// `continue` 意味着 **responseCallback 一次都不会被调用**。
|
||||
//
|
||||
// 早前这里调了 `responseCallback?(nil)`,会给 H5 回一条 {responseId:...}。
|
||||
// 现网 H5 的 `Func.getlocation()` 就是带 callback 调 `getlocationinfo` 的
|
||||
// (`bridge.callHandler('getlocationinfo',"",function(resp){})`,而
|
||||
// `getlocationinfo` 两边都没有对应的原生 handler),于是这条在原工程里
|
||||
// 永不触发的 JS 回调在我们这儿会被触发一次 —— 链条不一致。改为不回。
|
||||
diagLog("[\(self.label)] no NATIVE handler registered for '\(name)'(msext 同款:不回 responseCallback)")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -140,31 +248,41 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
|
||||
sendToJS(payload: payload)
|
||||
}
|
||||
|
||||
/// 等价 msext `WebViewJavascriptBridgeBase._queueMessage:`:
|
||||
/// 桥未就绪则入 `startupMessageQueue`,就绪则直接派发。
|
||||
private func sendToJS(payload: [String: Any]) {
|
||||
guard bridgeReady else {
|
||||
let what = (payload["handlerName"] as? String) ?? (payload["responseId"] as? String) ?? "?"
|
||||
diagLog("[\(self.label)] '\(what)' 桥未就绪 → 入队(msext startupMessageQueue 等价,队列长 \(startupMessageQueue.count + 1))")
|
||||
startupMessageQueue.append(payload)
|
||||
return
|
||||
}
|
||||
dispatchToJS(payload: payload)
|
||||
}
|
||||
|
||||
/// 等价 msext `_dispatchMessage:`:序列化 → 注入 `_handleMessageFromObjC`。
|
||||
/// H5 侧派发是 `setTimeout` 异步的(msext dispatchMessagesWithTimeoutSafety=true),
|
||||
/// 因此 evaluateJavaScript 的返回值**不代表** H5 handler 是否执行,不要据此判断送达。
|
||||
private func dispatchToJS(payload: [String: Any]) {
|
||||
guard let webView else {
|
||||
bridgeLog.error("[\(self.label, privacy: .public)] sendToJS 丢弃:webView 已释放")
|
||||
diagLog("[\(self.label)] dispatchToJS 丢弃: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)")
|
||||
diagLog("[\(self.label)] JSON encode failed for payload: \(String(describing: payload))")
|
||||
return
|
||||
}
|
||||
// base64 包装避免 JSON 内单引号 / 反斜杠扰乱 JS 字符串字面量
|
||||
// (msext 是逐个 escape 反斜杠/引号/换行/U+2028/U+2029,等价)
|
||||
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 js = "window.\(Self.jsBridgeName)._handleMessageFromObjC('\(base64)');"
|
||||
let tag = label
|
||||
let what = (payload["handlerName"] as? String) ?? (payload["responseId"] as? String) ?? "?"
|
||||
webView.evaluateJavaScript(js) { result, error in
|
||||
webView.evaluateJavaScript(js) { _, 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) 不存在")
|
||||
diagLog("[\(tag)] dispatchToJS '\(what)' evaluateJavaScript 失败: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user