一、release 路径已经实现,但不能把两轮成功写成“无泄漏”

动图魔方的抽帧与导出代码已经存在明确释放路径:探测帧释放、AVImageGenerator 释放、帧数组在 finally 中逐个释放,帧处理器创建的临时 PixelMap 也在各自作用域清理。模拟器同一进程内连续完成两次短片段导出,作品页同时回读 8 帧与 9 帧两条记录。

同一进程两轮真实导出作品

这组证据能证明释放代码真实参与了可运行链路,也能证明第二轮任务没有因为第一轮资源失效而立即失败;它不能证明所有内存都回落,更不能证明不存在泄漏。

二、视频生成器由外层 finally 兜底

视频抽帧器创建 AVImageGenerator 后,在最外层 finally 中调用 release()。无论中途取消、取帧失败还是调用方异常,都不会跳过生成器清理。

const generator = await media.createAVImageGenerator()
try {
  generator.fdSrc = sourceFd
  // 探测并逐帧获取 PixelMap
  return frames
} finally {
  await generator.release()
}

探测阶段使用的 PixelMap 不进入正式帧数组,读取尺寸后立即释放:

const probeFrame = await generator.fetchFrameByTime(startUs, option, {})
const probeInfo = await probeFrame.getImageInfo()
await probeFrame.release()

这两个释放点分别控制生成器和探测帧,职责不同。只释放 generator 不能替代单帧释放,只释放单帧也不能关闭媒体生成器。

三、正式帧数组在导出服务中统一释放

当前视频路径仍会一次返回 PixelMap[]ExportService 接管数组后,用 try/finally 保证无论处理与编码是否成功,都会进入 releasePixelMaps()

const pixelMaps = await VideoFrameExtractor.extract(
  sourceUri,
  duration,
  fps,
  signal,
  options
)
try {
  return await ExportService.encodePixelMaps(pixelMaps, preset, signal)
} finally {
  await ExportService.releasePixelMaps(pixelMaps)
}
private static async releasePixelMaps(
  pixelMaps: image.PixelMap[]
): Promise<void> {
  for (const pixelMap of pixelMaps) {
    try {
      await pixelMap.release()
    } catch (_) {
      // 单帧释放失败不阻断其余帧清理
    }
  }
}

逐个捕获释放异常很重要。如果第一帧释放失败就直接抛出,后面的几十帧会全部滞留。

四、帧处理器也清理临时对象

除了抽出的原始帧,缩放、裁剪、字幕与颜色处理还可能创建临时 PixelMap。项目的 FrameProcessor 在复制像素或转换完成后调用 pixelMap.release(),避免把短生命周期对象交给更外层。

对象 创建位置 当前释放位置
探测帧 VideoFrameExtractor 读取尺寸后
正式视频帧 VideoFrameExtractor ExportService finally
AVImageGenerator VideoFrameExtractor 外层 finally
图片/临时处理帧 FrameProcessor 对应处理作用域
GIF 字节与 RGB 数组 JS 内存 依赖引用清理与 GC

最后一行是当前文章必须保留的边界:PixelMap 释放并不等于 JavaScript 大数组立即回收。RGB、索引帧和编码缓冲仍可能占用内存。

五、连续两轮导出的实际观测

第一轮使用 0.56 秒、15fps、标清,完成 8 帧、480×270、1.1MB。第二轮使用 0.61 秒、15fps、标清,完成 9 帧、480×270、1.3MB。

第二轮导出前:PID 1799,RSS 271336
第二轮授权保存后:PID 1799,RSS 275608
作品列表:视频转GIF_2(9帧)+ 视频转GIF_1(8帧)

PID 一致说明进程没有重启。RSS 增加约 4MB,没有在第二轮结束后立刻回落到更低值,因此本文不能下“内存完全回收”结论。更准确的说法是:释放路径存在,连续短任务可完成,长期稳定性仍需更多轮次和峰值采样。

六、长任务暴露的是主线程阻塞

5.1 秒、15fps、标清任务预计 75 帧。故障日志记录了抽帧进度和大量 PixelMap 创建,随后出现:

Reason: THREAD_BLOCK_6S
App main thread is not response!

因此现阶段最优先的问题不是再加一个 release(),而是改变执行模型。抽帧与 GIF 编码不能长时间占据主线程;即使最终能够释放资源,主线程先被系统判定无响应,用户仍会看到应用退桌面。

建议把改造顺序固定为:

  1. 重任务迁移到 Worker/TaskPool。
  2. 改为小批次处理,不同时持有全部 PixelMap。
  3. 每批完成后释放并让出主线程。
  4. 采集处理前、峰值、释放后和下一轮开始前四个内存点。
  5. 连续执行至少三轮,再讨论“稳定区间”。

七、证据边界与验收条件

当前可验收的是:短片段能连续导出;第二轮进程未重启;作品列表能回读两条;源码有 probe、generator 和正式帧释放路径。尚未验收的是:长片段稳定、取消路径稳定、坏帧后释放、三轮内存回落和后台切换。

PixelMap 生命周期相关接口可参考华为 HarmonyOS 图像开发指导

八、总结

动图魔方已经把 PixelMap 与 AVImageGenerator 的释放职责落到了真实代码,并在同一进程完成两轮短片段导出。与此同时,75 帧任务的 THREAD_BLOCK_6S 说明释放正确不等于执行模型正确。下一阶段应把重点放在分批、异步和主线程让出,再用多轮峰值数据验证稳定性。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