首页 / 资讯中心 / 文章详情

HarmonyOS 7 ArkWeb:WebGL上下文丢失后纹理重建与状态回放

HarmonyOS 7 ArkWeb:WebGL上下文丢失后纹理重建与状态回放 ★ FEATURED ARTICLE
一、页面回来了博物馆场景却只剩一块灰布WebSceneRecovery是一个用 ArkWeb 承载 Three.js 的展陈预览页。模型museum_lobby.glb有 9 个材质、14 张纹理用户可以旋转相机并选中展台。正常进入只需 436 ms问题出现在应用切到后台再回来ArkWeb 页面仍在工具栏也能点击Canvas 却只显示灰色背景。更糟的一次是场景恢复了但转动相机明显加速HiLog 里同一帧出现两次render。这不是“重新加载网页”能稳定解决的问题。WebGL 上下文丢失后旧纹理、buffer 和 program 已失效页面前后台切换又可能重复启动动画循环。如果 ArkTS 只看到 WebView 还活着就会把旧场景状态、旧 GPU 资源和新的requestAnimationFrame混在一起。本次 Demo 固定项目WebSceneRecovery、页面ModelPreviewPage界面标题“WebGL 场景恢复”、任务GL-2236、时间 22:36使用three0.183.0。故障注入后状态经历ACTIVE → SUSPENDED → CONTEXT_LOST → RESTORING → READYgeneration 12。最终恢复 14/14 纹理、9/9 材质帧循环保持 1 条总耗时 684 ms。二、先把“页面生命周期”和“图形上下文生命周期”分成两本账项目里原先只有一个isReady。ArkWeb 加载完成后置 true页面隐藏时置 false回前台再调用start()。这在普通 H5 页面够用但图形页至少有三层状态ArkTS 页面是否可见、WebView 文档是否可通信、WebGL 上下文是否有效。任何一层单独为 true都不能证明可以渲染。我把状态拆成两本账。宿主账由ModelPreviewPage管理VISIBLE/HIDDEN/DESTROYED渲染账由网页运行时管理BOOTING/ACTIVE/SUSPENDED/CONTEXT_LOST/RESTORING/READY。两边通过 taskId、generation 和 commandId 通信每条命令只有收到对应 ACK 才改变页面展示。工程结构也跟着调整WebSceneRecovery/ ├── entry/src/main/ets/pages/ModelPreviewPage.ets ├── entry/src/main/ets/web/SceneBridge.ets ├── entry/src/main/resources/rawfile/viewer/index.html ├── entry/src/main/resources/rawfile/viewer/scene-runtime.ts ├── entry/src/main/resources/rawfile/viewer/ResourceRegistry.ts ├── entry/src/main/resources/rawfile/viewer/SceneSnapshot.ts └── entry/src/main/resources/rawfile/models/museum_lobby.glb宿主不保存 Three.js 对象只保存可序列化快照网页不猜页面是否可见只接受显式SUSPEND/RESUME/DESTROY。这样上下文恢复和页面回前台即使顺序相反也不会各自启动一次循环。三、ArkTS 侧的关键不是 runJavaScript而是命令有代次、有回执第一段代码解决 ArkWeb 命令晚到的问题。页面隐藏时先发SUSPEND回前台再发RESUME每次重新装载文档都会递增 generation。旧文档迟到的 ACK 会被丢弃不能把新页面改成 ACTIVE。// ModelPreviewPage.etsEntryComponentstruct ModelPreviewPage{privatecontrollernewwebview.WebviewController()Statesnapshot:SceneHostSnapshotSceneHostSnapshot.booting(GL-2236)privategeneration:number12privateasyncsend(type:SceneCommandType):Promisevoid{constcommandIdGL-2236-${this.generation}-${Date.now()}constpayloadJSON.stringify({taskId:GL-2236,generation:this.generation,commandId,type})constresultawaitthis.controller.runJavaScript(window.sceneHost.dispatch(${payload}))constackJSON.parse(result)asSceneAckif(ack.generation!this.generation||ack.commandId!commandId)returnthis.snapshotthis.snapshot.apply(ack)}onPageHide():void{voidthis.send(SUSPEND)}onPageShow():void{voidthis.send(RESUME)}aboutToDisappear():void{voidthis.send(DESTROY)}}这里没有在onPageShow里直接改 READY。RESUME只是表达宿主愿意继续如果网页仍处于CONTEXT_LOSTACK 会返回waitingForRestoretrue页面保持恢复中。aboutToDisappear发送 DESTROY 后不等待 UI 更新只要求网页停止帧循环并释放可释放资源Ability 被系统终止时也不能依赖异步回执完成业务保存所以相机快照在每次交互结束后已写入宿主账。四、context lost 回调里先停帧再冻结可回放快照第二段代码位于网页侧。我们监听webglcontextlost与webglcontextrestored故障发生时立即把setAnimationLoop(null)保存相机、选中节点与曝光参数并把状态上报为CONTEXT_LOST。恢复事件到来后不复用旧 Texture 引用而是创建新 renderer、重新加载 GLB再回放纯数据快照。// scene-runtime.tscanvas.addEventListener(webglcontextlost,(event:WebGLContextEvent){event.preventDefault()runtime.stateCONTEXT_LOSTruntime.contextLostCount1runtime.renderer.setAnimationLoop(null)runtime.loopActivefalseruntime.frozencaptureSnapshot({camera,controls,selectedNodeId:bench_A,exposure:1.15})bridge.report(CONTEXT_LOST,runtime.metrics())})canvas.addEventListener(webglcontextrestored,async(){constgenerationruntime.restoreGeneration runtime.stateRESTORINGawaitregistry.disposeScene()awaitruntime.rebuildRenderer()awaitruntime.loadModel(museum_lobby.glb,generation)runtime.assertRestoreGeneration(generation)applySnapshot(runtime.frozen)runtime.startSingleLoop()runtime.stateREADYbridge.report(READY,runtime.metrics())})preventDefault()表示应用接管 WebGL 上下文丢失处理恢复后旧 GPU 资源不再有效因此快照里绝不放 Mesh、Texture 或 Material。restoreGeneration还处理第二次上下文丢失第一次恢复的 GLB 请求即使完成也只能释放自己的结果不能覆盖更新的恢复代次。五、纹理恢复和帧循环必须分别设闸门有一次修复让场景重新出现却留下两条动画循环。原因是RESUME与webglcontextrestored都调用了startLoop()。第三段代码把循环收口成单一入口并给资源建立注册表。新场景完全装载前旧注册项不会被新结果覆盖恢复失败则按逆序 dispose 新建材质、纹理和几何体。// ResourceRegistry.tsclassSceneRuntime{privateloopActivefalseprivaterestoreGeneration0startSingleLoop():void{if(this.loopActive||this.stateCONTEXT_LOST)returnthis.loopActivetruethis.renderer.setAnimationLoop((){if(!this.loopActive||document.hidden)returnthis.controls.update()this.renderer.render(this.scene,this.camera)})}asyncdisposeScene():Promisevoid{this.loopActivefalsethis.renderer.setAnimationLoop(null)this.scene.traverse(objectdisposeObjectResources(object))this.pmremGenerator?.dispose()this.renderer.dispose()this.registry.clear()}}Three.js 的dispose()不是页面级垃圾回收。共享材质、环境贴图和 PMREM 资源要由注册表按所有权释放不能遍历一次就对同一 Texture dispose 两次。renderer.info.memory.textures用来做恢复后的数量检查但内部复用对象不一定立即归零所以门禁比较的是当前模型预期的 14 张业务纹理而不是强行要求所有内部统计为 0。六、故障注入比等一次偶现灰屏更省时间测试页增加“模拟上下文丢失”按钮只在 Debug 构建启用。网页通过WEBGL_lose_context.loseContext()触发故障400 ms 后调用restoreContext()。这一条路径能稳定复现真实资源失效而不是用清空 Canvas 冒充。本轮关键日志如下22:36:02.418 WebScene GL-2236 SUSPENDED loop0 snapshotbench_A 22:36:02.621 WebScene GL-2236 CONTEXT_LOST count1 textures14 loop0 22:36:02.639 WebScene GL-2236 RESTORING generation12 modelmuseum_lobby.glb 22:36:03.305 WebScene GL-2236 READY textures14/14 materials9/9 loop1 total684ms684 ms 包含宿主桥接 ACK 18 ms、模型与场景资源重新加载 412 ms、纹理上传 236 ms以及状态回放 18 ms。恢复后相机位置仍为[2.4, 1.6, 5.2]target[0, 1.2, 0]选中节点bench_A曝光 1.15。它们都来自快照不是网页重新打开后的默认值。七、手机页展示恢复证据不展示一张“看起来正常”的场景图最终页面状态为READY任务GL-2236generation 12模型museum_lobby.glb上下文丢失次数 1纹理 14/14材质 9/9活动帧循环 1恢复总耗时 684 ms。状态轨迹完整保留ACTIVE → SUSPENDED → CONTEXT_LOST → RESTORING → READY。页面还显示相机与选中节点快照方便判断“恢复的是同一浏览上下文”而不只是重新加载了模型。按钮为“再次注入故障”和“查看资源明细”恢复过程中两个按钮都禁用防止把并发注入当成普通重试。八、边界能恢复上下文不等于所有 ArkWeb 页面都该常驻 GPU这套方案适合需要保持浏览位置的短时前后台切换不代表 WebView 隐藏后永远保留模型。若页面离开超过 30 秒、系统内存压力升高或用户真正退出预览宿主会发送 DESTROY网页释放场景和 renderer下次进入按冷启动处理。另一个边界是版本与设备。本文锁定three0.183.0和当前 DemoWebGL 扩展、纹理压缩格式及 ArkWeb 行为仍要按目标设备验收。上下文恢复失败时不能无限循环最多自动尝试一次之后进入RESTORE_FAILED并保留快照让用户手动重新加载。这次修复最后只留下一个朴素约束页面可见、文档可通信、WebGL 有效三件事同时成立帧循环才允许启动GPU 资源失效后只回放数据不回放对象。把这条约束落实到 taskId、generation、资源注册表和单循环闸门灰屏和双倍渲染才真正从偶现问题变成可验证的工程状态。
阅读完成 · 觉得有帮助?
咨询建站