最近我在一个素材列表里放了十几张动画 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 只属于一次解码调用。把三种资源的生命周期分开以后,长列表不再依赖“系统什么时候帮我回收”,峰值也从不可控变成可以验证的数字。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