加桥/定位边界诊断日志,排查子游戏收不到 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:
co-authored by
Claude Opus 5
parent
8a7d543cdc
commit
3f8b382ad3
@@ -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 非隔离上下文,故显式 nonisolated(Logger 本身 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 {
|
||||
// 其它失败也归一到 12(msext 行为)
|
||||
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
|
||||
|
||||
/// 定位诊断日志(文件级 let,Logger 是 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 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
|
||||
|
||||
@@ -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 [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 不做升级
|
||||
|
||||
Reference in New Issue
Block a user