diff --git a/entry/src/main/ets/pages/BridgeGameContainer.ets b/entry/src/main/ets/pages/BridgeGameContainer.ets index c9325d8..e07f29c 100644 --- a/entry/src/main/ets/pages/BridgeGameContainer.ets +++ b/entry/src/main/ets/pages/BridgeGameContainer.ets @@ -22,7 +22,8 @@ export struct BridgeGameContainer { pathStack: NavPathStack = new NavPathStack(); params: BridgeGameParams = { entryUrl: '', launchType: '0' }; private controllerL: webview.WebviewController = new webview.WebviewController(); - private controllerS: webview.WebviewController = new webview.WebviewController(); + // 子游戏 controller 不复用:每次进子游戏新建(随 slotS),避免复用同一 controller 时 + // onControllerAttached 不再触发导致第二个子游戏建不出桥(review 发现)。存于 slotS.controller。 private uploadServer: LocalUploadServer = new LocalUploadServer(); private hostCtx: common.UIAbilityContext | undefined = undefined; private config: ConfigManager | undefined = undefined; @@ -41,12 +42,13 @@ export struct BridgeGameContainer { } aboutToDisappear(): void { - this.slotS?.dispose(); - this.slotL?.dispose(); + // 先退订事件,再 dispose——避免销毁期 SWITCH_GAME/FOREGROUND 等事件又触发建槽/回调 for (const cancel of this.cancels) { cancel(); } this.cancels = []; + this.slotS?.dispose(); + this.slotL?.dispose(); this.uploadServer.stop(); this.slotS = undefined; this.slotL = undefined; @@ -134,7 +136,7 @@ export struct BridgeGameContainer { const values: AppDataValues = AppDataInjector.buildValues(cfg.getLocal(), `${res.localVersion()}`, '1'); AppDataInjector.inject(subDir, values); this.slotL?.deactivate(); - this.slotS = new WebSlot('subgame', this.controllerS, true, this.uploadServer); + this.slotS = new WebSlot('subgame', new webview.WebviewController(), true, this.uploadServer); this.applyOrientation(d.webtype !== '2'); // "3" 横 / "2" 竖 this.subgameUrl = `file://${subDir}/index.html?Launchtype=1`; // 触发子游戏 Web 渲染 → setupSubgame } @@ -192,8 +194,8 @@ export struct BridgeGameContainer { .onRenderExited((event: OnRenderExitedEvent) => this.slotL?.onRenderExited(event.renderExitReason)) // 子游戏 Web(临时,subgameUrl 非空时存在) - if (this.subgameUrl !== '') { - Web({ src: this.subgameUrl, controller: this.controllerS }) + if (this.subgameUrl !== '' && this.slotS !== undefined) { + Web({ src: this.subgameUrl, controller: this.slotS.controller }) .javaScriptAccess(true).domStorageAccess(true).fileAccess(true) .mixedMode(MixedMode.All).cacheMode(CacheMode.None) .geolocationAccess(true).zoomAccess(false) diff --git a/entry/src/main/ets/web/WebSlot.ets b/entry/src/main/ets/web/WebSlot.ets index a7d5530..b5ab61c 100644 --- a/entry/src/main/ets/web/WebSlot.ets +++ b/entry/src/main/ets/web/WebSlot.ets @@ -29,6 +29,7 @@ export class WebSlot { private firstDone: boolean = false; private active: boolean; private disposed: boolean = false; + private needReload: boolean = false; constructor(role: string, controller: webview.WebviewController, active: boolean, uploadServer: LocalUploadServer) { this.role = role; @@ -89,12 +90,22 @@ export class WebSlot { if (this.disposed) { return; } - WebSlot.log.w(`[${this.role}] render exited (reason=${reason}); reloading`); + WebSlot.log.w(`[${this.role}] render exited (reason=${reason})`); this.firstDone = false; + if (this.active) { + this.doReload(); + } else { + // 不可见(非激活/隐藏)时不立即重载,标记待激活时重载(官方建议:可见后再 reload) + this.needReload = true; + } + } + + private doReload(): void { + this.needReload = false; try { this.controller.refresh(); } catch (e) { - WebSlot.log.e(`[${this.role}] refresh after render exit failed: ${(e as BusinessError).message}`); + WebSlot.log.e(`[${this.role}] reload after render exit failed: ${(e as BusinessError).message}`); } } @@ -113,8 +124,8 @@ export class WebSlot { this.flushPending(); } - /** 激活(onActive + 桥放行 + 能力前台)。页面已加载的大厅返回时另调 resumeLoaded 补发 appservice。 */ - activate(): void { + /** 激活(onActive + 桥放行 + 能力前台)。返回是否触发了延迟重载(非激活期渲染崩溃 → 此刻重载)。 */ + activate(): boolean { this.active = true; try { this.controller.onActive(); @@ -123,12 +134,26 @@ export class WebSlot { } this.bridgeCtrl?.setActive(true); this.registrar?.forEachForeground(); + if (this.needReload) { + this.doReload(); // 重载后由 onFirstProgress 补 appservice/setPostUrl + return true; + } + return false; } - /** 大厅从子游戏返回(页面仍在、无新 100%):激活并直接补发 appservice('1') + 待发 getWebdata。 */ + /** 大厅从子游戏返回(页面仍在、无新 100%):激活并补发 appservice('1') + setPostUrl + 待发 getWebdata。 + * 补发 setPostUrl:覆盖"非激活期渲染崩溃重载后 onFirstProgress 被闸门抑制而丢失上传地址"的情形。 */ resumeLoaded(): void { - this.activate(); - this.bridgeCtrl?.callHandler(OutboundHandlers.AppService, '1'); + const reloaded: boolean = this.activate(); + if (reloaded) { + // 页面正在重载(非激活期崩溃恢复):由重载后的 onFirstProgress 补发 appservice/setPostUrl/getWebdata + return; + } + const b = this.bridgeCtrl; + if (b !== undefined) { + b.callHandler(OutboundHandlers.AppService, '1'); + b.callHandler(OutboundHandlers.SetPostUrl, this.uploadServer.baseUrl()); + } this.flushPending(); } diff --git a/entry/src/main/resources/base/profile/network_config.json b/entry/src/main/resources/base/profile/network_config.json index 5fefff5..a566f19 100644 --- a/entry/src/main/resources/base/profile/network_config.json +++ b/entry/src/main/resources/base/profile/network_config.json @@ -1,18 +1,7 @@ { "network-security-config": { "base-config": { - "cleartextTrafficPermitted": false - }, - "domain-config": [ - { - "domains": [ - { - "include-subdomains": true, - "name": "daoqi88.cn" - } - ], - "cleartextTrafficPermitted": true - } - ] + "cleartextTrafficPermitted": true + } } } diff --git a/entry/src/main/resources/rawfile/gamehall_builtin.zip b/entry/src/main/resources/rawfile/gamehall_builtin.zip index b0dee88..d71bce1 100644 Binary files a/entry/src/main/resources/rawfile/gamehall_builtin.zip and b/entry/src/main/resources/rawfile/gamehall_builtin.zip differ diff --git a/feature_bridge/src/main/ets/core/BridgeController.ets b/feature_bridge/src/main/ets/core/BridgeController.ets index 60c9ad6..5ae9b03 100644 --- a/feature_bridge/src/main/ets/core/BridgeController.ets +++ b/feature_bridge/src/main/ets/core/BridgeController.ets @@ -59,6 +59,8 @@ export class BridgeController { this.responseCallbacks.clear(); this.registry.clear(); this.startupMessages = null; + // 置非激活:dispose 后能力的延迟异步回调若仍 callHandler,dispatch 闸门显式抑制(不依赖 adapter emitter-off 的偶然安全) + this.active = false; } // —— H5 → 原生:在 Web().onLoadIntercept 调用,返回 true 表示拦截、阻断真实导航 —— diff --git a/feature_capabilities/src/main/ets/providers/AudioProvider.ets b/feature_capabilities/src/main/ets/providers/AudioProvider.ets index 0522bda..d89a050 100644 --- a/feature_capabilities/src/main/ets/providers/AudioProvider.ets +++ b/feature_capabilities/src/main/ets/providers/AudioProvider.ets @@ -27,6 +27,7 @@ export class AudioProvider implements CapabilityProvider { private voicePlayer: media.AVPlayer | undefined = undefined; private sfxPlayer: media.AVPlayer | undefined = undefined; private muted: boolean = false; + private disposed: boolean = false; register(bridge: BridgeController, ctx: CapabilityContext): void { this.bridge = bridge; @@ -55,6 +56,7 @@ export class AudioProvider implements CapabilityProvider { } onDestroy(): void { + this.disposed = true; this.releaseVoice(); this.releaseSfx(); } @@ -74,6 +76,10 @@ export class AudioProvider implements CapabilityProvider { const user: string = req.user; this.releaseVoice(); media.createAVPlayer().then((player: media.AVPlayer) => { + if (this.disposed) { + player.release(); // 创建期间已销毁:直接释放,勿播放/勿挂回调(防泄漏 + 打已弃桥) + return; + } this.voicePlayer = player; player.on('stateChange', (state: string) => { if (state === 'initialized') { @@ -121,6 +127,10 @@ export class AudioProvider implements CapabilityProvider { const loop: boolean = req.isloop > 0; this.releaseSfx(); media.createAVPlayer().then((player: media.AVPlayer) => { + if (this.disposed) { + player.release(); + return; + } this.sfxPlayer = player; player.on('stateChange', (state: string) => { if (state === 'initialized') { diff --git a/feature_capabilities/src/main/ets/providers/LocationProvider.ets b/feature_capabilities/src/main/ets/providers/LocationProvider.ets index 52924b8..8fe7b9f 100644 --- a/feature_capabilities/src/main/ets/providers/LocationProvider.ets +++ b/feature_capabilities/src/main/ets/providers/LocationProvider.ets @@ -23,6 +23,7 @@ export class LocationProvider implements CapabilityProvider { private lastInfo: MaplocationInfo | undefined = undefined; private continuousOn: boolean = false; private wantContinuous: boolean = false; + private disposed: boolean = false; private readonly onLocation: (loc: geoLocationManager.Location) => void; constructor() { @@ -53,6 +54,7 @@ export class LocationProvider implements CapabilityProvider { } onDestroy(): void { + this.disposed = true; this.wantContinuous = false; this.stopContinuous(); } @@ -63,6 +65,10 @@ export class LocationProvider implements CapabilityProvider { return; } const granted: boolean = await PermissionGuard.request(ctx, LOC_PERMS); + // 权限对话框期间槽可能已销毁:勿在 onDestroy 后再注册监听(否则泄漏 + 打已弃桥) + if (this.disposed) { + return; + } if (!granted) { this.lastInfo = this.errorInfo(-2); this.push(this.lastInfo); diff --git a/feature_capabilities/src/main/ets/providers/NetworkProvider.ets b/feature_capabilities/src/main/ets/providers/NetworkProvider.ets index 4dd4b7a..7e42f1b 100644 --- a/feature_capabilities/src/main/ets/providers/NetworkProvider.ets +++ b/feature_capabilities/src/main/ets/providers/NetworkProvider.ets @@ -30,11 +30,12 @@ export class NetworkProvider implements CapabilityProvider { this.subscribe(); } - /** 槽激活:恢复网络变化订阅。 */ + /** 槽激活:恢复网络变化订阅,并补播当前网络(覆盖非激活期发生的网络变化,避免 H5 状态陈旧)。 */ onForeground(): void { if (this.conn === undefined) { this.subscribe(); } + this.broadcast(); } /** 槽去激活:注销网络变化订阅(非激活端不监听)。 */