当前位置: 首页 > news >正文

HarmonyOS7 缓存不是越多越好:图片、数据、视图多层缓存策略这样定

文章目录

    • 前言
    • 缓存分层设计思路
    • L1 内存缓存:LRUCache 封装
    • 图片缓存组件
    • 数据缓存:接口响应 + 过期策略
    • CacheManager:统一入口
    • 踩坑提醒
    • 小结

前言

做过列表页的同学都有体会:用户上下滑动的时候,图片反复加载、接口反复请求,列表卡顿,流量还哗哗地跑。体验差到想摔手机。

这些问题的根源就一个——没有缓存。或者更准确地说,没有一套分层合理的缓存策略。今天来聊聊怎么在组件层面搭建内存→磁盘→网络的多层缓存体系。

缓存分层设计思路

经典的三层缓存模型:

  1. 内存缓存(L1):速度最快,容量有限,App 退出就没了
  2. 磁盘缓存(L2):速度中等,容量大,持久化存储
  3. 网络请求(L3):速度最慢,数据最新鲜

每次取数据,按 L1 → L2 → L3 的顺序找,哪层命中就返回,同时回填上层缓存。写入时反向,先写 L3(如果是接口),再同步到 L2 和 L1。

L1 内存缓存:LRUCache 封装

内存缓存的核心是 LRU(Least Recently Used)策略,容量满了就淘汰最久没用的。ArkTS 里Map本身就是按插入顺序迭代的,可以利用这个特性实现 LRU:

// cache/LRUCache.etsexportclassLRUCache<K,V>{privatecache:Map<K,V>=newMap()privatemaxSize:numberconstructor(maxSize:number){this.maxSize=maxSize}get(key:K):V|undefined{constvalue=this.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.size>=this.maxSize){// 淘汰最久没用的(Map 第一个元素)constoldestKey=this.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的有序性,deleteset就能把元素移到末尾,keys().next().value拿到的就是最久没访问的元素。

图片缓存组件

图片是缓存需求最强烈的场景。基于 LRUCache 封装一个图片缓存:

// cache/ImageCache.etsimport{image}from'@kit.ImageKit'interfaceCachedImage{pixelMap:image.PixelMap width:numberheight:numbersizeBytes:number}exportclassImageCache{privatestaticinstance:ImageCache// 内存缓存:最多缓存 50 张图privatememoryCache:LRUCache<string,CachedImage>=newLRUCache(50)// 磁盘缓存目录privatediskCacheDir:string=''staticgetInstance():ImageCache{if(!ImageCache.instance){ImageCache.instance=newImageCache()}returnImageCache.instance}asyncinitDiskCache(context:Context):Promise<void>{this.diskCacheDir=context.cacheDir+'/images'// 确保目录存在constfs=awaitimport('@kit.CoreFileKit')if(!fs.fs.accessSync(this.diskCacheDir)){fs.fs.mkdirSync(this.diskCacheDir,true)}}asyncgetImage(url:string):Promise<image.PixelMap|null>{// L1: 内存缓存constmemCached=this.memoryCache.get(url)if(memCached){console.info(`[ImageCache] L1 命中:${url}`)returnmemCached.pixelMap}// L2: 磁盘缓存constdiskResult=awaitthis.loadFromDisk(url)if(diskResult){console.info(`[ImageCache] L2 命中:${url}`)this.memoryCache.put(url,diskResult)returndiskResult.pixelMap}// L3: 网络下载console.info(`[ImageCache] L3 网络请求:${url}`)constdownloaded=awaitthis.downloadAndCache(url)returndownloaded}privateasyncloadFromDisk(url:string):Promise<CachedImage|null>{try{constfileName=this.urlToFileName(url)constfilePath=`${this.diskCacheDir}/${fileName}`constfs=awaitimport('@kit.CoreFileKit')if(fs.fs.accessSync(filePath)){constbuffer=fs.fs.readFileSync(filePath)constsource:image.ImageSource=image.createImageSource(buffer.buffer)constpixelMap=awaitsource.createPixelMap()return{pixelMap,width:0,height:0,sizeBytes:buffer.byteLength}}}catch(e){// 磁盘缓存未命中,正常流程}returnnull}privateasyncdownloadAndCache(url:string):Promise<image.PixelMap|null>{try{consthttp=awaitimport('@kit.NetworkKit')constresponse=awaithttp.http.createHttp().request(url)constdata=response.resultasArrayBuffer// 写入磁盘缓存constfileName=this.urlToFileName(url)constfilePath=`${this.diskCacheDir}/${fileName}`constfs=awaitimport('@kit.CoreFileKit')fs.fs.writeFileSync(filePath,data)// 写入内存缓存constsource=image.createImageSource(data)constpixelMap=awaitsource.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{// 简单哈希,避免文件名冲突和过长lethash=0for(leti=0;i<url.length;i++){hash=((hash<<5)-hash)+url.charCodeAt(i)hash|=0}return`${Math.abs(hash).toString(16)}.cache`}}

