由于应用主打图文社交,最近总收到用户投诉,只要发动态时一口气选了 9 张手机拍摄的 4K 高清大图,应用必定闪退。抓取崩溃日志,清一色的 Out Of Memory。图片处理看似简单,实则是对应用内存管理的终极考验。

原图直接加载的死亡账单
很多开发者的惯性思维是直接把相册 URI 塞给 Image 组件,顶多改一下宽高属性。但这在 ArkUI 中是极其致命的。
一张 4000x3000 分辨率的手机原图,如果按默认的 RGBA_8888 格式解码,每个像素占用 4 个字节,加载进内存的瞬间,占用就是 4000 * 3000 * 4 字节,约等于 45MB。九宫格凑齐 9 张,内存直接爆炸。

降采样:解码层的物理缩容
我们必须让底层的解码器直接吐出缩略图,而不是先解压大图再缩小。这就需要借助 ImageSource 的 desiredSize 属性。

typescript

const imageSource = image.createImageSource(file.fd);
const decodingOptions: image.DecodingOptions = {
  desiredSize: { width: 300, height: 300 },
  // 警示:如果不需要透明度,改用 RGB_565 格式,每个像素只占2字节
  desiredPixelFormat: image.PixelMapFormat.RGB_565 
};
const pixelMap = await imageSource.createPixelMap(decodingOptions);

仅仅加上了这两行配置,内存占用立刻从 45MB 降到了不到 0.2MB。界面滑动也瞬间丝滑了。注意,这个处理过程必须在 TaskPool 中异步执行,否则解码时的 CPU 计算依然会卡住主线程。

跨语言对象的释放契约
在处理头像裁剪功能时,我发现了一个诡异的现象:即使使用了降采样,只要用户频繁地更换头像,应用的内存依然会在几分钟内缓慢攀升,直至崩溃。
深入底层发现,PixelMap 虽然是 ArkTS 对象,但它的真实数据块是由 C++ 渲染引擎管理的。ArkTS 的垃圾回收器(GC)管不到这块内存。
必须手动释放。每次赋新值前,必须显式调用 release()。

typescript

if (this.currentAvatar) {
  this.currentAvatar.release();
}
this.currentAvatar = newPixelMap;

压缩与文件重编码
本地展示没问题了,但在上传服务器时,原始大图依然会消耗用户大量的流量。我们引入了 ImagePacker,在端侧做一次质量为 80% 的 JPEG 重编码。这样一来,文件体积通常能缩小 80% 以上。但请注意,ImagePacker 同样极其消耗算力,并且它和 ImageSource 一样,用完必须调用 release() 释放资源。

写一个遍历相册的方法,分别用直接加载和降采样加载读取 50 张图片。通过 Profiler 监控内存曲线,看看如果不加 release(),内存是不是像脱缰的野马一样一去不复返。

Logo

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

更多推荐