review: M5 双 Web 审查后的质量加固(两轮真机验证)

八角度审查双 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) <noreply@anthropic.com>
This commit is contained in:
lanterngamescn
2026-06-26 00:13:44 +08:00
parent c0addf8ec2
commit 9be1a23635
8 changed files with 62 additions and 27 deletions
@@ -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') {
@@ -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);
@@ -30,11 +30,12 @@ export class NetworkProvider implements CapabilityProvider {
this.subscribe();
}
/** 槽激活:恢复网络变化订阅。 */
/** 槽激活:恢复网络变化订阅,并补播当前网络(覆盖非激活期发生的网络变化,避免 H5 状态陈旧)。 */
onForeground(): void {
if (this.conn === undefined) {
this.subscribe();
}
this.broadcast();
}
/** 槽去激活:注销网络变化订阅(非激活端不监听)。 */