配合 UI 组件使用:

// components/CachedImage.ets@Componentexportstruct CachedImage{@Propurl:string=''@PropplaceholderColor:string='#E0E0E0'@StatepixelMap:image.PixelMap|null=null@StateisLoading:boolean=trueaboutToAppear():void{this.loadImage()}asyncloadImage():Promise<void>{this.isLoading=truethis.pixelMap=awaitImageCache.getInstance().getImage(this.url)this.isLoading=false}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.etsinterfaceCacheEntry<T>{data:Ttimestamp:numberttl:number// 毫秒}exportclassDataCache{privatememoryStore:Map<string,CacheEntry<any>>=newMap()privatediskPath:string=''asyncinit(context:Context):Promise<void>{this.diskPath=context.cacheDir+'/data_cache.json'awaitthis.loadFromDisk()}// 写入缓存,ttl 默认 5 分钟set<T>(key:string,data:T,ttl:number=5*60*1000):void{constentry:CacheEntry<T>={data,timestamp:Date.now(),ttl}this.memoryStore.set(key,entry)this.persistToDisk()}// 读取缓存,自动检查过期get<T>(key:string):T|null{constentry=this.memoryStore.get(key)if(!entry)returnnullconstisExpired=Date.now()-entry.timestamp>entry.ttlif(isExpired){this.memoryStore.delete(key)console.info(`[DataCache] 缓存过期,已清除:${key}`)returnnull}returnentry.dataasT}// 获取缓存,如果过期就执行 refresh 函数获取新数据asyncgetOrRefresh<T>(key:string,refresh:()=>Promise<T>,ttl:number=5*60*1000):Promise<T>{constcached=this.get<T>(key)if(cached!==null)returncachedconstfreshData=awaitrefresh()this.set(key,freshData,ttl)returnfreshData}// 清理所有过期缓存purgeExpired():number{letcount=0constnow=Date.now()this.memoryStore.forEach((entry,key)=>{if(now-entry.timestamp>entry.ttl){this.memoryStore.delete(key)count++}})returncount}clear():void{this.memoryStore.clear()this.persistToDisk()}privateasyncloadFromDisk():Promise<void>{try{constfs=awaitimport('@kit.CoreFileKit')if(fs.fs.accessSync(this.diskPath)){constcontent=fs.fs.readTextFileSync(this.diskPath)constdata=JSON.parse(content)asRecord<string,CacheEntry<any>>// 恢复时过滤掉已过期的constnow=Date.now()for(constkeyofObject.keys(data)){if(now-data[key].timestamp<=data[key].ttl){this.memoryStore.set(key,data[key])}}}}catch(e){console.warn('[DataCache] 磁盘缓存加载失败,使用空缓存')}}privateasyncpersistToDisk():Promise<void>{try{constfs=awaitimport('@kit.CoreFileKit')constdata:Record<string,CacheEntry<any>>={}this.memoryStore.forEach((entry,key)=>{data[key]=entry})fs.fs.writeFileSync(this.diskPath,JSON.stringify(data))}catch(e){console.warn('[DataCache] 磁盘缓存持久化失败')}}}

getOrRefresh是最实用的方法——有缓存用缓存,没缓存就自动拉取并缓存,业务代码用起来特别省心:

