ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HarmonyOS 7 ImageAnimator:长动图帧时序修正与前后台恢复

HarmonyOS 7 ImageAnimator:长动图帧时序修正与前后台恢复 一、那张“偶尔加速”的动图没有坏FramePulseLab原本只是一个动效素材验收页设计同学把metro_signals.gif放进resources/rawfile开发侧显示帧数、原始时长和当前生命周期确认无误后再把素材交给业务页面。文件是 720×720、48 帧桌面浏览器里一圈约 2.2 秒可在真机上连续切两次后台再回来指示灯会明显变快第三次进入页面时内存也不再回落。最初我把问题归咎于 GIF 本身。HiLog 却给了另一组证据解码只发生一次createPixelMapList()返回 48 个 PixelMap异常发生后onPageShow的次数是 3而播放控制器的活动实例从 1 变成了 2。更容易忽略的是源文件的延时表里有 11 帧小于 20 ms最短甚至为 0 ms。浏览器替素材做了容错Demo 却把这些值原样交给ImageAnimator于是“素材时序不稳定”和“前后台重复启动”叠在一起看上去像解码器随机加速。本次任务固定为GIF-0012。最终验收口径也提前写死48 帧原始总时长 1860 ms规范化后 2240 ms11 个延时被钳制处置类型中restore background为 7 帧、restore previous为 2 帧解码 186 ms峰值 108.4 MB任意次数前后台切换后activeController必须始终为 1。二、先把解码、播放和页面生命周期拆开这个问题不适合用一个State playing糊住。解码资源、组件状态和页面可见性是三条不同的时间线ImageSource 持有输入数据PixelMap 列表持有解码结果ImageAnimator 只消费帧信息页面进入后台时应该暂停不该再次解码页面真正离开时才停止并释放所有 PixelMap。我给GifPlaybackPage定义了七个状态IDLE → DECODING → READY → PLAYING ↔ PAUSED → STOPPED → RELEASED。READY以前不允许触发播放RELEASED以后任何异步回调都不得回写 UI。另有一个自增的loadGeneration用户快速离开再回来时旧解码任务即使晚到也只能释放自己的结果不能覆盖新页面。项目目录保持很小但职责必须清楚FramePulseLab/ └── entry/src/main/ ├── ets/entryability/EntryAbility.ets ├── ets/pages/GifPlaybackPage.ets ├── ets/media/GifFrameRepository.ets ├── ets/media/DelayPolicy.ets └── resources/rawfile/metro_signals.gifGifFrameRepository只管 Image Kit 对象的获得和释放DelayPolicy只处理时序页面只决定何时把AnimationStatus设为Running或Paused。这样复现日志能明确回答错在解码、元数据还是生命周期而不是所有异常都叫“播放失败”。三、一次读取三份证据拒绝只看 PixelMap第一段代码解决的是“画面能播但我们不知道它按什么节奏播”。Image Kit 对动图不仅能给出 PixelMap 列表还能读取逐帧延时和处置类型。三者必须来自同一个 ImageSource并在返回后校验长度只要有一项不是 48就不能进入READY。import{image}fromkit.ImageKit;import{resourceManager}fromkit.LocalizationKit;exportinterfaceGifFrameSet{maps:image.PixelMap[];delays:number[];disposals:number[];decodeMs:number;}exportclassGifFrameRepository{privatesource?:image.ImageSource;asyncload(rm:resourceManager.ResourceManager):PromiseGifFrameSet{conststartedDate.now();constbytesawaitrm.getRawFileContent(metro_signals.gif);this.sourceimage.createImageSource(bytes.buffer);const[maps,delays,disposals]awaitPromise.all([this.source.createPixelMapList({editable:false}),this.source.getDelayTimeList(),this.source.getDisposalTypeList()]);if(maps.length!delays.length||maps.length!disposals.length){maps.forEach((item:image.PixelMap)item.release());thrownewError(FRAME_META_MISMATCH maps${maps.length}delay${delays.length});}return{maps,delays,disposals,decodeMs:Date.now()-started};}release():void{this.source?.release();this.sourceundefined;}}这里特意不在仓库里保存 PixelMap 数组。所有权随GifFrameSet转交给页面控制器仓库只保留 ImageSource。这样页面销毁时能明确释放 48 个 PixelMap再释放 ImageSource不会出现两个对象都以为对方负责回收的模糊地带。Promise.all也不是为了炫技。延时表、处置表和图像帧属于同一个快照串行读取会延长DECODING窗口也让快速退页更容易撞上旧回调。易错点是 rawfile 的Uint8Array生命周期创建 ImageSource 后不能复用并修改底层 buffer另一个边界是超大动图全帧解码会线性增加内存本 Demo 的 108.4 MB 已经接近我设定的 120 MB 警戒线因此生产页还要限制分辨率与帧数。四、延时修正不是统一改成 40 ms源延时表为毫秒。metro_signals.gif中 0 ms 和 10 ms 并不是“应该极速播放”而是导出工具留下的含糊值。若把所有帧统一成 40 ms节奏停顿会消失若完全信任源值又会让不同运行环境表现不同。我采用的策略是只钳制小于 20 ms 的值到 40 ms保留其余延时且把修改数量写入审计结果。第二段代码同时解释处置类型。createPixelMapList()已经返回可用于显示的帧业务层不应再次手工清背景否则会把解码器已经合成的画面清两遍。处置表在这里是诊断信息确认素材确实含局部更新帧并把 7 个背景恢复与 2 个前态恢复呈现在页面上帮助定位拖影究竟来自素材还是播放器。import{image}fromkit.ImageKit;exportinterfaceFrameAudit{frames:ImageFrameInfo[];rawTotalMs:number;normalizedTotalMs:number;clampedCount:number;restoreBackground:number;restorePrevious:number;}exportfunctionbuildFrameAudit(maps:image.PixelMap[],delays:number[],disposals:number[]):FrameAudit{letclampedCount0;constnormalizeddelays.map((value:number){if(value20){clampedCount;return40;}returnvalue;});return{frames:maps.map((src:image.PixelMap,index:number)({src,width:720,height:720,top:0,left:0,duration:normalized[index]})),rawTotalMs:delays.reduce((sum:number,value:number)sumvalue,0),normalizedTotalMs:normalized.reduce((sum:number,value:number)sumvalue,0),clampedCount,restoreBackground:disposals.filter((value:number)value2).length,restorePrevious:disposals.filter((value:number)value3).length};}这段逻辑的边界也很明确20/40 ms 是本项目策略不是 GIF 标准常量遇到长停顿帧不会缩短遇到负数或数组越界则直接拒绝素材。规范化后的 2240 ms 是 48 帧 duration 之和ImageAnimator 以逐帧 duration 为准因此外层不再设置一个相互竞争的总 duration。五、前后台恢复只改状态不再创建播放器真正导致倍速的代码曾写在onPageShow每次可见都重新构造一次帧数组并把状态从Initial推到Running。组件本身还存在于是旧实例没有停止新状态又触发了启动。修复后的原则是“解码只属于页面实例恢复只属于状态迁移”。第三段代码用loadGeneration拦截过期结果并用activeController做运行期断言。onPageHide只暂停aboutToDisappear才停止、清空ImageFrameInfo引用、逐个释放 PixelMap再释放 ImageSource。重复调用disposeFrames()是安全的因为数组先被交换为空数组。EntryComponentstruct GifPlaybackPage{Statephase:stringIDLE;StateanimatorState:AnimationStatusAnimationStatus.Initial;Stateframes:ImageFrameInfo[][];StatetaskId:stringGIF-0012;privatemaps:image.PixelMap[][];privaterepo:GifFrameRepositorynewGifFrameRepository();privateloadGeneration:number0;privateactiveController:number0;asyncaboutToAppear():Promisevoid{constgenerationthis.loadGeneration;this.phaseDECODING;constsetawaitthis.repo.load(getContext(this).resourceManager);if(generation!this.loadGeneration){set.maps.forEach((item:image.PixelMap)item.release());return;}constauditbuildFrameAudit(set.maps,set.delays,set.disposals);this.mapsset.maps;this.framesaudit.frames;this.activeController1;this.phasePLAYING;this.animatorStateAnimationStatus.Running;console.info(GIF-0012 READY frames48 decode${set.decodeMs}ms normalized2240ms);}onPageHide():void{if(this.phasePLAYING){this.animatorStateAnimationStatus.Paused;this.phasePAUSED;}}onPageShow():void{if(this.phasePAUSEDthis.activeController1){this.animatorStateAnimationStatus.Running;this.phasePLAYING;}}aboutToDisappear():void{this.loadGeneration;this.animatorStateAnimationStatus.Stopped;this.frames[];constreleasingthis.maps.splice(0);releasing.forEach((item:image.PixelMap)item.release());this.repo.release();this.activeController0;this.phaseRELEASED;}build(){Column(){ImageAnimator().images(this.frames).state(this.animatorState).iterations(-1)Text(任务${this.taskId}·${this.phase})}}}需要注意aboutToAppear()里的异步操作不会因为页面退出自动取消所以代次检查必须发生在 await 之后。另一个坑是先释放 PixelMap 再清空组件引用渲染线程可能仍持有最后一帧。这里先把状态设为Stopped再让frames[]触发引用断开最后释放资源顺序不能颠倒。六、把异常恢复写成可重复的实验我没有用“肉眼看起来正常”作为结论而是做了三组固定动作。第一组冷启动后连续播放 20 圈归一化周期稳定在 2240 ms 左右第二组在第 31 帧切到后台停留 5 秒再返回组件从PAUSED回到PLAYING没有从第 0 帧额外创建播放链第三组连续执行三次“进入详情—返回—再次进入”PixelMap 数量按 48→0→48→0 变化内存能回到基线附近。最终页面展示任务GIF-0012、素材metro_signals.gif、48 帧、720×720、原始 1860 ms、规范化 2240 ms、钳制 11 帧、处置统计 7/2、解码 186 ms、峰值 108.4 MB。当前帧是 31/48生命周期经历onPageHide → onPageShow恢复计数 3活动控制器仍是 1状态为PLAYING_STABLE。这里的PLAYING_STABLE是验收页的派生标签只有内部phasePLAYING、activeController1且延时审计通过时才显示它不是额外的 ImageAnimator 枚举值。IDE 中看到的phasePLAYING与模拟器上的绿色稳定态因此属于同一时刻的两层状态而不是两套不一致的状态机。HiLog 的关键行也与页面一一对应GIF-0012 DECODE frames48 size720x720 cost186ms peak108.4MB GIF-0012 TIMING raw1860ms normalized2240ms clamped11 GIF-0012 DISPOSAL background7 previous2 GIF-0012 RESUME count3 activeController1 frame31/48 GIF-0012 STATE PLAYING_STABLE七、这次真正修掉的是所有权表面上这是一个动图速度问题根因却是三个所有权没有说清延时由谁解释、播放状态由谁推进、PixelMap 由谁释放。把它们拆开后ImageAnimator只负责显示Image Kit 只负责解码与元数据页面控制器负责生命周期任何异常都能落到一条确定的状态迁移和一行日志上。这套写法也有边界。全帧 PixelMap 不适合数百帧、2K 分辨率的长动图超过 120 MB 预算时应改为视频资源、Native 流式解码或在入库阶段压缩。处置类型是诊断信息不应在createPixelMapList()之后再自行合成。最后延时钳制必须由产品接受因为它改变了素材时间轴若帧时序具有业务含义就应该拒绝素材而不是自动修正。但对资源验收页而言这次结果足够明确同一份metro_signals.gif在 3 次前后台恢复后不再加速活动控制器始终只有一个页面离开后 48 个 PixelMap 全部释放。比“换个组件试试”更重要的是我们终于能解释每一帧为什么在那个时刻出现也能证明它何时被回收。
返回列表