
一、展开屏幕之后波形还在跑画面却停住了FoldScopeLab 是一个实时信号查看器XComponent 承载 Native 绘制ArkUI 负责工具栏和诊断卡片。半折叠状态下持续滚动没有问题从HALF_FOLDED展开到EXPANDED后偶尔会出现一个很矛盾的现象采样计数继续增长FPS 也在打印波形却冻结在旧画面再折回去又可能同时出现两条渲染线程。最开始我把它当成尺寸适配问题反复修改 XComponent 宽高。后来从日志里看到旧 Surfacesfc-22已销毁渲染线程仍提交了 6 帧新 Surfacesfc-23创建后又启动一个循环。问题不在布局而在 Surface 所有权、视口状态和线程生命周期没有绑定到同一个代次。本轮 Demo 工程是FoldScopeLab页面WaveformLabPage任务SURFACE-1608。当前姿态从半折叠变为展开Surface 由sfc-22切到sfc-23视口保持12.4 s—18.4 s显示 1200 个样本活动渲染线程 1旧 Surface 帧拒绝 6恢复耗时 47 ms最终状态SURFACE_STABLE。二、窗口尺寸变化不等于 Surface 已经可画旧实现监听窗口尺寸一旦变宽就调用 Nativeresize。但 XComponent 的 Surface 创建和销毁有自己的回调时序窗口已经是展开宽度时新 Surface 可能还没出现。把尺寸回调当作可绘制信号会让 Native 层拿旧 surfaceId 配新尺寸。当前实现用SurfaceSession保存 surfaceId、宽高和 generation。只有onSurfaceCreated能提升代次并绑定新句柄尺寸变化只更新候选布局等 Surface 就绪后一起提交。typeSurfaceSnapshot{surfaceId:string;width:number;height:number;generation:number;};classSurfaceOwner{privateactive?:SurfaceSnapshot;privategeneration0;bindSurface(surfaceId:string,width:number,height:number):SurfaceSnapshot{constnext{surfaceId,width,height,generation:this.generation};NativeWaveRenderer.attach(next.surfaceId,next.width,next.height,next.generation);this.activenext;returnnext;}invalidate(surfaceId:string):void{if(this.active?.surfaceId!surfaceId)return;NativeWaveRenderer.detach(surfaceId,this.active.generation);this.activeundefined;this.generation;}current():SurfaceSnapshot|undefined{returnthis.active;}}这里没有在invalidate里立即创建替代对象因为旧 Surface 销毁不能推断新 Surface 一定会来。组件离开页面、应用转后台和姿态切换都可能触发销毁恢复策略不同。detach必须幂等系统重复回调或页面主动释放时第二次调用只记录一次ALREADY_DETACHED。Surface 句柄不写入 Preferences也不放进 AppStorage。它只在当前进程和当前组件实例内有效。跨页面需要恢复的是视口、缩放和游标这些纯数据而不是图形资源本身。三、视口快照要在销毁前拿不在新画布上猜用户把波形放大到 6 秒窗口展开屏幕后如果按新宽度重新计算往往会跳回“显示最近 10 秒”。这不是渲染错误却破坏了连续观察。Demo 在 Surface 销毁前保存ViewportSnapshot起止时间、纵向范围、游标位置、冻结状态和数据 revision。下面的代码解决快照与采样流不一致的问题。快照记录sampleRevision新 Surface 恢复时先确认环形缓冲仍包含这个时间段若数据已经淘汰才把视口钳制到可用范围并显示一次VIEWPORT_CLAMPED。typeViewportSnapshot{startMs:number;endMs:number;minY:number;maxY:number;cursorMs:number;sampleRevision:number;};functionsnapshotViewport(model:WaveModel):ViewportSnapshot{returnObject.freeze({startMs:12_400,endMs:18_400,minY:model.minY,maxY:model.maxY,cursorMs:model.cursorMs,sampleRevision:model.revision});}functionrestoreViewport(snapshot:ViewportSnapshot,range:SampleRange):ViewportSnapshot{constdurationsnapshot.endMs-snapshot.startMs;conststartMsMath.max(range.firstMs,Math.min(snapshot.startMs,range.lastMs-duration));return{...snapshot,startMs,endMs:startMsduration};}Object.freeze不是为了性能而是防止姿态变化期间工具栏继续修改旧快照。页面的手势状态进入RESTORING后会暂时禁用缩放47 ms 恢复完成再开放。若用户主动切换数据源sampleRevision改变旧视口不再回放而是使用新数据的默认范围。快照不持有样本数组。1200 个显示样本仍由环形缓冲管理Native 层根据时间范围读取这样 Surface 重建不会复制整段数据也不会把 ArrayBuffer 所有权从一个线程偷偷转给另一个线程。四、渲染循环只能由一个所有者启动和停止双线程来自两个入口都调用startRenderLoop页面onPageShow调一次新 Surface 创建又调一次。修复后页面只声明“希望可见”Surface 回调只声明“资源可用”RenderGate在两个条件同时满足时启动唯一线程。下面的reconcile解决重复启动和旧 Surface 提交。每个循环捕获 generation绘制前和交换缓冲前各检查一次即使旧线程晚停也没有资格向sfc-23提交。classRenderGate{privatevisiblefalse;privaterunningGeneration-1;setVisible(value:boolean):void{this.visiblevalue;this.reconcile();}onSurfaceChanged():void{this.reconcile();}privatereconcile():void{constsurfaceSurfaceRegistry.current();if(!this.visible||!surface){NativeWaveRenderer.stopRenderLoop(this.runningGeneration);this.runningGeneration-1;return;}if(this.runningGenerationsurface.generation)return;if(this.runningGeneration0){NativeWaveRenderer.stopRenderLoop(this.runningGeneration);}NativeWaveRenderer.startRenderLoop(surface.surfaceId,surface.generation);this.runningGenerationsurface.generation;}}Native 侧的停止顺序是设置停止闩锁、唤醒等待条件、join 线程、释放图形资源。不能在 UI 线程上等待一个永不醒来的循环也不能先销毁资源再 join。Demo 的stopRenderLoop可重复调用generation 不匹配时只拒绝不会停止当前新线程。应用进入后台时visiblefalseSurface 即使还在也停止循环返回前台后重新协调。页面永久离开时还要注销 FoldStatus 与窗口监听清空视口订阅。只处理 Surface 回调而忘记系统监听页面重建后仍会收到旧实例的姿态变化。五、用故障注入验证旧 Surface 真的失权调试页的“模拟展开”依次发出HALF_FOLDED → EXPANDED、销毁sfc-22、延迟 20 ms 创建sfc-23。同时让旧循环再尝试提交 6 帧。正确结果不是日志安静而是 6 条OLD_SURFACE_DROPPED随后只有一条RENDER_LOOP_STARTED gen23。DevEco Studio 画面中项目树包含WaveformLabPage.ets、SurfaceOwner.ets、ViewportStore.ets和 Native 渲染桥中间代码显示 generation 闸门右侧模拟器显示sfc-22 → sfc-23、视口、线程和恢复耗时底部 HiLog 给出销毁、拒绝和恢复顺序。“导出视口快照”只写纯 JSON任务号、姿态、Surface 代次、视口范围、样本 revision 和恢复决策不包含原始波形。测试同学可以比较折叠前后快照确认 12.4—18.4 秒没有漂移。若发生钳制JSON 还会记录缓冲区的真实首尾时间。六、稳定不是没有重建而是重建过程可解释最终手机页显示姿态EXPANDED活动 Surfacesfc-23上一 Surfacesfc-22已释放视口仍为12.4 s—18.4 s1200 个样本活动渲染线程 1旧 Surface 拒绝帧 6恢复 47 ms状态SURFACE_STABLE。回归时我连续执行 30 次半折叠与展开Surface 代次每轮单调增加线程峰值始终为 1Native 资源账在每轮结束后回到基线。第 17 轮故意把新 Surface 创建延迟到 160 ms页面会显示WAITING_SURFACE采样继续但不绘制资源就绪后仍恢复原视口而不是为了隐藏空窗再启动一个临时循环。这套实现不保证所有折叠设备的 Surface 时序完全相同所以没有把“销毁一定先于创建”写死。它只建立一个不依赖回调顺序的合同新 Surface 获得新代次旧代次立即失去提交资格视口以纯数据恢复渲染线程由唯一闸门协调。实际产品还要处理 GPU 内存压力、Native 崩溃恢复和多窗口并存。本 Demo 每个页面只有一个 XComponent多实例场景必须把 pageId 和 componentId 加进所有权键不能只用全局 current。波形采样也有自己的生命周期停止渲染不等于停止采集两个资源需要分别计数。我最后用三项数据做回归门禁活动线程必须等于 1旧 Surface 提交必须等于 0拒绝计数可以大于 0视口恢复误差必须小于一个采样周期。折叠屏适配并不是把布局从一栏改成两栏图形场景里更重要的是让每一份系统资源都有明确所有者和退出路径。