HarmonyOS7 缓存不是越多越好:图片、数据、视图多层缓存策略这样定
文章目录前言缓存分层设计思路L1 内存缓存LRUCache 封装图片缓存组件数据缓存接口响应 过期策略CacheManager统一入口踩坑提醒小结前言做过列表页的同学都有体会用户上下滑动的时候图片反复加载、接口反复请求列表卡顿流量还哗哗地跑。体验差到想摔手机。这些问题的根源就一个——没有缓存。或者更准确地说没有一套分层合理的缓存策略。今天来聊聊怎么在组件层面搭建内存→磁盘→网络的多层缓存体系。缓存分层设计思路经典的三层缓存模型内存缓存L1速度最快容量有限App 退出就没了磁盘缓存L2速度中等容量大持久化存储网络请求L3速度最慢数据最新鲜每次取数据按 L1 → L2 → L3 的顺序找哪层命中就返回同时回填上层缓存。写入时反向先写 L3如果是接口再同步到 L2 和 L1。L1 内存缓存LRUCache 封装内存缓存的核心是 LRULeast Recently Used策略容量满了就淘汰最久没用的。ArkTS 里Map本身就是按插入顺序迭代的可以利用这个特性实现 LRU// cache/LRUCache.etsexportclassLRUCacheK,V{privatecache:MapK,VnewMap()privatemaxSize:numberconstructor(maxSize:number){this.maxSizemaxSize}get(key:K):V|undefined{constvaluethis.cache.get(key)if(value!undefined){// 移到末尾最近使用this.cache.delete(key)this.cache.set(key,value)}returnvalue}put(key:K,value:V):void{if(this.cache.has(key)){this.cache.delete(key)}elseif(this.cache.sizethis.maxSize){// 淘汰最久没用的Map 第一个元素constoldestKeythis.cache.keys().next().valueif(oldestKey!undefined){this.cache.delete(oldestKey)}}this.cache.set(key,value)}has(key:K):boolean{returnthis.cache.has(key)}remove(key:K):void{this.cache.delete(key)}clear():void{this.cache.clear()}getsize():number{returnthis.cache.size}}这个实现利用了Map的有序性delete再set就能把元素移到末尾keys().next().value拿到的就是最久没访问的元素。图片缓存组件图片是缓存需求最强烈的场景。基于 LRUCache 封装一个图片缓存// cache/ImageCache.etsimport{image}fromkit.ImageKitinterfaceCachedImage{pixelMap:image.PixelMap width:numberheight:numbersizeBytes:number}exportclassImageCache{privatestaticinstance:ImageCache// 内存缓存最多缓存 50 张图privatememoryCache:LRUCachestring,CachedImagenewLRUCache(50)// 磁盘缓存目录privatediskCacheDir:stringstaticgetInstance():ImageCache{if(!ImageCache.instance){ImageCache.instancenewImageCache()}returnImageCache.instance}asyncinitDiskCache(context:Context):Promisevoid{this.diskCacheDircontext.cacheDir/images// 确保目录存在constfsawaitimport(kit.CoreFileKit)if(!fs.fs.accessSync(this.diskCacheDir)){fs.fs.mkdirSync(this.diskCacheDir,true)}}asyncgetImage(url:string):Promiseimage.PixelMap|null{// L1: 内存缓存constmemCachedthis.memoryCache.get(url)if(memCached){console.info([ImageCache] L1 命中:${url})returnmemCached.pixelMap}// L2: 磁盘缓存constdiskResultawaitthis.loadFromDisk(url)if(diskResult){console.info([ImageCache] L2 命中:${url})this.memoryCache.put(url,diskResult)returndiskResult.pixelMap}// L3: 网络下载console.info([ImageCache] L3 网络请求:${url})constdownloadedawaitthis.downloadAndCache(url)returndownloaded}privateasyncloadFromDisk(url:string):PromiseCachedImage|null{try{constfileNamethis.urlToFileName(url)constfilePath${this.diskCacheDir}/${fileName}constfsawaitimport(kit.CoreFileKit)if(fs.fs.accessSync(filePath)){constbufferfs.fs.readFileSync(filePath)constsource:image.ImageSourceimage.createImageSource(buffer.buffer)constpixelMapawaitsource.createPixelMap()return{pixelMap,width:0,height:0,sizeBytes:buffer.byteLength}}}catch(e){// 磁盘缓存未命中正常流程}returnnull}privateasyncdownloadAndCache(url:string):Promiseimage.PixelMap|null{try{consthttpawaitimport(kit.NetworkKit)constresponseawaithttp.http.createHttp().request(url)constdataresponse.resultasArrayBuffer// 写入磁盘缓存constfileNamethis.urlToFileName(url)constfilePath${this.diskCacheDir}/${fileName}constfsawaitimport(kit.CoreFileKit)fs.fs.writeFileSync(filePath,data)// 写入内存缓存constsourceimage.createImageSource(data)constpixelMapawaitsource.createPixelMap()constcached:CachedImage{pixelMap,width:0,height:0,sizeBytes:data.byteLength}this.memoryCache.put(url,cached)returnpixelMap}catch(e){console.error([ImageCache] 下载失败:${url},${e})returnnull}}privateurlToFileName(url:string):string{// 简单哈希避免文件名冲突和过长lethash0for(leti0;iurl.length;i){hash((hash5)-hash)url.charCodeAt(i)hash|0}return${Math.abs(hash).toString(16)}.cache}}配合 UI 组件使用// components/CachedImage.etsComponentexportstruct CachedImage{Propurl:stringPropplaceholderColor:string#E0E0E0StatepixelMap:image.PixelMap|nullnullStateisLoading:booleantrueaboutToAppear():void{this.loadImage()}asyncloadImage():Promisevoid{this.isLoadingtruethis.pixelMapawaitImageCache.getInstance().getImage(this.url)this.isLoadingfalse}build(){Stack(){if(this.isLoading){// 占位图Column().width(100%).height(100%).backgroundColor(this.placeholderColor).borderRadius(8)}elseif(this.pixelMap){Image(this.pixelMap).width(100%).height(100%).objectFit(ImageFit.Cover).borderRadius(8)}}}}数据缓存接口响应 过期策略接口数据不能无限期缓存需要加过期时间。封装一个带 TTL 的数据缓存// cache/DataCache.etsinterfaceCacheEntryT{data:Ttimestamp:numberttl:number// 毫秒}exportclassDataCache{privatememoryStore:Mapstring,CacheEntryanynewMap()privatediskPath:stringasyncinit(context:Context):Promisevoid{this.diskPathcontext.cacheDir/data_cache.jsonawaitthis.loadFromDisk()}// 写入缓存ttl 默认 5 分钟setT(key:string,data:T,ttl:number5*60*1000):void{constentry:CacheEntryT{data,timestamp:Date.now(),ttl}this.memoryStore.set(key,entry)this.persistToDisk()}// 读取缓存自动检查过期getT(key:string):T|null{constentrythis.memoryStore.get(key)if(!entry)returnnullconstisExpiredDate.now()-entry.timestampentry.ttlif(isExpired){this.memoryStore.delete(key)console.info([DataCache] 缓存过期已清除:${key})returnnull}returnentry.dataasT}// 获取缓存如果过期就执行 refresh 函数获取新数据asyncgetOrRefreshT(key:string,refresh:()PromiseT,ttl:number5*60*1000):PromiseT{constcachedthis.getT(key)if(cached!null)returncachedconstfreshDataawaitrefresh()this.set(key,freshData,ttl)returnfreshData}// 清理所有过期缓存purgeExpired():number{letcount0constnowDate.now()this.memoryStore.forEach((entry,key){if(now-entry.timestampentry.ttl){this.memoryStore.delete(key)count}})returncount}clear():void{this.memoryStore.clear()this.persistToDisk()}privateasyncloadFromDisk():Promisevoid{try{constfsawaitimport(kit.CoreFileKit)if(fs.fs.accessSync(this.diskPath)){constcontentfs.fs.readTextFileSync(this.diskPath)constdataJSON.parse(content)asRecordstring,CacheEntryany// 恢复时过滤掉已过期的constnowDate.now()for(constkeyofObject.keys(data)){if(now-data[key].timestampdata[key].ttl){this.memoryStore.set(key,data[key])}}}}catch(e){console.warn([DataCache] 磁盘缓存加载失败使用空缓存)}}privateasyncpersistToDisk():Promisevoid{try{constfsawaitimport(kit.CoreFileKit)constdata:Recordstring,CacheEntryany{}this.memoryStore.forEach((entry,key){data[key]entry})fs.fs.writeFileSync(this.diskPath,JSON.stringify(data))}catch(e){console.warn([DataCache] 磁盘缓存持久化失败)}}}getOrRefresh是最实用的方法——有缓存用缓存没缓存就自动拉取并缓存业务代码用起来特别省心constproductsawaitdataCache.getOrRefresh(product_list,()api.fetchProducts(),10*60*1000// 10 分钟过期)CacheManager统一入口把图片缓存和数据缓存统一管理// cache/CacheManager.etsexportclassCacheManager{privatestaticinstance:CacheManagerprivateimageCache:ImageCacheImageCache.getInstance()privatedataCache:DataCachenewDataCache()staticgetInstance():CacheManager{if(!CacheManager.instance){CacheManager.instancenewCacheManager()}returnCacheManager.instance}asyncinit(context:Context):Promisevoid{awaitthis.imageCache.initDiskCache(context)awaitthis.dataCache.init(context)// 启动时清理过期缓存this.dataCache.purgeExpired()}getImage(url:string):Promiseimage.PixelMap|null{returnthis.imageCache.getImage(url)}getDataT(key:string):T|null{returnthis.dataCache.getT(key)}setDataT(key:string,data:T,ttl?:number):void{this.dataCache.set(key,data,ttl)}getOrRefreshT(key:string,refresh:()PromiseT,ttl?:number):PromiseT{returnthis.dataCache.getOrRefresh(key,refresh,ttl)}// 清理全部缓存clearAll():void{this.imageCache.clear()this.dataCache.clear()}}踩坑提醒内存缓存别贪大。图片缓存 50 张看着不多如果是高清大图内存占用轻松上几百 MB。建议根据图片平均大小动态调整上限或者限制总字节数而不是条目数。磁盘缓存要定期清理。用户如果一直不清缓存磁盘占用会持续增长。可以在 App 启动时检查磁盘缓存大小超过阈值就清理最老的一半。注意线程安全。ArkTS 单线程模型下问题不大但如果用了 TaskPool多个线程同时读写磁盘缓存文件就可能出问题。加个简单的锁或者队列就行。小结多层缓存不是越多越好关键是根据场景选择合适的缓存策略。图片适合 LRU 内存缓存 磁盘持久化接口数据适合 TTL 过期策略频繁创建的组件适合复用池。我自己的习惯是在项目初期就把CacheManager搭好所有网络请求都走缓存层。前期多花半天时间后期能省掉大量性能优化的工作。用户体验也明显更好——页面秒开滑动流畅流量还省了一大半。

相关新闻