HarmonyOS开发中的图像缓存:LRU缓存策略、磁盘缓存、内存缓存、缓存淘汰、缓存预热

举报
Jack20 发表于 2026/06/21 11:25:30 2026/06/21
【摘要】 HarmonyOS开发中的图像缓存:LRU缓存策略、磁盘缓存、内存缓存、缓存淘汰、缓存预热核心要点:图像缓存是移动端性能优化的「命门」。本文从LRU淘汰策略出发,深入讲解HarmonyOS中内存缓存与磁盘缓存的双层架构设计,剖析缓存淘汰机制与预热策略,帮你彻底告别「图片加载慢、内存爆、流量贵」三大顽疾。项目说明核心API@ohos.file.cacheManager、@ohos.app.a...

HarmonyOS开发中的图像缓存:LRU缓存策略、磁盘缓存、内存缓存、缓存淘汰、缓存预热

核心要点:图像缓存是移动端性能优化的「命门」。本文从LRU淘汰策略出发,深入讲解HarmonyOS中内存缓存与磁盘缓存的双层架构设计,剖析缓存淘汰机制与预热策略,帮你彻底告别「图片加载慢、内存爆、流量贵」三大顽疾。

项目 说明
核心API @ohos.file.cacheManager、@ohos.app.ability.Context、LRUCache

一、背景与动机

你有没有这样的体验——打开一个图片密集型的App,首页瀑布流刷刷刷地加载,每张图都从网络拉取,流量哗哗地跑,加载速度还慢得让人抓狂。更离谱的是,你往回滑动的时候,刚才看过的图片又得重新下载一遍。

这不是个别现象,而是很多开发者的通病:只管加载,不管缓存

图像缓存的本质就是「用空间换时间」。把已经加载过的图片存起来,下次用的时候直接从本地读取,省流量、省时间、省电。但缓存又不是无脑存——手机存储有限、内存更有限,你得有一套科学的策略来决定「存什么、存多久、什么时候清」。

这就是今天要聊的核心:LRU缓存策略 + 双层缓存架构 + 缓存淘汰 + 缓存预热,一套组合拳打下来,你的图片加载体验能提升好几个档次。


二、核心原理

2.1 双层缓存架构

图像缓存通常采用「内存缓存 + 磁盘缓存」的双层架构。内存缓存快但容量小,磁盘缓存慢但容量大。两者配合,形成「先查内存、再查磁盘、最后网络」的三级查找链路。

flowchart TD
    classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
    classDef error fill:#EF5350,stroke:#C62828,color:#fff
    classDef info fill:#81C784,stroke:#388E3C,color:#000
    classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000

    A[请求图片] --> B{内存缓存命中?}
    B -- 命中 --> C[直接返回PixelMap]:::primary
    B -- 未命中 --> D{磁盘缓存命中?}
    D -- 命中 --> E[读取文件解码为PixelMap]:::info
    E --> F[写入内存缓存]:::primary
    F --> C
    D -- 未命中 --> G[网络下载图片]:::warning
    G --> H[写入磁盘缓存]:::info
    H --> I[解码为PixelMap]:::primary
    I --> J[写入内存缓存]:::primary
    J --> C

2.2 LRU缓存淘汰策略

LRU(Least Recently Used,最近最少使用)是缓存淘汰的经典算法。核心思想很简单:当缓存满了,把最久没被访问的数据踢出去

HarmonyOS的LRUCache就是基于这个原理实现的。它内部维护了一个双向链表 + 哈希表的结构:

  • 哈希表:O(1)时间查找key是否存在
  • 双向链表:O(1)时间完成节点的移动和删除

每次访问一个key,就把对应节点移到链表头部;插入新数据也放在头部;缓存满了就从链表尾部删除。

flowchart LR
    classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
    classDef error fill:#EF5350,stroke:#C62828,color:#fff
    classDef info fill:#81C784,stroke:#388E3C,color:#000
    classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000

    subgraph LRUCache工作流程
        A[访问Key] --> B{Key存在?}
        B ----> C[将节点移至链表头部]:::primary
        B ----> D{缓存已满?}
        D ----> E[删除链表尾部节点]:::error
        D ----> F[在链表头部插入新节点]:::info
        E --> F
    end

2.3 缓存预热

缓存预热是指在用户真正需要数据之前,提前把数据加载到缓存中。比如:

  • App启动时预加载首页图片
  • 列表滑动时预加载即将出现的图片
  • 根据用户行为预测可能访问的图片