constproducts=awaitdataCache.getOrRefresh('product_list',()=>api.fetchProducts(),10*60*1000// 10 分钟过期)

CacheManager:统一入口

把图片缓存和数据缓存统一管理:

// cache/CacheManager.etsexportclassCacheManager{privatestaticinstance:CacheManagerprivateimageCache:ImageCache=ImageCache.getInstance()privatedataCache:DataCache=newDataCache()staticgetInstance():CacheManager{if(!CacheManager.instance){CacheManager.instance=newCacheManager()}returnCacheManager.instance}asyncinit(context:Context):Promise<void>{awaitthis.imageCache.initDiskCache(context)awaitthis.dataCache.init(context)// 启动时清理过期缓存this.dataCache.purgeExpired()}getImage(url:string):Promise<image.PixelMap|null>{returnthis.imageCache.getImage(url)}getData<T>(key:string):T|null{returnthis.dataCache.get<T>(key)}setData<T>(key:string,data:T,ttl?:number):void{this.dataCache.set(key,data,ttl)}getOrRefresh<T>(key:string,refresh:()=>Promise<T>,ttl?:number):Promise<T>{returnthis.dataCache.getOrRefresh(key,refresh,ttl)}// 清理全部缓存clearAll():void{this.imageCache.clear()this.dataCache.clear()}}

踩坑提醒

内存缓存别贪大。图片缓存 50 张看着不多,如果是高清大图,内存占用轻松上几百 MB。建议根据图片平均大小动态调整上限,或者限制总字节数而不是条目数。

磁盘缓存要定期清理。用户如果一直不清缓存,磁盘占用会持续增长。可以在 App 启动时检查磁盘缓存大小,超过阈值就清理最老的一半。

注意线程安全。ArkTS 单线程模型下问题不大,但如果用了 TaskPool,多个线程同时读写磁盘缓存文件就可能出问题。加个简单的锁或者队列就行。

小结

多层缓存不是越多越好,关键是根据场景选择合适的缓存策略。图片适合 LRU 内存缓存 + 磁盘持久化,接口数据适合 TTL 过期策略,频繁创建的组件适合复用池。

我自己的习惯是在项目初期就把CacheManager搭好,所有网络请求都走缓存层。前期多花半天时间,后期能省掉大量性能优化的工作。用户体验也明显更好——页面秒开,滑动流畅,流量还省了一大半。

http://www.gsyq.cn/news/1615982.html

相关文章:

  • VSCode JSON 样式
  • 2026年6月份化工储存用玻璃钢储罐,源头生产企业该如何筛选
  • 亿俐缇国际物流(YLT GLOBAL)——中东双清包税门到门物流服务的优势与特点
  • Spring Cloud Alibaba 生产级实战:16 个模块覆盖全栈微服务
  • 亲测有效:瑜伽缓解腰痛的南湖实践分享
  • 预约小程序怎么搭建?全球5款工具实测:餐宝盈/BBWEYY/比文云/Brizy/PageXL(2026年7月更新),含零代码SAAS、AI编程、源码定制交付
  • STC3115+PIC18F97J94电池监控系统设计与优化
  • 四个看不见的成本漏洞,系统一个一个帮你堵上
  • HarmonyOS7 购物车看着简单最容易翻车:增删改、全选、价格计算一篇讲透
  • 云尖信息参编《Token驱动智能经济研究报告》正式发布
  • 恶意软件窃取 Chrome 会话 Cookie 的攻击机制与防御研究
  • CVE-2025-12108漏洞应急响应实战:从情报研判到深度防御的完整指南
  • AI写了60%的代码,你的研发周期却没变短?问题不在AI,在你对“写代码”的理解
  • 如何在Mac上实现优雅的桌面歌词显示:LyricsX完全指南
  • Trae界面闪烁?一招禁用GPU硬件加速轻松搞定!
  • 微信聊天记录删了别乱找!官方全套恢复方法,无备份也能救
  • 使用无障碍技术实现自动化脚本
  • 告别部署报错!OpenClaw 2.7.9 Win11超稳安装配置全流程
  • Nuke Survival Toolkit:150个专业插件打造高效合成工作流
  • 从消费决策变化看信息透明化的商业价值
  • 分层实验智能体(HExA):基于上下文自演化物理推理智能体框架
  • 如何选择靠谱的装修公司?从泰美空间设计合作案例看筛选标准
  • SQL优化_监管指标计算性能全维度优化方案
  • GEO 是什么?从 “关键词匹配” 到 “AI 信任” 的营销革命
  • 三明 开店扫码点餐系统到底要花多少钱?别被坑了才知道!
  • 总部-门店素材协同:从统一上传到一键调用的落地指南
  • Wu.CommTool工业通信调试工具技术实现深度解析:基于C WPF的模块化架构设计
  • 基于ArcGIS Pro、R、INVEST等多技术融合下生态系统服务权衡与协同动态分析实践应用
  • 强烈推荐一个基于 .NET 8 开发的企业级 OAuth 2.0 / OpenID Connect 认证框架
  • 2026数字化转型新锚点:4SAPI企业级大模型API中转网关赋能商业级AI规模化落地