4. 内存优化实战:图片内存优化、列表复用优化、对象池模式应用

内存优化这件事,说实话,是鸿蒙开发里最容易被忽视、但坑又最多的地方。我早期带团队做一款资讯类App时,就吃过图片内存的亏——滑动列表越来越卡,最后直接OOM崩溃。后来我花了整整两周,把图片加载、列表复用、对象池这三块彻底重构了一遍,才把内存峰值降了60%。今天我就把这套实战经验拆开来讲。

4.1 图片内存优化:别让一张图吃掉几十MB

图片是内存消耗的大户。一张1920x1080的图片,如果以ARGB_8888格式加载到内存里,占多大空间?算一下:1920 * 1080 * 4字节 ≈ 7.9MB。如果你一次加载20张,那就是160MB。嗯,很多App就是这么崩的。

4.1.1 图片压缩与采样

我个人的习惯是:永远不要直接加载原图。鸿蒙提供了ImageSourceDecodeOptions,可以指定采样率。

// 设置采样率,将图片缩小为1/4
let options = new image.DecodeOptions();
options.desiredSize = { width: 200, height: 200 }; // 指定目标尺寸
options.desiredPixelFormat = image.PixelMapFormat.RGBA_8888;

let imageSource = image.createImageSource(fileUri);
let pixelMap = await imageSource.createPixelMap(options);

这里有个坑:desiredSize不是直接缩放,而是让解码器按最接近的采样因子来解码。比如原图4000x3000,你设200x200,解码器会按20:1采样,内存占用直接降到原来的1/400。我在项目中遇到过有人设了desiredSize但忘记检查实际输出尺寸,结果内存还是爆了——因为采样因子没对齐。

避坑指南:我曾经在加载网络图片时,没做尺寸预判。一张用户头像原图是4K的,直接解码成PixelMap,内存瞬间飙到30MB。后来我强制对所有图片做最大尺寸限制:头像不超过120x120,列表缩略图不超过320x240。内存占用直接降了80%。

4.1.2 图片缓存策略

图片缓存,说白了就是「用空间换时间」。但空间是有限的,你得定好规则。我一般用两级缓存:内存缓存 + 磁盘缓存

缓存层级 容量建议 淘汰策略
内存缓存 App可用内存的1/8 LRU(最近最少使用)
磁盘缓存 50MB ~ 100MB 按文件最后访问时间淘汰

你想想看,如果每次滑动列表都重新解码图片,那性能肯定完蛋。内存缓存命中率如果能到90%以上,列表滑动基本就是秒开。我习惯用LruCache来实现,鸿蒙的@ohos.util.LruCache可以直接用。

import LruCache from '@ohos.util.LruCache';

let imageCache = new LruCache(50); // 最多缓存50张
imageCache.put('key_avatar', pixelMap);

// 获取时
let cached = imageCache.get('key_avatar');
if (cached) {
  // 直接使用缓存
} else {
  // 重新加载
}
小技巧:内存缓存不要只存PixelMap对象,还要考虑它占用的实际字节数。我一般会重写LruCachegetSize()方法,按PixelMap的字节数来计算,而不是按数量。这样更精确。

4.2 列表复用优化:让1000条数据只渲染10个Item

列表复用,说白了就是「循环利用」。你滑动列表时,离开屏幕的Item会被回收,新出现的Item直接复用它的布局。鸿蒙的List组件默认支持复用,但用不好照样卡。

4.2.1 正确使用ListItem复用

我见过很多新手在ListItem里动态创建组件,这是大忌。复用机制要求:布局结构固定,只更新数据

// 错误示范:每次创建新组件
build() {
  List() {
    ForEach(this.dataArray, (item) => {
      ListItem() {
        // 这里动态创建组件,复用失效
        this.createItemView(item);
      }
    })
  }
}

// 正确做法:固定布局,只更新数据
build() {
  List() {
    ForEach(this.dataArray, (item) => {
      ListItem() {
        ItemView({ data: item }) // ItemView是固定组件
      }
    })
  }
}

为什么会这样?因为鸿蒙的复用机制是基于组件树的。如果你每次返回不同的组件树,系统就没法复用,只能销毁重建。嗯,这里要注意:ForEach的key值也很关键。key要稳定且唯一,我习惯用数据项的id,而不是索引。