这就像饭店在饭点之前就把热门菜备好,客人一来直接上菜,不用等。


三、代码实战

3.1 基于LRUCache的内存缓存管理器

这是整个缓存体系的核心——一个完整的内存缓存管理器,支持LRU淘汰、容量控制、命中率统计。

import { LRUCache } from '@kit.ArkTS';

/**
 * 图像内存缓存管理器
 * 基于LRU策略管理PixelMap缓存,控制内存占用
 */
export class ImageMemoryCache {
  // LRU缓存实例,key为图片URL的MD5摘要
  private lruCache: LRUCache<string, image.PixelMap>;
  // 缓存命中统计
  private hitCount: number = 0;
  private missCount: number = 0;
  // 最大缓存容量(条目数)
  private readonly MAX_CAPACITY: number = 100;

  constructor(capacity: number = 100) {
    this.MAX_CAPACITY = capacity;
    this.lruCache = new LRUCache<string, image.PixelMap>(capacity);
  }

  /**
   * 从内存缓存获取图片
   * @param key 图片唯一标识(通常为URL的哈希值)
   * @returns PixelMap或undefined
   */
  get(key: string): image.PixelMap | undefined {
    const pixelMap = this.lruCache.get(key);
    if (pixelMap !== undefined) {
      this.hitCount++;
      console.info(`[ImageCache] 内存缓存命中: ${key}`);
    } else {
      this.missCount++;
      console.info(`[ImageCache] 内存缓存未命中: ${key}`);
    }
    return pixelMap;
  }

  /**
   * 将图片写入内存缓存
   * @param key 图片唯一标识
   * @param pixelMap 图片像素数据
   */
  put(key: string, pixelMap: image.PixelMap): void {
    if (key === '' || pixelMap === undefined) {
      console.warn('[ImageCache] 无效的缓存写入参数');
      return;
    }
    this.lruCache.put(key, pixelMap);
    console.info(`[ImageCache] 写入内存缓存: ${key}, 当前容量: ${this.lruCache.length}/${this.MAX_CAPACITY}`);
  }

  /**
   * 移除指定缓存
   */
  remove(key: string): void {
    this.lruCache.remove(key);
  }

  /**
   * 清空所有缓存
   */
  clear(): void {
    this.lruCache.clear();
    this.hitCount = 0;
    this.missCount = 0;
    console.info('[ImageCache] 内存缓存已清空');
  }

  /**
   * 获取缓存命中率
   */
  getHitRate(): string {
    const total = this.hitCount + this.missCount;
    if (total === 0) return '0.00%';
    return ((this.hitCount / total) * 100).toFixed(2) + '%';
  }

  /**
   * 获取缓存统计信息
   */
  getStats(): object {
    return {
      capacity: this.MAX_CAPACITY,
      currentSize: this.lruCache.length,
      hitCount: this.hitCount,
      missCount: this.missCount,
      hitRate: this.getHitRate()
    };
  }
}

3.2 磁盘缓存管理器

磁盘缓存负责将图片文件持久化存储到应用沙箱目录,支持文件读写、容量限制和自动清理。

import { fileIo as fs } from '@kit.CoreFileKit';
import { hash } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';

/**
 * 图像磁盘缓存管理器
 * 将图片文件存储到应用沙箱目录,支持容量限制和LRU清理
 */
export class ImageDiskCache {
  private context: common.Context;
  // 磁盘缓存根目录
  private cacheDir: string = '';
  // 索引文件路径(记录访问时间,用于LRU淘汰)
  private indexFile: string = '';
  // 最大磁盘缓存大小(默认50MB)
  private readonly MAX_DISK_SIZE: number = 50 * 1024 * 1024;

  constructor(context: common.Context, maxDiskSize?: number) {
    this.context = context;
    if (maxDiskSize !== undefined) {
      this.MAX_DISK_SIZE = maxDiskSize;
    }
    this.initCacheDir();
  }

  /**
   * 初始化缓存目录
   */
  private initCacheDir(): void {
    // 使用应用的缓存目录
    this.cacheDir = this.context.cacheDir + '/image_cache';
    this.indexFile = this.cacheDir + '/cache_index.json';

    // 创建缓存目录
    if (!fs.accessSync(this.cacheDir)) {
      fs.mkdirSync(this.cacheDir, true);
      console.info('[DiskCache] 创建缓存目录: ' + this.cacheDir);
    }
    // 创建索引文件
    if (!fs.accessSync(this.indexFile)) {
      fs.writeTextSync(this.indexFile, JSON.stringify({}));
      console.info('[DiskCache] 创建索引文件');
    }
  }

