HarmonyOS 7 Image Kit + Image_NativeModule:动画 WebP 帧级解码中的按帧取样、PixelMap 释放与长列表内存峰值控制【鸿蒙心迹】
最近我在一个贴纸素材页里放了十几张动画 WebP。单独打开一张时没问题,真正放进长列表以后,内存曲线开始持续抬高:滑过几屏,峰值越来越高,返回上一页也降不下来。
最后定位到 Native 解码层。我原来的实现会一次性把一张动图解成完整 PixelMap 列表,页面只显示其中一帧,却长期持有整组对象。于是我把 Demo 改成 AnimatedFrameLab:固定文件 sticker_wave.webp,18 帧,当前第 12 帧,delay 80 ms;列表里只保留 3 帧缓存,累计释放 15 帧,峰值内存控制在 42 MB,最近一次单帧解码 11 ms。

一、列表预览不适合默认全量解码
独立详情页只有一个动图时,全量解码简单直接;长列表完全不同。十几张动画会同时经历“进入可视区、短暂停留、快速滑走、组件复用、再次进入”,只要每个卡片都持有完整 PixelMap 组,内存基线就会一点点抬高。
所以这次先换策略:列表统一使用 FRAME_BY_FRAME。ImageSource 在一次动图会话里保持有效,只在当前帧真正需要显示时解码对应索引。
二、先读取帧数和 delay,播放节奏不要写死
第一段 Native 代码解决的是播放元信息。页面不再假设所有 WebP 都是 80 ms 一帧,而是直接读取 frameCount 与 delay list。
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);
本次素材得到 Frame Count=18,第 12 帧 delay 为 80 ms。正式项目里我还会给异常 delay 增加业务下限,例如极端小值不能直接变成超高刷新频率;但原始 delay 会保留在日志里,方便判断到底是素材问题还是播放器问题。
三、按帧解码的关键不是 API 多,而是资源边界清楚
真正的单帧解码只做三件事:创建 DecodingOptions、设置 frame index、得到该帧 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 属于一次调用,用完就释放;ImageSource 不能在第一帧结束后释放,因为后续 frame request 还要继续从同一 source 取数据。
这里最容易出现的错误,是把所有 Native 对象都当成“页面结束时一起释放”。这样 DecodingOptions 会积压,PixelMap 也会越堆越多。
四、内存真正降下来,是在新帧接管前释放旧帧
按帧解码改完后,我第一次测试仍看到内存慢慢涨。原因是新 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 帧,而是保留当前帧、前一帧、后一帧三个小缓存。超过上限就按最久未使用顺序释放,因此最终 Cached Frames=3、Released Frames=15。
五、长列表里,缓存控制应该上移到播放器层
我最早让每个卡片自己维护 timer。组件一复用,就会出现旧 timer 回调新卡片、同一素材重复播放的问题。
现在页面只发 activate(assetId) 和 deactivate(assetId)。播放器负责当前帧、缓存上限、计时器和 Native 解码会话。卡片离开可视区时,不只是暂停,也会触发不再需要的 PixelMap 释放。
正式项目如果同一动画可能同时出现在两个位置,还要加引用计数,不能一个卡片离屏就把另一个卡片正在显示的帧释放掉。
六、三种资源必须有三种不同的结束时间
这次我最后把生命周期写成一句话:
DecodingOptions 属于一次 DecodeFrame 调用;PixelMap 属于缓存窗口;ImageSource 属于一次动图会话。
页面销毁或切换文件时,才统一释放 ImageSource。ArkTS 侧同时用 generation 丢弃页面销毁后晚到的旧异步结果。Native 资源释放解决内存占用,generation 解决旧回调穿透页面,两者缺一不可。
项目只保留 AnimatedFramePage.ets、FrameCache.ets、AnimatedPreview.ets 和 AnimatedDecoder.cpp/.h。HiLog 固定输出 frameCount=18、delay[12]=80ms、decode frame=12 cost=11ms、cache=3 released=15、peakMemory=42MB。

七、最终验收不再只看“动画顺不顺”
最终运行页里,当前帧 12 / 18,状态 PLAYING,最近一次解码 11 ms。真正要盯的是右上角三项:
- Cached Frames:
3 - Released Frames:
15 - Peak Memory:
42 MB
如果动画很顺,但 Released Frames 一直是 0,我会直接判定实现不合格。列表场景里,内存曲线比单次播放帧率更能暴露生命周期问题。

八、从 Demo 到正式产品,还要补几条边界
逐帧解码适合内存敏感的列表预览,不代表所有动图都该这么做。详情页只有一张动画、帧数又少时,全量解码可能更简单。
批量动图也不要把 Native 解码压在 UI 主线程上。真实素材库更适合放进独立任务,再把最终 PixelMap 结果交给 UI。
快速滑动时,不要追着每次可见性变化立即解码。可以给进入可视区增加短稳定窗口,一闪而过的卡片根本不启动动画。
动图文件变化后必须重新创建 ImageSource,旧 source 不能继续给新文件提供帧索引。
九、这次优化的核心,是让资源有明确主人
最终状态链路是:
SOURCE_READY → DECODING → RELEASING → PLAYING
把 ImageSource、PixelMap 和 DecodingOptions 的生命周期真正拆开以后,长列表不再依赖“系统什么时候顺手帮我回收”。峰值内存从一个模糊感觉,变成了可以直接写进验收条件的数字。
更多推荐




所有评论(0)