核心原则:列表复用优化的本质是「减少创建和销毁」。你想想看,如果1000条数据每次滑动都创建新组件,那内存和CPU都扛不住。复用做得好,内存占用基本是恒定的。

4.2.2 图片加载与列表滚动的协调

列表快速滑动时,如果每个Item都去加载图片,会造成「图片风暴」。我个人的解决方案是:滑动停止后再加载

// 监听列表滚动状态
List() {
  // ...
}
.onScroll((xOffset, yOffset, scrollState) => {
  if (scrollState === ScrollState.Idle) {
    // 停止滑动,开始加载可见区域的图片
    this.loadVisibleImages();
  } else {
    // 滑动中,暂停加载
    this.pauseImageLoading();
  }
})

这个优化看起来简单,但效果非常明显。我在项目中遇到过列表快速滑动时,图片加载任务堆积,导致UI线程卡顿。加上这个策略后,帧率从20fps直接提升到55fps。

4.3 对象池模式应用:减少频繁创建对象的开销

对象池,说白了就是「用完别扔,洗洗再用」。频繁创建和销毁对象,会触发GC(垃圾回收),GC一多,界面就卡。我习惯在以下场景使用对象池:

  • 消息对象:比如自定义事件、通知数据
  • 临时数据结构:比如坐标点、矩形区域
  • 渲染相关对象:比如画笔、矩阵

4.3.1 实现一个简单的对象池

class ObjectPool<T> {
  private pool: T[] = [];
  private factory: () => T;
  private reset: (obj: T) => void;

  constructor(factory: () => T, reset: (obj: T) => void, initialSize: number = 10) {
    this.factory = factory;
    this.reset = reset;
    // 预创建对象
    for (let i = 0; i < initialSize; i++) {
      this.pool.push(this.factory());
    }
  }

  acquire(): T {
    if (this.pool.length > 0) {
      return this.pool.pop()!;
    }
    return this.factory(); // 池为空时创建新对象
  }

  release(obj: T): void {
    this.reset(obj); // 重置状态
    this.pool.push(obj);
  }
}

使用方式很简单:

// 假设我们频繁创建坐标点
let pointPool = new ObjectPool(
  () => { return { x: 0, y: 0 }; },
  (p) => { p.x = 0; p.y = 0; },
  20
);

// 使用时
let p = pointPool.acquire();
p.x = 100;
p.y = 200;
// 用完后归还
pointPool.release(p);
我的经验:对象池不是万能的。如果对象创建成本很低,或者使用频率不高,用对象池反而增加复杂度。我一般只在「每秒创建超过100次」的场景下使用。比如列表滑动时的坐标计算、动画帧的临时数据。

4.3.2 对象池的避坑指南

我曾经在项目里用过对象池,结果内存泄漏了。原因是:对象归还后,还有外部引用持有它。你想想看,池里的对象被误用了,数据就乱了。

避坑指南:我曾经在对象池里存放了Bitmap对象,归还时没有清空像素数据。结果下一个使用者拿到的是脏数据,显示出了上一帧的残留图像。后来我在reset函数里强制清空所有字段,才解决这个问题。记住:归还对象时,一定要彻底重置

4.4 本章小结:内存优化的三个抓手

图片内存优化、列表复用优化、对象池模式,这三块是内存优化的核心。我个人的经验是:先做图片优化,收益最大;再做列表复用,解决滑动卡顿;最后用对象池,压榨极致性能。

你想想看,如果一张图省下7MB,一个列表省下100个Item的创建,一个对象池省下每秒100次的GC——加起来,你的App想不流畅都难。

一句话总结:内存优化的本质是「减少浪费」。图片别加载原图,列表别创建新组件,对象别频繁new。做到这三点,你的鸿蒙App内存表现不会差。
内存优化实战 · 知识体系 内存优化三大抓手 图片内存优化 • 图片压缩与采样 • 内存缓存 + 磁盘缓存 • LRU淘汰策略 列表复用优化 • 固定布局结构 • 唯一且稳定的key • 滑动停止再加载 对象池模式 • 预创建 + 复用 • acquire / release • 彻底重置防脏数据 减少浪费 = 减少创建 + 减少销毁 + 减少GC
专注资料整理