HarmonyOS 的 Image 元件,相信大家平時用得還是挺開心的:一個 url
往裡一塞,咔咔就能顯示,啥也不用管,直接起飛。
但是,用著用著你可能會發現一些“奇妙體驗”——
這就很尷尬了,本來以為是“開箱即用”,結果掉坑裡了。
老樣子
如果您有任何疑問、對文章寫的不滿意、發現錯誤或者有更好的方法,如果你想支持下一期請務必點讚~,歡迎在評論、私信或郵件中提出,這真的對我很重要!非常感謝您的支持。🙏
Image模組提供了三級快取機制,解碼後的記憶體圖片快取、解碼前的資料快取、物理磁碟快取。在加載圖片時會逐級查找,如果在快取中找到之前加載過的圖片則提前返回對應的結果。
setImageCacheCount、setImageRawDataCacheSize、和setImageFileCacheSize這三個圖片快取介面的靈活性不足,後續不再演進,對於複雜情況,建議使用ImageKnife。(以上全是官方說的)
所以ImageKnife是如何將LRU算法、磁碟快取和記憶體優化融為一體,以及為什麼官方說複雜情況,建議使用ImageKnife呢。看官往下看。
ImageKnife採用了雙層快取架構,就像我們生活中的"短期記憶"和"長期存儲"一樣:
這種設計遵循了計算機科學中的快取層次結構原理,通過不同層級的快取來平衡訪問速度和存儲容量。
ImageKnife提供了三種快取策略,讓開發者可以根據需求靈活選擇:
export enum CacheStrategy {
// 默認-寫入/讀取記憶體和文件快取
Default = 0,
// 只寫入/讀取記憶體快取
Memory = 1,
// 只寫入/讀取文件快取
File = 2
}
可能有人會說,為什麼需要提供多種快取策略,可實際上對於任何事物,總會存在頻繁和不頻繁的情況,那麼頻繁訪問的圖片適合放在記憶體中,不常用的圖片更適合放在磁碟中。以及遇到大圖片那麼也可能不適合常駐在記憶體,你也不希望那樣吧?
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() // 確保記憶體不超限
}
}
可以看到核心代碼也非常簡單。
為了處理"更新現有快取"的場景。如果key已存在,我們不需要刪除其他條目,只需要替換現有值即可,所以在刪除舊快取時要先檢查!this.lruCache.contains(key)
Harmony非常貼心的給我們提供一個LRU的工具
LRUCache透過LinkedHashMap來實現LRU。LinkedHashMap繼承於HashMap,HashMap用於快速查找資料,LinkedHashMap雙向鏈表用於記錄資料的順序關係。因此,對於get()、put()、remove()等操作,LinkedHashMap除了包含HashMap的功能,還需要實現調整Entry順序鏈表的工作。其資料結構如下圖所示:
圖1 LRUCache的LinkedHashMap資料結構圖
LruCache中將LinkedHashMap的順序設置為LRU順序,鏈表頭部的物件為近期最少用到的物件。常用的方法及其說明如下所示:
(以上官方原話)
在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)
}
}
}
// 添加快取鍵值對,同時寫檔案
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?
讓我們綜合看看ImageKnife類是如何協調各個快取組件的:
ImageKnife採用了經典的三層架構設計,每一層都有明確的職責邊界:
這種分層設計讓系統具有了高內聚、低耦合的特性。每一層都可以獨立演進,不會因為其他層的變化而受到影響。
快取策略的選擇不是硬編碼的,而是通過策略模式實現的。開發者可以通過CacheStrategy枚舉選擇不同的快取行為:這種設計讓ImageKnife能夠適應不同的使用場景。
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的快取策略源碼,我們不僅看到了這是一個優秀的圖片快取系統,更看到了這是一個開源專案在可擴展性和設計理念上的卓越表現。
不僅僅是圖片。其實這樣的設計,我們完全可以當做輪子,用於我們自己專案的檔案管理、快取管理等等~拿來主義!
沒了
看這 -------------------->如果有想加入鴻蒙生態的大佬們,快來加入鴻蒙認證吧!初高級證書沒獲取的,點我!!!!!!!!,我真的很需要求求了!<-------------------- 看這