  /**
   * 将URL转换为安全的文件名
   * 使用哈希算法避免特殊字符问题
   */
  private urlToFileName(url: string): string {
    // 简单哈希:将URL转为十六进制摘要
    let hash = 0;
    for (let i = 0; i < url.length; i++) {
      const char = url.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash; // 转为32位整数
    }
    return 'img_' + Math.abs(hash).toString(16) + '.cache';
  }

  /**
   * 读取索引文件
   */
  private readIndex(): Record<string, number> {
    try {
      const content = fs.readTextSync(this.indexFile);
      return JSON.parse(content) as Record<string, number>;
    } catch (e) {
      return {};
    }
  }

  /**
   * 写入索引文件
   */
  private writeIndex(index: Record<string, number>): void {
    fs.writeTextSync(this.indexFile, JSON.stringify(index));
  }

  /**
   * 从磁盘缓存获取图片数据
   * @param url 图片URL
   * @returns ArrayBuffer或null
   */
  get(url: string): ArrayBuffer | null {
    const fileName = this.urlToFileName(url);
    const filePath = this.cacheDir + '/' + fileName;

    if (!fs.accessSync(filePath)) {
      console.info('[DiskCache] 磁盘缓存未命中: ' + url);
      return null;
    }

    try {
      // 读取文件内容
      const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
      const stat = fs.statSync(filePath);
      const buffer = new ArrayBuffer(stat.size);
      fs.readSync(file.fd, buffer);
      fs.closeSync(file);

      // 更新访问时间(LRU)
      const index = this.readIndex();
      index[fileName] = Date.now();
      this.writeIndex(index);

      console.info('[DiskCache] 磁盘缓存命中: ' + url);
      return buffer;
    } catch (e) {
      console.error('[DiskCache] 读取缓存文件失败: ' + e);
      return null;
    }
  }

  /**
   * 将图片数据写入磁盘缓存
   * @param url 图片URL
   * @param data 图片二进制数据
   */
  put(url: string, data: ArrayBuffer): void {
    const fileName = this.urlToFileName(url);
    const filePath = this.cacheDir + '/' + fileName;

    try {
      // 写入文件
      const file = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
      fs.writeSync(file.fd, data);
      fs.closeSync(file);

      // 更新索引
      const index = this.readIndex();
      index[fileName] = Date.now();
      this.writeIndex(index);

      console.info('[DiskCache] 写入磁盘缓存: ' + url);

      // 检查磁盘容量,超限则清理
      this.evictIfNeeded();
    } catch (e) {
      console.error('[DiskCache] 写入缓存文件失败: ' + e);
    }
  }

  /**
   * 检查并清理磁盘缓存
   * 当总缓存大小超过阈值时,按LRU策略淘汰最久未访问的文件
   */
  private evictIfNeeded(): void {
    const index = this.readIndex();
    let totalSize = 0;

    // 计算当前缓存总大小
    const fileNames = Object.keys(index);
    for (const name of fileNames) {
      const filePath = this.cacheDir + '/' + name;
      try {
        if (fs.accessSync(filePath)) {
          const stat = fs.statSync(filePath);
          totalSize += stat.size;
        }
      } catch (e) {
        // 文件可能已被删除,从索引中移除
        delete index[name];
      }
    }

    // 未超限,无需清理
    if (totalSize <= this.MAX_DISK_SIZE) {
      return;
    }

    console.info(`[DiskCache] 缓存超限(${(totalSize / 1024 / 1024).toFixed(2)}MB),开始LRU淘汰`);

    // 按访问时间排序(升序,最旧的在前)
    const sortedEntries = Object.entries(index).sort((a, b) => a[1] - b[1]);

    // 逐个删除最旧的文件,直到总大小低于阈值的80%
    const targetSize = this.MAX_DISK_SIZE * 0.8;
    for (const [name, _] of sortedEntries) {
      if (totalSize <= targetSize) break;

      const filePath = this.cacheDir + '/' + name;
      try {
        if (fs.accessSync(filePath)) {
          const stat = fs.statSync(filePath);
          totalSize -= stat.size;
          fs.unlinkSync(filePath);
          delete index[name];
          console.info('[DiskCache] 淘汰缓存文件: ' + name);
        }
      } catch (e) {
        delete index[name];
      }
    }

    this.writeIndex(index);
  }

