站長阿川

站長阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

站長精心設計,帶你實作 63 個小專案,得到作品集!

立即開始免費試讀!

HarmonyOS5 一頓飯時間 —— LRU、磁碟快取與記憶體優化的結合

一、前言

HarmonyOS 的 Image 元件,相信大家平時用得還是挺開心的:一個 url 往裡一塞,咔咔就能顯示,啥也不用管,直接起飛。

但是,用著用著你可能會發現一些“奇妙體驗”——

  • 圖片只加載了一半,像是網斷了。
  • 圖片加載失敗了,但是給你一個毫無用處的錯誤碼和錯誤信息。
  • 最難搞的是,快取你根本沒法控制,它就那麼一直卡著,死活不重新拉。

這就很尷尬了,本來以為是“開箱即用”,結果掉坑裡了。

老樣子

如果您有任何疑問、對文章寫的不滿意、發現錯誤或者有更好的方法,如果你想支持下一期請務必點讚~,歡迎在評論私信郵件中提出,這真的對我很重要!非常感謝您的支持。🙏

二、關於Image快取

Image的快取策略

Image模組提供了三級快取機制,解碼後的記憶體圖片快取、解碼前的資料快取、物理磁碟快取。在加載圖片時會逐級查找,如果在快取中找到之前加載過的圖片則提前返回對應的結果。

Image元件如何配置打開和關閉快取

  • 記憶體圖片快取:透過setImageCacheCount介面打開快取,如果希望每次連網都獲取最新資源,可以不設置或設置為0不快取。
  • 磁碟快取:磁碟快取是默認開啟的,默認值為100M,可以將setImageFileCacheSize的值設置為0關閉磁碟快取。
  • 解碼前資料快取:透過setImageRawDataCacheSize設置記憶體中快取解碼前圖片資料的大小上限,單位為字節,提升再次加載同源圖片的加載速度。如果不設置則默認為0,不進行快取。

setImageCacheCount、setImageRawDataCacheSize、和setImageFileCacheSize這三個圖片快取介面的靈活性不足,後續不再演進,對於複雜情況,建議使用ImageKnife。(以上全是官方說的)

所以ImageKnife是如何將LRU算法、磁碟快取和記憶體優化融為一體,以及為什麼官方說複雜情況,建議使用ImageKnife呢。看官往下看。

三、快取策略基礎概念

ImageKnife採用了雙層快取架構,就像我們生活中的"短期記憶"和"長期存儲"一樣:

  • 記憶體快取(Memory Cache):相當於短期記憶,訪問速度極快,但容量有限
  • 磁碟快取(File Cache):相當於長期存儲,容量大但訪問速度相對較慢

這種設計遵循了計算機科學中的快取層次結構原理,通過不同層級的快取來平衡訪問速度和存儲容量。

快取策略枚舉

ImageKnife提供了三種快取策略,讓開發者可以根據需求靈活選擇:

export enum CacheStrategy {
  // 默認-寫入/讀取記憶體和文件快取
  Default = 0,
  // 只寫入/讀取記憶體快取
  Memory = 1,
  // 只寫入/讀取文件快取
  File = 2
}

可能有人會說,為什麼需要提供多種快取策略,可實際上對於任何事物,總會存在頻繁和不頻繁的情況,那麼頻繁訪問的圖片適合放在記憶體中,不常用的圖片更適合放在磁碟中。以及遇到大圖片那麼也可能不適合常駐在記憶體,你也不希望那樣吧?

四、LRU記憶體快取機制

LRU,相信每個開發者都非常了解。Least Recently Used算法就像圖書館的借閱系統:最近被借閱的書籍放在最顯眼的位置,長期沒人問津的書籍會被移到角落,甚至下架。這樣可以確保最常用的資源始終在快速訪問範圍內。

在ImageKnife中,我們可以找到顯眼的

private memoryCache: IMemoryCache = new MemoryLruCache(256, 128 * 1024 * 1024);

讓我們看看MemoryLruCache的核心實現:

