HarmonyOS 7 Image Kit + Image_NativeModule:动画 WebP 帧级解码中的按帧取样、PixelMap 释放与长列表内存峰值控制【鸿蒙心迹】
最近我在一个素材列表里放了十几张动画 WebP。单独打开一张时没问题,真正放进长列表以后,内存曲线开始持续抬高:滑过几屏,峰值越来越高,返回上一页也降不下来。
我最初怀疑 ArkUI 的 Image 组件复用,后来把解码链路拆开才发现,真正的问题在 Native 层——为了省事,我把一张动图一次性解成了全部 PixelMap,页面只显示其中一帧,却长期持有整组对象。
这次我把问题收缩到一个工程点:长列表里的动画 WebP 怎样按帧解码、只缓存必要帧,并在滑动和页面销毁时主动释放 PixelMap。
Demo 叫 AnimatedFrameLab。固定文件 sticker_wave.webp,共 18 帧,当前第 12 帧,延迟 80 ms。最终缓存 3 帧,累计释放 15 帧,峰值内存 42 MB,最近一次单帧解码耗时 11 ms。

一、列表场景先换思路:不要默认全量解码
Image_NativeModule 的动图解码既可以一次性获取全部帧,也可以按索引解码指定帧。全量方式适合帧数少、独立播放页这类场景;长列表不同,大量动画并不在可视区,用户可能只停留很短时间。
如果每个卡片都持有完整 PixelMap 列表,内存峰值会随着滑动不断抬高。
所以我把列表统一改成 FRAME_BY_FRAME:ImageSource 在一次动图会话里保持有效,只在真正需要显示时解当前帧。
二、先读帧数和延迟,播放节奏不要写死
创建 ImageSource 后,我先读取帧数和 delay list。这样页面不会假设所有 WebP 都是 80 ms 一帧。
uint32_t frameCount = 0;
Image_ErrorCode code =
OH_ImageSourceNative_GetFrameCount(
source_, &frameCount);
if (code != IMAGE_SUCCESS || frameCount <= 1) {
return code;
}
std::vector<int32_t> delays(frameCount);
code = OH_ImageSourceNative_GetDelayTimeList(
source_, delays.data(), delays.size());
frameCount_ = frameCount;
delayList_ = std::move(delays);
这段代码解决播放元信息。本次文件共 18 帧,第 12 帧 delay 为 80 ms。
正式项目里还要给异常 delay 做保护,例如极端小值不能直接变成无限高刷新频率。我会在播放器层加业务下限,但原始 delay 仍保留在日志里。
三、真正的单帧解码只有三个关键动作
当前 Native 接口用 OH_DecodingOptions_SetIndex() 指定帧索引,再通过 OH_ImageSourceNative_CreatePixelmap() 得到该帧 PixelMap。
OH_PixelmapNative* AnimatedDecoder::DecodeFrame(
uint32_t index)
{
if (source_ == nullptr || index >= frameCount_) {
return nullptr;
}
OH_DecodingOptions* options = nullptr;
if (OH_DecodingOptions_Create(&options)
!= IMAGE_SUCCESS) {
return nullptr;
}
OH_DecodingOptions_SetIndex(options, index);
OH_PixelmapNative* next = nullptr;
Image_ErrorCode code =
OH_ImageSourceNative_CreatePixelmap(
source_, options, &next);
OH_DecodingOptions_Release(options);
return code == IMAGE_SUCCESS ? next : nullptr;
}
这里有两个资源边界很关键。
OH_DecodingOptions 属于一次解码调用,用完马上 Release;ImageSource 则要在逐帧解码期间保持有效,不能解完第一帧就释放。
四、内存真正降下来,是从“替换前释放上一帧”开始的
单帧解码改完后,我第一次测试仍看到内存缓慢上涨。原因很直接:新的 PixelMap 出来了,旧的 PixelMap 还被 Native 指针持有。
我把帧替换封装成一个明确动作:
Image_ErrorCode AnimatedDecoder::AdvanceTo(
uint32_t index)
{
OH_PixelmapNative* next = DecodeFrame(index);
if (next == nullptr) {
return IMAGE_BAD_PARAMETER;
}
if (current_ != nullptr) {
OH_PixelmapNative_Release(current_);
current_ = nullptr;
releasedFrames_++;
}
current_ = next;
currentIndex_ = index;
return IMAGE_SUCCESS;
}
如果新帧解码失败,旧帧继续保留,页面不会突然空白。
Demo 为了前后切帧更顺滑,没有只保留 1 帧,而是维持 3 帧小缓存:当前帧、前一帧、下一帧。超过 3 帧就按最久未使用顺序释放,所以最终 Cached Frames=3、Released Frames=15。
五、缓存控制放在播放器层,不要让每个卡片自己起计时器
长列表里最怕每个卡片维护自己的 timer 和缓存。组件一复用,就容易出现重复播放、旧 timer 回调新卡片等问题。
页面只发送 activate(assetId) / deactivate(assetId),播放器层维护当前活跃素材、帧序号和缓存上限。卡片离开可视区时,deactivate() 不只是暂停播放,也会释放不再需要的 PixelMap。
如果同一资源同时出现在两个位置,正式项目还应加引用计数,不能因为一个卡片离屏,就把另一个卡片正在显示的帧一起释放。
六、三种资源的结束时间一定要分开
这次最值得记住的是:
OH_DecodingOptions:一次调用级,用完即释放;OH_PixelmapNative:一帧显示对象,离开缓存窗口就释放;OH_ImageSourceNative:一次动图会话的源,逐帧播放期间保持有效。
页面真正退出或切换文件时,我才释放 ImageSource。ArkTS 层同时用 generation 丢掉页面销毁后晚到的旧结果。Native 释放解决内存占用,generation 解决旧异步结果回写,它们不是一回事。
七、调试页我改成了“按帧解码账本”
工程只保留 AnimatedFramePage.ets、FrameCache.ets、AnimatedPreview.ets 和 Native 的 AnimatedDecoder.cpp/.h。
HiLog 固定输出:
frameCount=18、delay[12]=80ms、decode frame=12 cost=11ms、cache=3 released=15、peakMemory=42MB、State: DECODING -> PLAYING。

八、最终验收我不再只看“动画顺不顺”
运行图里当前帧 12 / 18,状态 PLAYING,最近一次解码 11 ms。更重要的是内存数据:缓存 3 帧、累计释放 15 帧、峰值 42 MB。
如果动画很顺,但 Released Frames 一直是 0,我会直接判定实现不合格。列表场景里,内存曲线比单次播放帧率更能暴露生命周期问题。

九、Demo 到正式产品还有几个边界
第一,逐帧解码适合内存敏感的列表预览,不代表所有动图都应该这么做;详情页帧数少时,全量解码可能更简单。
第二,批量素材不要把 Native 解码压在 UI 主线程上,应该放到独立任务,再把结果交给 UI。
第三,快速滑动时不要追着每一次可见性变化立刻解码。可以加几十毫秒稳定窗口,一闪而过的卡片根本不启动动画。
第四,动图文件变化后要重新创建 ImageSource,旧 source 不能继续给新文件提供帧索引。
十、这次优化的核心,是让资源有明确主人
最后链路变成:
SOURCE_READY → DECODING → RELEASING → PLAYING
ImageSource 属于一次动图会话,PixelMap 属于缓存窗口,DecodingOptions 只属于一次解码调用。把三种资源的生命周期分开以后,长列表不再依赖“系统什么时候帮我回收”,峰值也从不可控变成可以验证的数字。
更多推荐


所有评论(0)