  /**
   * 清空所有磁盘缓存
   */
  clear(): void {
    const index = this.readIndex();
    for (const name of Object.keys(index)) {
      const filePath = this.cacheDir + '/' + name;
      try {
        if (fs.accessSync(filePath)) {
          fs.unlinkSync(filePath);
        }
      } catch (e) {
        // 忽略删除失败
      }
    }
    this.writeIndex({});
    console.info('[DiskCache] 磁盘缓存已清空');
  }
}

3.3 双层缓存 + 网络加载的完整图片加载器

把内存缓存和磁盘缓存组合起来,加上网络请求,就是一个完整的图片加载管线。同时支持缓存预热。

import { http } from '@kit.NetworkKit';
import { image } from '@kit.ImageKit';
import { common } from '@kit.AbilityKit';

/**
 * 图像双层缓存加载器
 * 整合内存缓存、磁盘缓存和网络请求,实现三级查找链路
 */
export class ImageCacheLoader {
  private memoryCache: ImageMemoryCache;
  private diskCache: ImageDiskCache;
  // 正在进行的请求,避免重复下载
  private pendingRequests: Map<string, Promise<image.PixelMap>> = new Map();

  constructor(context: common.Context) {
    this.memoryCache = new ImageMemoryCache(100);
    this.diskCache = new ImageDiskCache(context, 50 * 1024 * 1024);
  }

  /**
   * 加载图片(三级缓存链路)
   * @param url 图片URL
   * @param options 图片解码参数
   */
  async loadImage(url: string, options?: image.DecodingOptions): Promise<image.PixelMap> {
    // 第一级:内存缓存
    const memCached = this.memoryCache.get(url);
    if (memCached !== undefined) {
      return memCached;
    }

    // 第二级:磁盘缓存
    const diskData = this.diskCache.get(url);
    if (diskData !== null) {
      const pixelMap = await this.decodeToPixelMap(diskData, options);
      this.memoryCache.put(url, pixelMap);
      return pixelMap;
    }

    // 第三级:网络请求(防重复)
    if (this.pendingRequests.has(url)) {
      return this.pendingRequests.get(url)!;
    }

    const requestPromise = this.fetchFromNetwork(url, options);
    this.pendingRequests.set(url, requestPromise);

    try {
      const pixelMap = await requestPromise;
      return pixelMap;
    } finally {
      this.pendingRequests.delete(url);
    }
  }

  /**
   * 从网络下载图片
   */
  private async fetchFromNetwork(url: string, options?: image.DecodingOptions): Promise<image.PixelMap> {
    console.info('[ImageLoader] 网络请求: ' + url);

    const httpRequest = http.createHttp();
    try {
      const response = await httpRequest.request(url, {
        method: http.RequestMethod.GET,
        expectDataType: http.HttpDataType.ARRAY_BUFFER,
        connectTimeout: 10000,
        readTimeout: 10000
      });

      if (response.responseCode !== 200) {
        throw new Error(`HTTP错误: ${response.responseCode}`);
      }

      const imageData = response.result as ArrayBuffer;

      // 写入磁盘缓存
      this.diskCache.put(url, imageData);

      // 解码并写入内存缓存
      const pixelMap = await this.decodeToPixelMap(imageData, options);
      this.memoryCache.put(url, pixelMap);

      return pixelMap;
    } finally {
      httpRequest.destroy();
    }
  }

  /**
   * 将ArrayBuffer解码为PixelMap
   */
  private async decodeToPixelMap(data: ArrayBuffer, options?: image.DecodingOptions): Promise<image.PixelMap> {
    const imageSource = image.createImageSource(data);
    const decodingOptions: image.DecodingOptions = options ?? {
      editable: false,
      desiredSize: { width: 0, height: 0 },
      desiredPixelFormat: image.PixelMapFormat.RGBA_8888
    };
    const pixelMap = await imageSource.createPixelMap(decodingOptions);
    imageSource.release();
    return pixelMap;
  }

  /**
   * 缓存预热:提前加载图片到缓存
   * 适用于首页图片预加载场景
   * @param urls 需要预热的图片URL列表
   */
  async prewarm(urls: string[]): Promise<void> {
    console.info(`[ImageLoader] 开始缓存预热,共${urls.length}张图片`);
    const tasks: Promise<void>[] = [];

    for (const url of urls) {
      // 并发预热,但限制并发数
      tasks.push(this.loadImage(url).then(() => {
        console.info('[ImageLoader] 预热完成: ' + url);
      }).catch((err: Error) => {
        console.warn('[ImageLoader] 预热失败: ' + url + ', 原因: ' + err.message);
      }));
    }

    await Promise.allSettled(tasks);
    console.info('[ImageLoader] 缓存预热全部完成');
  }