export class MemoryLruCache implements IMemoryCache {
  maxMemory: number = 0          // 最大記憶體限制
  currentMemory: number = 0      // 當前已使用記憶體
  maxSize: number = 0            // 最大快取條目數
  private lruCache: util.LRUCache<string, ImageKnifeData>  // 鴻蒙系統LRU快取

  // 添加快取的核心邏輯
  put(key: string, value: ImageKnifeData): void {
    let size = this.getImageKnifeDataSize(value)

    // 如果快取已滿,按LRU方式刪除最舊的條目
    if (this.lruCache.length == this.maxSize && !this.lruCache.contains(key)) {
      this.remove(this.lruCache.keys()[0])  // 刪除第一個(最舊的)
    } else if (this.lruCache.contains(key)) {
      this.remove(key)  // 如果key已存在,先刪除舊的
    }

    this.lruCache.put(key, value)
    this.currentMemory += size
    this.trimToSize()  // 確保記憶體不超限
  }
}

可以看到核心代碼也非常簡單。

  1. 容量檢查:this.lruCache.length == this.maxSize 檢查快取條目數是否達到上限
  2. LRU刪除:this.lruCache.keys()[0] 獲取最舊的快取條目
  3. 記憶體管理:trimToSize() 確保記憶體使用量不超過設定閾值

為了處理"更新現有快取"的場景。如果key已存在,我們不需要刪除其他條目,只需要替換現有值即可,所以在刪除舊快取時要先檢查!this.lruCache.contains(key)

LRUCache

Harmony非常貼心的給我們提供一個LRU的工具

LRUCache透過LinkedHashMap來實現LRU。LinkedHashMap繼承於HashMap,HashMap用於快速查找資料,LinkedHashMap雙向鏈表用於記錄資料的順序關係。因此,對於get()、put()、remove()等操作,LinkedHashMap除了包含HashMap的功能,還需要實現調整Entry順序鏈表的工作。其資料結構如下圖所示:

圖1 LRUCache的LinkedHashMap資料結構圖
點擊放大

LruCache中將LinkedHashMap的順序設置為LRU順序,鏈表頭部的物件為近期最少用到的物件。常用的方法及其說明如下所示:

  • 调用get()方法:根據key查詢對應,如果沒有查到則返回null。查詢到對應的物件後,將該物件移到鏈表的尾端,並返回查詢的物件。
  • 调用put()方法:將key-value對添加到快取中,同時將新物件存儲在鏈表尾端。當記憶體快取達到最大值時,移除鏈表頭部的物件。如果key已存在,則更新其對應的value。
  • 调用remove()方法:刪除key對應的快取value,如果key對應的value不在,則返回為null,否則,返回已刪除的key-value鍵值對。
  • 调用updateCapacity()方法,設置快取存儲容量。如果新容量小於原容量,僅保留新容量大小的資料。

(以上官方原話)

可擴展性

在MemoryLruCache,我們可以看到實現了IMemoryCache,而且ImageKnife中也是

private memoryCache: IMemoryCache = new MemoryLruCache(256, 128 * 1024 * 1024);

所以是支持我們自定義擴展的,只需要我們實現IMemoryCache~

/**
 * 設置自定義的記憶體快取
 * @param newMemoryCache 自定義記憶體快取
 */
initMemoryCache(newMemoryCache: IMemoryCache): void {
  this.memoryCache = newMemoryCache
}

五、磁碟快取與檔案管理

磁碟快取就像是一個智能的檔案櫃系統:不僅要知道檔案在哪裡,還要知道哪些檔案最常用,哪些可以清理。

很容易的,我們能觀察到FileCache這個檔案:

讓我們看看FileCache的實現:

