加桥/定位边界诊断日志,排查子游戏收不到 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
This commit is contained in:
joywayer
2026-08-07 01:54:18 +08:00
co-authored by Claude Opus 5
parent 8a7d543cdc
commit 3f8b382ad3
8 changed files with 97 additions and 19 deletions
+37 -6
View File
@@ -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) 不存在")
}
}
}
}
@@ -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<arguments.length; i++) { args.push(String(arguments[i])); }
send({ kind:'console.warn', args: args });
} catch(e){}
if (origWarn) { origWarn.apply(console, arguments); }
};
window.addEventListener('error', function(ev){
send({
kind:'onerror',
@@ -57,7 +57,7 @@ public enum BackGameDataHandler {
#if canImport(AMapLocationKit)
// msext gameController.m:2473-2478 cleanUpAction
LocationService.shared.stop()
LocationService.shared.stop(caller: "backgameData")
#endif
// Phase 3.C / Phase 3.B QiniuUploader.cancelInFlight
@@ -17,20 +17,29 @@
//
import Foundation
import os.log
// SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor let MainActor
// handler @Sendable nonisolatedLogger Sendable
private nonisolated let startLocLog = Logger(subsystem: "ylgamehall", category: "Location")
public enum StartLocationHandler {
public static func register(on bridge: any BridgeProtocol) {
bridge.register("startlocation") { _, callback in
/// - Parameter label: `lobby` / `subGame`
public static func register(on bridge: any BridgeProtocol, label: String = "?") {
bridge.register("startlocation") { data, callback in
// msext cb
callback?(.string("startlocation"))
startLocLog.debug("[\(label, privacy: .public)] ← startlocation 入参 type=\(data?.asLooseString ?? "nil", privacy: .public)")
#if canImport(AMapLocationKit)
// SDK callback 9
// data == 1 Phase 5.4
Task { @MainActor in
do {
let payload = try await LocationService.shared.requestOnce()
let payload = try await LocationService.shared.requestOnce(caller: label)
startLocLog.debug("[\(label, privacy: .public)] → getlocationinfo 成功 city=\(payload.city, privacy: .public) province=\(payload.province, privacy: .public)")
bridge.call("getlocationinfo", data: .object([
"address": .string(payload.address),
"city": .string(payload.city),
@@ -44,12 +53,14 @@ public enum StartLocationHandler {
]), callback: nil)
} catch LocationError.authorizationDenied {
// msext gameController.m:2507
startLocLog.error("[\(label, privacy: .public)] → getlocationinfo 失败:authorizationDenied")
bridge.call("getlocationinfo", data: .object([
"errorCode": .number(12),
"errorMsg": .string("缺少定位权限")
]), callback: nil)
} catch {
// 12msext
startLocLog.error("[\(label, privacy: .public)] → getlocationinfo 失败:\(String(describing: error), privacy: .public)")
bridge.call("getlocationinfo", data: .object([
"errorCode": .number(12),
"errorMsg": .string("缺少定位权限")
@@ -11,6 +11,10 @@
import Foundation
import CoreLocation
import os.log
/// letLogger Sendable便 AMap
private let locLog = Logger(subsystem: "ylgamehall", category: "Location")
#if canImport(AMapLocationKit)
import AMapLocationKit
@@ -59,11 +63,18 @@ public final class LocationService {
}()
#endif
/// requestOnce
private var requestSeq: UInt64 = 0
/// + msext locAction + completionBlock
public func requestOnce() async throws -> 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<LocationPayload, Error>) 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 errorCodehandler
if err.code == AMapLocationErrorCode.locateFailed.rawValue {
@@ -89,15 +100,23 @@ public final class LocationService {
street: regeo.street ?? ""
))
}
// AMapLocationManager.hRequest 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.hstopUpdatingLocation cancel
// shared manager ****
locLog.debug("[\(caller, privacy: .public)] stop()stopUpdatingLocation + delegate=nil(会取消所有在飞的单次定位)")
manager.stopUpdatingLocation()
manager.delegate = nil
#endif
@@ -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
@@ -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
@@ -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 20Phase 5 Phase 2 stub
StartLocationHandler.register(on: bridge, label: "lobby") // §3.1 20Phase 5 Phase 2 stub
// Phase 3.A + 3.C/3.D stub
// assetsRoot = H5 lobbyIndex lobby