  /**
   * 获取缓存统计信息
   */
  getCacheStats(): object {
    return {
      memory: this.memoryCache.getStats()
    };
  }

  /**
   * 清空所有缓存
   */
  clearAll(): void {
    this.memoryCache.clear();
    this.diskCache.clear();
  }
}

3.4 在UI组件中使用缓存加载器

import { image } from '@kit.ImageKit';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct ImageCacheDemoPage {
  // 缓存加载器实例
  private cacheLoader: ImageCacheLoader | null = null;
  // 图片列表
  @State imageList: string[] = [
    'https://example.com/photo1.jpg',
    'https://example.com/photo2.jpg',
    'https://example.com/photo3.jpg',
    'https://example.com/photo4.jpg',
    'https://example.com/photo5.jpg'
  ];
  // 已加载的PixelMap
  @State pixelMaps: Map<string, image.PixelMap> = new Map();
  // 加载状态
  @State loadingStates: Map<string, boolean> = new Map();
  // 缓存统计
  @State cacheStats: string = '';

  aboutToAppear(): void {
    // 初始化缓存加载器
    this.cacheLoader = new ImageCacheLoader(getContext(this) as common.Context);
    // 缓存预热:提前加载前3张图片
    this.prewarmCache();
  }

  /**
   * 缓存预热
   */
  async prewarmCache(): Promise<void> {
    if (this.cacheLoader === null) return;
    const prewarmUrls = this.imageList.slice(0, 3);
    await this.cacheLoader.prewarm(prewarmUrls);
    // 预热完成后自动加载
    for (const url of prewarmUrls) {
      this.loadAndDisplay(url);
    }
  }

  /**
   * 加载并显示图片
   */
  async loadAndDisplay(url: string): Promise<void> {
    if (this.cacheLoader === null) return;
    this.loadingStates.set(url, true);

    try {
      const pixelMap = await this.cacheLoader.loadImage(url);
      this.pixelMaps.set(url, pixelMap);
      this.loadingStates.set(url, false);
      // 更新缓存统计
      const stats = this.cacheLoader.getCacheStats();
      this.cacheStats = JSON.stringify(stats, null, 2);
    } catch (err) {
      this.loadingStates.set(url, false);
      console.error('图片加载失败: ' + url);
    }
  }

  build() {
    Column() {
      // 标题栏
      Text('图像缓存演示')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 16 })

      // 缓存统计面板
      Row() {
        Text('缓存统计')
          .fontSize(14)
          .fontColor('#999999')
        Text(this.cacheStats)
          .fontSize(12)
          .fontColor('#666666')
          .maxLines(3)
      }
      .width('100%')
      .padding(12)
      .backgroundColor('#1A1A2E')
      .borderRadius(8)
      .margin({ bottom: 16 })

      // 图片网格
      Grid() {
        ForEach(this.imageList, (url: string) => {
          GridItem() {
            Column() {
              if (this.pixelMaps.has(url)) {
                Image(this.pixelMaps.get(url))
                  .width('100%')
                  .height(160)
                  .objectFit(ImageFit.Cover)
                  .borderRadius(8)
              } else if (this.loadingStates.get(url) === true) {
                // 加载中占位
                Column() {
                  LoadingProgress()
                    .width(32)
                    .height(32)
                    .color('#4FC3F7')
                  Text('加载中...')
                    .fontSize(12)
                    .fontColor('#999999')
                    .margin({ top: 8 })
                }
                .width('100%')
                .height(160)
                .justifyContent(FlexAlign.Center)
                .backgroundColor('#1A1A2E')
                .borderRadius(8)
              } else {
                // 未加载占位
                Column() {
                  Text('点击加载')
                    .fontSize(14)
                    .fontColor('#4FC3F7')
                }
                .width('100%')
                .height(160)
                .justifyContent(FlexAlign.Center)
                .backgroundColor('#1A1A2E')
                .borderRadius(8)
                .onClick(() => this.loadAndDisplay(url))
              }
            }
          }
          .padding(4)
        })
      }
      .columnsTemplate('1fr 1fr')
      .rowsGap(8)
      .columnsGap(8)
      .width('100%')
      .height(400)

      // 操作按钮
      Row() {
        Button('全部加载')
          .backgroundColor('#4FC3F7')
          .fontColor('#000000')
          .onClick(() => {
            for (const url of this.imageList) {
              this.loadAndDisplay(url);
            }
          })

        Button('清空缓存')
          .backgroundColor('#EF5350')
          .fontColor('#FFFFFF')
          .onClick(() => {
            this.cacheLoader?.clearAll();
            this.pixelMaps.clear();
            this.loadingStates.clear();
            this.cacheStats = '缓存已清空';
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .margin({ top: 16 })
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#0D0D1A')
  }
}

四、踩坑与注意事项

4.1 PixelMap不能跨线程传递

这是一个非常常见的坑。PixelMap对象不能直接通过@SendableTaskPool跨线程传递。如果你在子线程解码了PixelMap,必须在线程内完成所有操作后再回到主线程使用。

解决方案:在子线程中只做网络请求和磁盘读写,将ArrayBuffer传回主线程后再解码为PixelMap。

4.2 内存缓存容量不是越大越好

LRUCache的容量设置需要权衡。容量太大,会导致内存占用过高,触发系统内存回收反而更慢;容量太小,缓存命中率低,失去了缓存的意义。

建议

  • 一般App:50-100条
  • 图片密集型App(如相册):200-300条
  • 根据实际图片大小动态调整,总内存占用建议不超过可用内存的1/8

4.3 磁盘缓存的线程安全问题

多个请求同时写入磁盘缓存时,可能出现文件读写冲突。索引文件的并发修改尤其危险。

解决方案

  • 使用@Sendable + TaskPool时,确保磁盘缓存操作在同一个串行队列中执行
  • 或者使用distributedKVStore替代手动管理索引文件

4.4 缓存Key的设计

直接用URL作为缓存Key可能不够安全。同一个URL可能返回不同内容(服务端更新了图片),或者URL中包含时效性参数。

建议

  • 使用URL + 内容哈希作为Key
  • 或者响应头中的ETag/Last-Modified来判断缓存是否过期
  • 对于CDN图片,URL通常就是稳定的,可以直接用

4.5 缓存预热不要阻塞启动

预热是好事,但不要在App启动时同步等待所有预热完成,否则启动速度会变慢。

建议:预热放在后台异步执行,不阻塞首屏渲染。首页先显示占位图,预热完成后自动替换。


五、HarmonyOS 6适配

5.1 LRUCache API变化

变化项 HarmonyOS 5 HarmonyOS 6
LRUCache构造参数 new LRUCache(capacity) 不变
contains方法 lruCache.contains(key) 不变
getCapacity方法 lruCache.getCapacity() 不变
trimToSize方法 新增trimToSize(newSize),主动缩减缓存

5.2 缓存目录变化

HarmonyOS 6中,应用沙箱的缓存目录路径规则不变,但系统对缓存目录的空间配额管理更加严格。当设备存储空间不足时,系统可能自动清理cacheDir下的文件。

迁移建议

  • 重要缓存数据不要只存在cacheDir,应同时备份到filesDirpreferences
  • 使用@ohos.file.statvfsAPI定期检查磁盘剩余空间
  • 监听@ohos.app.ability.Want中的存储空间广播

5.3 新增cacheManager API

HarmonyOS 6增强了@ohos.file.cacheManager模块,支持应用级缓存配额设置:

// HarmonyOS 6 新增API示例
import { cacheManager } from '@kit.CoreFileKit';

// 设置应用缓存配额
cacheManager.setCacheQuota({
  totalQuota: 100 * 1024 * 1024, // 100MB
  imageQuota: 50 * 1024 * 1024    // 图片缓存50MB
});

六、总结

知识点 核心内容
双层缓存架构 内存缓存(快、小)+ 磁盘缓存(慢、大),三级查找链路:内存→磁盘→网络
LRU淘汰策略 最近最少使用,双向链表+哈希表实现O(1)操作,满时淘汰链表尾部
内存缓存 使用LRUCache<string, PixelMap>,容量建议50-300条,注意PixelMap不能跨线程
磁盘缓存 文件存储+索引文件记录访问时间,超限时按LRU淘汰,注意线程安全
缓存预热 启动时异步预加载,不阻塞首屏,限制并发数避免资源争抢
缓存Key设计 URL+内容哈希,注意CDN参数和时效性参数
HarmonyOS 6 新增trimToSize方法、cacheManager配额管理,系统可能自动清理cacheDir

一句话总结:图像缓存不是「存起来就行」,而是一套从淘汰策略到预热调度的系统工程。LRU是骨架,双层缓存是血肉,预热是灵魂——三者缺一不可。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。