export class FileCache {
  private lruCache: util.LRUCache<string, number>
  // 初始化快取目錄,掃描現有檔案
  public async initFileCache(path: string = FileCache.CACHE_FOLDER) {
    // 遍歷快取目錄下的檔案,按照時間順序加入快取
    let filenames: string[] = await FileUtils.getInstance().ListFile(this.path)

    // 按照檔案創建時間排序
    let cachefiles: CacheFileInfo[] = []
    for (let i = 0; i < filenames.length; i++) {
      let stat: fs.Stat | undefined = await FileUtils.getInstance().Stat(this.path + filenames[i])
      cachefiles.push({
        file: filenames[i],
        ctime: stat === undefined ? 0 : stat.ctime,  // 檔案創建時間
        size: stat?.size ?? 0
      })
    }

    // 按時間排序,確保LRU順序
    let sortedCachefiles: CacheFileInfo[] = cachefiles.sort((a, b) => a.ctime - b.ctime)

    // 將檔案信息加入LRU快取
    for (let i = 0; i < sortedCachefiles.length; i++) {
      this.lruCache.put(sortedCachefiles[i].file, fileSize)
      this.addMemorySize(fileSize)
    }
  }
}
  1. 啟動時掃描:應用啟動時掃描快取目錄,重建LRU快取狀態
  2. 時間排序:按檔案創建時間排序,確保LRU順序正確
  3. 記憶體同步:LRU快取記錄檔案大小,用於記憶體使用量統計

快取寫入策略

// 添加快取鍵值對,同時寫檔案
put(key: string, value: ArrayBuffer): void {
  // LRU容量管理
  if (this.lruCache.length == this.maxSize && !this.lruCache.contains(key)) {
    this.remove(this.lruCache.keys()[0])  // 刪除最舊的檔案
  }

  let pre = this.lruCache.put(key, value.byteLength)
  FileUtils.getInstance().writeDataSync(this.path + key, value)  // 同步寫入檔案

  if (pre !== undefined) {
    this.addMemorySize(value)  // 更新記憶體統計
  }
  this.trimToSize()  // 確保不超限
}

有兩個小細節:LRU快取在主執行緒管理,避免並發衝突,檔案讀寫在子執行緒進行,不阻塞UI。

作為一個程序員,我們必須有拿來主義的精神,實際上不僅僅是圖片,大家可以看到兩個地方都有lruCache,而ImageKnife給我們提供了記憶體和檔案的Lru實現。我們完全可以用到我們的下載等框架裡面使用~

不同快取策略

// 從記憶體或檔案快取中獲取圖片資料
getCacheImage(loadSrc: string, cacheType: CacheStrategy = CacheStrategy.Default, signature?: string): Promise<ImageKnifeData | undefined> {
  return new Promise((resolve, reject) => {
    if (cacheType == CacheStrategy.Memory) {
      // 只從記憶體快取獲取
      resolve(this.readMemoryCache(loadSrc, option, engineKeyImpl))
    } else if (cacheType == CacheStrategy.File) {
      // 只從檔案快取獲取
      this.readFileCache(loadSrc, engineKeyImpl, resolve)
    } else {
      // 默認策略:先查記憶體,再查磁碟
      let data = this.readMemoryCache(loadSrc, option, engineKeyImpl)
      data == undefined ? this.readFileCache(loadSrc, engineKeyImpl, resolve) : resolve(data)
    }
  })
}

// 預加載快取,支持不同策略
putCacheImage(url: string, pixelMap: PixelMap, cacheType: CacheStrategy = CacheStrategy.Default, signature?: string) {
  let memoryKey = this.getEngineKeyImpl().generateMemoryKey(url, ImageKnifeRequestSource.SRC, { loadSrc: url, signature: signature });
  let fileKey = this.getEngineKeyImpl().generateFileKey(url, signature);

  switch (cacheType) {
    case CacheStrategy.Default:
      // 同時存入記憶體和磁碟
      this.saveMemoryCache(memoryKey, imageKnifeData);
      this.saveFileCache(fileKey, this.pixelMapToArrayBuffer(pixelMap));
      break;
    case CacheStrategy.File:
      // 只存入磁碟
      this.saveFileCache(fileKey, this.pixelMapToArrayBuffer(pixelMap));
      break;
    case CacheStrategy.Memory:
      // 只存入記憶體
      this.saveMemoryCache(memoryKey, imageKnifeData);
      break;
  }
}

可以看到ImageKnife,支持三種不同的快取策略,滿足不同需求,可以在運行時動態選擇快取策略,無需重新初始化。

