动图魔方 HarmonyOS 设计(22):PixelMap 生命周期与内存释放

📅 2026/7/25 1:51:13 👁️ 阅读次数 📝 编程学习
动图魔方 HarmonyOS 设计(22):PixelMap 生命周期与内存释放

一、先算清一张图真正占多少内存

图像处理最危险的误判是只看文件大小。一个几百 KB 的压缩图片解码成 RGBA 后,内存约为宽 × 高 × 4字节;四千像素长边的图片可能瞬间占用数十 MB。视频抽出多帧、字幕再创建一张临时画布、量化阶段保留 RGB 副本时,峰值会继续叠加。

本设计关注PixelMap从创建、读取、转换到释放的所有权,不讨论具体滤镜视觉效果。目标是让图片序列、视频帧和 GIF 解码帧遵守同一种资源协议,并用预算决定能否继续,而不是等进程接近 OOM 才被动失败。

资源估算方式所有者最晚释放点
原始 PixelMapwidth × height × 4解码/抽帧阶段RGB 转换完成
ImageSource实现相关创建它的局部作用域PixelMap 创建完成后
RGB 工作帧width × height × 3帧处理阶段索引帧生成后
字幕 PixelMapwidth × height × 4叠层构建函数像素读回后
索引帧width × heightGIF 编码阶段文件写入完成

二、所有权协议比 release 调用数量更重要

每个资源在任意时刻只能有一个明确所有者。函数返回PixelMap表示所有权转移给调用方;函数只读取但不接管时,参数名和接口说明必须标记 borrowed。数组中存放多帧并不会自动说明谁释放,必须把协议写进类型或封装。

export interface OwnedPixelMap { pixelMap: PixelMap owner: 'decoder' | 'processor' | 'overlay' released: boolean } export async function releaseOwned(resource: OwnedPixelMap): Promise<void> { if (resource.released) return resource.released = true await resource.pixelMap.release() } export interface PixelMapConsumer<T> { consume(resource: OwnedPixelMap): Promise<T> }

released只用于开发期防止重复释放,不能替代正确的作用域设计。生产代码更推荐将资源封装在一次性对象中,并减少把原始PixelMap暴露给多个模块的机会。UI 如果需要预览,应接收独立缩略图或稳定 URI,而不是借用正在处理的原始帧。

三、单图路径采用最短资源作用域

图片序列应逐张解码、转换为较小的 RGB 工作帧,然后立即释放原图。不要先把所有 URI 解码成数组再处理。ImageSourcePixelMap都放入try/finally,任何转换、读取或取消异常都不会跳过清理。

export async function decodeToRgbFrame( uri: string, options: FrameOptions, signal: ExportSignal ): Promise<RgbFrame> { const source = image.createImageSource(uri) let pixelMap: PixelMap | null = null try { signal.checkCancelled() pixelMap = await source.createPixelMap({ desiredPixelFormat: image.PixelMapFormat.RGBA_8888 }) return await copyToScaledRgb(pixelMap, options, signal) } finally { if (pixelMap !== null) { await pixelMap.release().catch(() => {}) } await source.release().catch(() => {}) } }

工作帧只保存 RGB 字节、输出尺寸和帧延迟。完成这次复制后,后续裁剪、滤镜与量化都不再依赖原始 PixelMap。这样资源边界清楚,也能把多张原图的峰值压缩为“一张原图 + 已缩小 RGB 帧集合”。

四、视频帧使用消费即释放的交接

视频抽帧器取得帧后交给处理器,处理器把像素复制到目标尺寸并在finally中释放。若取消发生在获取之后、消费之前,抽帧器仍是所有者;交接成功后,释放责任转到处理器。这个转换点需要有明确代码,而不是靠注释约定。

export async function processVideoFrame( frame: PixelMap, options: FrameOptions, signal: ExportSignal ): Promise<RgbFrame> { try { signal.checkCancelled() const info = await frame.getImageInfo() const budget = estimateRgbaBytes(info.size.width, info.size.height) signal.assertMemoryBudget(budget) return await copyToScaledRgb(frame, options, signal) } finally { await frame.release().catch(() => {}) } } export async function consumeFrames(frames: AsyncIterable<PixelMap>): Promise<void> { for await (const frame of frames) { const rgb = await processVideoFrame(frame, currentOptions(), exportSignal) rgbQueue.push(rgb) await yieldToUi() } }

