From 9be1a23635bba2b8728c1e7fa33581e32e4bb5a2 Mon Sep 17 00:00:00 2001 From: lanterngamescn Date: Fri, 26 Jun 2026 00:13:44 +0800 Subject: [PATCH] =?UTF-8?q?review:=20M5=20=E5=8F=8C=20Web=20=E5=AE=A1?= =?UTF-8?q?=E6=9F=A5=E5=90=8E=E7=9A=84=E8=B4=A8=E9=87=8F=E5=8A=A0=E5=9B=BA?= =?UTF-8?q?=EF=BC=88=E4=B8=A4=E8=BD=AE=E7=9C=9F=E6=9C=BA=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 八角度审查双 Web 批次。修确凿问题: 铁律风险(最高) - network_config:base-config cleartextTrafficPermitted 由 false 改 true。我之前的"白名单收紧"比平台默认更严, 会拦掉 H5 可能访问的任意第三方明文 http 与本地 127.0.0.1 上传 → 违反 H5 零改动。改回允许明文(对齐原 Android) 正确性 - 子游戏 controller 不再复用:每次进子游戏 new WebviewController()(存 slotS.controller),消除"复用同一 controller 时 onControllerAttached 不再触发致第二个子游戏建不出桥"的竞态。真机两轮验证 subB 桥正常 ✓ - BridgeController.dispose 置 active=false:显式抑制 dispose 后能力延迟异步 callHandler(不靠 adapter emitter-off 偶然安全) - WebSlot.resumeLoaded 补发 setPostUrl:覆盖"非激活期渲染崩溃重载后 onFirstProgress 被闸门抑制丢失上传地址"。真机验证返回必带 setPostUrl ✓ - onRenderExited 不可见时延迟重载:非激活槽崩溃标记 needReload,激活时再 reload(官方建议可见后重载);activate 返回是否重载,resumeLoaded 据此避免重复推送 - 容器 aboutToDisappear:先退订 cancels 再 dispose 槽,关闭销毁期事件又建槽窗口 - LocationProvider:权限对话框 await 后加 disposed 守卫(防 onDestroy 后再注册监听泄漏+打已弃桥) - AudioProvider:createAVPlayer.then 加 disposed 守卫(创建期已销毁则直接 release,防孤儿播放器泄漏) - NetworkProvider.onForeground:重订阅后补播当前网络(覆盖非激活期网络变化,避免 H5 状态陈旧) 研判驳回:network_config 需 metadata 关联(官方文档证实 profile 路径自动识别);app 前后台只发 activeSlot(非激活大厅已收 appservice'2',正确)。 延期:WeChatApi 单例陈旧回调(随微信集成)。 devecocli build + scripts/test.sh 通过;模拟器两轮(大厅↔subA↔大厅↔subB↔大厅)无回归。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../main/ets/pages/BridgeGameContainer.ets | 14 ++++--- entry/src/main/ets/web/WebSlot.ets | 39 ++++++++++++++---- .../base/profile/network_config.json | 15 +------ .../resources/rawfile/gamehall_builtin.zip | Bin 1597 -> 2140 bytes .../src/main/ets/core/BridgeController.ets | 2 + .../src/main/ets/providers/AudioProvider.ets | 10 +++++ .../main/ets/providers/LocationProvider.ets | 6 +++ .../main/ets/providers/NetworkProvider.ets | 3 +- 8 files changed, 62 insertions(+), 27 deletions(-) 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 b0dee883650b4ac21806d1c1264e5bcab268f3de..d71bce135d89f4c59d3895e7468263c8a582567b 100644 GIT binary patch literal 2140 zcmZvddpr~BAIHZi6;?Q|aWZC1W;s&)l0y`23r%hzo5?maj?MKqBzHzGQ7N}vj+1L9 zq%$4M&LP)Cx#kc$i@CH4mG)cbkFeM8d0x*S&+~e|@8@~FzMt>s>wpAGC;$KeDF9t; z%tLH__=$-)0Pv3l03a_s^}`1E60l^lA(4Xj4TlqG0ptfx&q(74g{U=`%7`p9rddn+ zwA>?O^oY9^CYx4@P;Q{{2*HY~fWHsbu?cZKy?*tU9C_#-IdZk+hczSxkF_I)58-T9(THyLGGxhPsi}UXz`k+h zkPB49epj5I2d`t34afqMh)I7HxwMTS^*J>|sVg0CEDbO*2Dt+ZEBmyIAl1vGsl0li zmd!59UP0nN0h)#-iuo=`2NS#%!`YSLOSFt#<)-9dh8~yjTMqh2mL2Mx$tu+=E}L0MM2AwvJtbf;UWW6`Wc{Ee8TCiSznzzGtQJDAv5@X&_k{P6N! zqlg=WP0wGI->Mk@I~>(ak)2{aZVisRdT!#2fU^67M!Y@L9`t16{Zf`R6r?qF(En;w z&h%{oc747`CQS87;q$2XQoO>V?s*%2G{9kp%+U8+nHZtG!iDRb_`b_5)Hj$)45Yxr z1ISD}`yTWIrX9+@SKqM{b^pRyr}Le_y`!*PUGDhk6?{7z@r;&92tJMT@*>H)f>m^K z?ucc>kOr{6o_>rjvPVyHZ@zMSnaqbPma`(l=Vo_Vy}1U5Jik}d|eQ6+%>D$?>afw^-EgjI54))AT(1kD|Iw65|Fy5|vy z+`Vg&ZR=Ky1Z|=3~TJ#9c1FZ zPISW>9h-dFUu(}`esjr7J;sRkesWsKLhiYjl zcz$#|-&Cc$loAgf0^L+z&aZxQB{-d@b)nJbMvRoVizm73E1s1`-u| z&uCwhxgKpbua3F<&c~`ul9_uDo(R@ysVM!Z68mhvyZjfM=RUUHJIggpk{t zW6yu~S*LD@(Z2k9dP{{z|una{Bz2%AnHq+Zlcn1V07*)9OreXmZd=KcA4+YYy>= zV)AMpIh=;#(vD>_gv_4LZV2nTQ!^F1ggQy%^Kr#%hG}(NbM$jJ>$g2@7^<@k>t2b_ zu{Nt$>zXF%;u!vB#7p}t;e~25diJ^>LzQh!F>g`HKe;|t9HrF_B@B8=U6aBy&+I*h zM=j@6aqg@%Lw-{cq(sKpCM>2tSV?|)J%i|j{yg+@IE|;l{#23EB@uG<#V}f9X=Ren zO#YT>F>!@|jp+Y(L1ATY8{f@pt0#&AMEz|m+E3VAzXP_pohU)noVEyy+X*|ZN)#e$ rPg@W}p<#)bm1wZ2;J1QRCI1cHE`JB4zwQ19czTWA delta 1299 zcmca3u$MS*n=$vdUX)N`Vqhp?VPFuRtj{i1KQ$yV`?i6=-*@p3 zpLORv+SO_x;rV9Mmc(dI@5tS~hZ0U)vb+C2KGAUj>E=HTSGg<^WF|Mvwfi;MG2Yn@~l z9K^I)Soa4L)3ny?iR`@-VoX1ppV-Hhk(cRPUTGb&BkksWg~hxgY}aBoX0cv6Hsy0> z??E34ZlMwfzmz&RTiwpMU)=xJMobCTX+QXE0Z%HsTFmv3->IAOQy9yl8o1Tp*1O9I zxJ)c)nRR+YkFL$lY}JjOMZOf0% z?JZxJZ%x{9tMItbn}1YF?3~HpMz1)F=K`4y z*P9%VtIO@YS^7#o;0EJ;FBg_so&oQlv6$AcyFb_HTen`^V!H+Znx%S=|J*5B*ZZr% z%J*zW;G0aBUlruJQ?!JHI*xNr3&OTMmDy*{NKlDy@$Blh@_w-Rx(c}`Q zCT3u2n*5W=p&pc~wth2tb&HXKVLLMe13xgW6_+MC>SyMqq*mx0cva+)*L8K2=h8saV>~n&wBVTb`=K=N#3g)| zd|q(S?TgP-Wxx6@i~e5!x9Gp{>*+jC4Q|M&9p0!czFz#yB&*xzM$GnM7uptIbr%eD zxmeL*e7$gPMa9Q*x8(MH(|5-GaBg=tIU&jF>27|9WqIFQY1ijXYt@c=D{PZ(_E{$R z*0nCQS<|z+UAXN-Wp%;C)GZg8yjD71xPAEv@7|?_Has2^BW7%!R)3S0WO=FTPv3W<%&* zjnV~M-RD?y2;Guxf9>b;wDTV-VMkWykEJe&@S#}e7Q1bBIxNCDTFj-UpF-RQ( z1WzttR|c!;aI8PX2bACeVsV55AnV5DP3&s!*peASnKUrrp(VNiZ&o&t0u~@-1*T0U HRuB&WCV4&y 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(); } /** 槽去激活:注销网络变化订阅(非激活端不监听)。 */