那在實際應用中,什麼時候應該使用CacheStrategy.Memory,什麼時候使用CacheStrategy.File?

  • Memory策略:適用於頻繁訪問的小圖片,如用戶頭像、圖標等
  • File策略:適用於大圖片或不常用圖片,如背景圖、詳情圖等
  • Default策略:適用於大多數場景,在性能和容量之間取得平衡

六、快取架構的整體設計

讓我們綜合看看ImageKnife類是如何協調各個快取組件的:
Untitled diagram _ Mermaid Chart-2025-08-26-061155

分層架構的層次關係

ImageKnife採用了經典的三層架構設計,每一層都有明確的職責邊界:

  • 應用層:負責用戶交互和配置管理
  • 策略層:負責快取策略決策和任務分發
  • 快取層:負責資料存儲和檢索
  • 加載層:負責從不同資料源獲取圖片
  • 存儲層:提供物理存儲支持

這種分層設計讓系統具有了高內聚、低耦合的特性。每一層都可以獨立演進,不會因為其他層的變化而受到影響。

快取策略的選擇不是硬編碼的,而是通過策略模式實現的。開發者可以通過CacheStrategy枚舉選擇不同的快取行為:這種設計讓ImageKnife能夠適應不同的使用場景。

Untitled diagram _ Mermaid Chart-2025-08-26-054159

責任鏈模式的負載均衡

ImageKnifeDispatcher作為系統的"交通指揮中心",採用了責任鏈模式來管理圖片加載請求:

// 策略選擇影響整個快取流程
switch (cacheType) {
  case CacheStrategy.Default:   // 平衡策略
  case CacheStrategy.Memory:    // 速度優先
  case CacheStrategy.File:      // 容量優先
}

這種設計確保了系統在高併發情況下的穩定性,就像高速公路的匝道控制,避免交通擁堵。

架構的擴展性設計

除了當前的場景,肯定要考慮可擴展性。

插件化的加載策略

ImageKnife通過工廠模式實現了加載策略的插件化:

// 檢查重複請求,避免重複下載
checkRepeatRequests(request: ImageKnifeRequest, imageSrc: string | ImageKnifeRequestSource): ImageKnifeCheckRequest | undefined {
  // 如果併發請求數達到上限,加入排隊隊列
  if (this.executingJobMap.length >= this.maxRequests && !this.executingJobMap.get(memoryKey)) {
    this.jobQueue.add(request)
    return
  }
}

這種設計讓開發者可以輕鬆添加新的圖片加載方式,比如從資料庫加載、從加密存儲加載等。

快取介面的抽象化

透過IMemoryCache介面,ImageKnife為快取實現提供了統一的抽象:

export class ImageLoaderFactory {
  static getLoaderStrategy(request: RequestJobRequest): IImageLoaderStrategy | null {
    if (request.customGetImage !== undefined) {
      return new CustomLoaderStrategy();        // 自定義加載
    }
    if (request.src.startsWith('http://')) {
      return new HttpLoaderStrategy();          // HTTP加載
    }
    if (request.src.startsWith('file://')) {
      return new FileSystemLoaderStrategy();    // 檔案系統加載
    }
    // ... 其他策略
  }
}

這種抽象讓系統可以輕鬆支持不同的快取實現,比如Redis快取、記憶體映射快取等。

七、小結

透過深入分析ImageKnife的快取策略源碼,我們不僅看到了這是一個優秀的圖片快取系統,更看到了這是一個開源專案在可擴展性和設計理念上的卓越表現。

  • 自定義快取策略:實現自己的記憶體管理算法
  • 替換核心組件:在不影響其他代碼的情況下更換快取實現
  • 擴展功能:添加新的快取特性,如快取統計、性能監控等

不僅僅是圖片。其實這樣的設計,我們完全可以當做輪子,用於我們自己專案的檔案管理、快取管理等等~拿來主義!

八、總結

沒了

看這 -------------------->如果有想加入鴻蒙生態的大佬們,快來加入鴻蒙認證吧!初高級證書沒獲取的,點我!!!!!!!!,我真的很需要求求了!<-------------------- 看這


原文出處:https://juejin.cn/post/7542529184570671144


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
站長阿川

站長阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

站長精心設計,帶你實作 63 個小專案,得到作品集!

立即開始免費試讀!