如果平台接口一次返回帧数组,调用方要在任何中途失败时释放“当前帧以及尚未处理的剩余帧”。更理想的改造是让抽帧器提供异步迭代器,从源头限制同时存活的 PixelMap 数量。

五、内存预算在创建前做决策

预算器根据源帧、目标 RGB、字幕叠层和索引帧估算本阶段峰值。估算不追求和系统分配完全相等,而是用于做确定性决策:降低长边、减少帧数、关闭字幕预览或拒绝继续。所有降级都要反馈给用户,不能静默输出低于选择档位的结果。

export interface MemoryPlan { sourceBytes: number rgbBytes: number overlayBytes: number indexedBytes: number estimatedPeakBytes: number action: 'accept' | 'downscale' | 'reduce_frames' | 'reject' } export function planMemory(input: MemoryInput, limitBytes: number): MemoryPlan { const sourceBytes = input.sourceWidth * input.sourceHeight * 4 const rgbBytes = input.frameCount * input.targetWidth * input.targetHeight * 3 const overlayBytes = input.hasSubtitle ? input.targetWidth * input.targetHeight * 4 : 0 const indexedBytes = input.frameCount * input.targetWidth * input.targetHeight const estimatedPeakBytes = sourceBytes + rgbBytes + overlayBytes + indexedBytes return { sourceBytes, rgbBytes, overlayBytes, indexedBytes, estimatedPeakBytes, action: chooseMemoryAction(estimatedPeakBytes, limitBytes) } }

预算器还应记录实际目标尺寸、帧数和采取的降级动作,供测试与故障定位使用。日志不能包含用户文件路径,只记录匿名任务标识、数值和阶段。

六、临时画布也必须进入清理清单

字幕、绘制和颜色转换常会创建临时 PixelMap。因为它们生命周期很短,反而容易在异常分支中遗漏。构建叠层时先创建、绘制、读回,再释放;绘制失败可以返回null并跳过字幕,但绝不能带着未释放画布继续编码。

异常位置仍可能存活的资源清理动作结果策略
createPixelMap 失败ImageSource释放 Source终止当前图片
readPixels 失败PixelMap、Source两者都释放报告坏帧
字幕绘制失败临时 PixelMap释放临时画布可降级为无字幕
调色板量化取消RGB 帧集合清空大数组引用任务取消
GIF 写入失败索引帧、文件句柄释放并关闭文件保留编辑参数

七、数组和缓存在阶段结束时主动瘦身

JavaScript 引用仍存在时,即使原始 PixelMap 已释放,RGB 与索引数组也会继续占用内存。每帧完成索引化后,把对应 RGB 字段替换为空数组;编码完成后清空索引帧列表和调色板引用。不要依赖函数结束后某个不确定时间点的垃圾回收。

缓存只保存可复用的小对象,例如尺寸计算、颜色索引映射和参数归一化结果。任何以任务 ID 缓存整帧像素的方案都必须设置容量、过期和取消清理。预览缩略图与导出原帧使用不同资源,关闭预览时释放缩略图,不影响导出任务。

八、验证要观察峰值和清理结果

实施顺序建议为:标注现有所有权;给每个创建点补齐finally;改成逐帧消费;加入内存预算;最后处理字幕和编码阶段的大数组清理。每完成一层都运行同一组素材,比较峰值而不是只看是否导出成功。

验收矩阵包含单张 4K 图片、五十张图片、十秒 1080p 视频、取消任务、坏帧和字幕绘制失败。真机证据需要记录处理前、峰值、结束后三个内存点,连续执行三轮后内存应回落到稳定区间;同时验证取消后不再报告进度、文件可重新选择、下一次任务可正常启动。PixelMap 操作能力可参考华为 HarmonyOS 开发文档。

九、总结

PixelMap 内存治理不是在代码末尾多写几个release(),而是建立可追踪的所有权、尽量缩短原图作用域、用背压限制并发存活帧,并在创建前执行预算。只有资源和字节数组都按阶段交接与清理,长视频和多图任务才有机会在真实设备上稳定运行。