Fresco的缓存命中和使用策略
Fresco 的缓存机制围绕**「三级缓存(2 级内存 + 1 级磁盘)」**展开,所有的命中(查找缓存)和使用(写入 / 淘汰 / 复用缓存)都是基于这个层级,且查找有严格的优先级,写入有固定的回写规则,框架全程自动管理,无需手动干预。
三级缓存的精准定义
| 缓存层级 | 全称 | 存储内容 | 存储区域 | 生命周期 | 核心作用 |
|---|---|---|---|---|---|
| 内存一级缓存(快存) | BitmapMemoryCache | 解码后的 Bitmap | Ashmem(匿名共享内存,不占 Java 堆) | 应用前台,主动淘汰 | 最快复用,直接展示无需解码 |
| 内存二级缓存(慢存) | EncodedMemoryCache | 编码后的原数据(JPG/PNG/Gif 字节流) | Java 堆(弱 / 软引用) | GC 优先回收,快存满后回收 | 内存兜底,解码后同步到快存 |
| 磁盘缓存 | DiskCache(双分区) | 未解码原数据(主)+ 解码后 Bitmap(辅) | 应用私有目录(无需存储权限) | 持久化,主动淘汰 | 跨页面 / 跨启动复用,网络兜底 |
关键补充:磁盘缓存是双分区设计——Fresco 默认优先缓存未解码的原数据(占空间小、通用性强),仅在特定场景缓存解码后的 Bitmap,这是为了平衡缓存命中率和磁盘空间占用。
一、Fresco 核心:缓存命中查找流程(从发起请求到找到缓存)
缓存命中:指加载图片时,Fresco 在某个缓存层级中找到对应的图片数据,无需向下一层级查找,更无需请求网络。
Fresco 的查找流程是**「自上而下、优先级从高到低」**的严格顺序,任意层级命中则终止查找,直接使用该层级的数据;所有层级未命中则走网络加载。
1. 完整的缓存命中查找流程(结合 ImagePipeline 工作机制)
当通过SimpleDraweeView设置 Uri 发起图片请求后,ImagePipeline 会执行以下步骤:
1 | 发起图片请求 → 1. 检查【内存快存(BitmapMemoryCache)】→ 命中/未命中 |
2. 各层级命中后的处理逻辑(核心,决定后续怎么展示 / 复用)
不同层级命中后,Fresco 的处理动作不同,目的是尽可能让后续请求能命中更高优先级的缓存:
内存快存命中:✅ 最优情况,直接将 Ashmem 中的 Bitmap 传递给 DraweeView 展示,无任何解码 / IO 操作,加载速度最快;
内存慢存命中:将编码原数据在 Ashmem 中异步解码为 Bitmap → 展示图片 → 同步将解码后的 Bitmap 写入内存快存(为下次请求做准备);
磁盘缓存命中:
若命中未解码原数据:将数据读入内存 → 异步解码为 Bitmap → 展示 → 同步写入内存快存 + 内存慢存;
若命中已解码 Bitmap:直接将 Bitmap 写入内存快存 → 展示(比解码原数据更快);
3. 网络加载成功后的缓存回写规则(保证下次能命中)
如果所有缓存都未命中,网络加载图片成功后,Fresco 会按「自下而上」的顺序回写缓存,完成缓存的初始化,让后续请求能直接复用:
网络加载原数据 → 解码为 Bitmap → 写入【内存快存】→ 写入【内存慢存】→ 写入【磁盘缓存(优先未解码原数据)】
二、Fresco 缓存使用机制:写入 / 淘汰 / 复用(框架自动管理)
缓存的 “使用” 不只是 “查找和展示”,还包括写入规则、淘汰机制(缓存满了怎么清理)、复用策略,这是 Fresco 能自动维护缓存、不造成内存 / 磁盘溢出的关键,所有操作均由 ImagePipeline 内部的缓存管理器自动执行,无需手动调用 API。
1. 内存双缓存(快存 + 慢存):基于LRU 算法的内存管理
Fresco 对内存缓存的管理核心是LRU(最近最少使用)算法+Android 内存状态感知,简单说:缓存满了,优先淘汰最近最少被使用的缓存数据,同时会根据 App 的内存状态(前台 / 后台 / 低内存)动态调整缓存大小。
(1)内存快存(BitmapMemoryCache)的使用细节
写入:仅存储解码后的 Bitmap,且强制存储在Ashmem(匿名共享内存),不占用 App 的 Java 堆内存—— 这是 Fresco 解决 OOM 的核心!
淘汰:① 按 LRU 淘汰,快存达到最大容量时,移除最久未使用的 Bitmap;② App 进入后台 / 低内存状态时,Fresco 会主动回收部分快存,释放 Ashmem;③ 不会被 GC 主动回收(强引用缓存),只有框架主动淘汰才会释放。
复用:Bitmap 一旦被加载,会被快存持有强引用,只要未被淘汰,任意页面请求相同图片都会直接命中,无需重复解码。
(2)内存慢存(EncodedMemoryCache)的使用细节
写入:仅存储编码后的原数据(字节流),占用内存极小(一张 1080P 的 JPG 约 100KB),存储在 Java 堆的弱引用 / 软引用中;
淘汰:① 优先级最低,GC 执行时会优先回收慢存;② 内存快存满了之后,会主动淘汰慢存的旧数据;③ 同样遵循 LRU 算法;
作用:快存的 “兜底缓存”—— 如果快存的 Bitmap 被淘汰,慢存的原数据还在,只需解码一次就能恢复快存,比从磁盘加载更快。
2. 磁盘缓存(DiskCache):LRU + 时间的双维度淘汰,双分区写入
Fresco 的磁盘缓存是持久化缓存(App 重启 / 卸载前一直存在),默认存储在 App 的私有缓存目录(/data/data/包名/cache/),无需申请存储权限,这是和 Glide/Picasso 的一致设计,避免权限问题。
(1)双分区写入规则(核心设计)
磁盘缓存分为**「未解码缓存区(Main)」和「已解码缓存区(Small)」**,Fresco 的写入策略是:
优先写入未解码缓存区:网络 / 内存加载的原数据,会默认写入这个分区,这是磁盘缓存的主分区(占 90% 以上磁盘缓存空间);
按需写入已解码缓存区:仅当图片被频繁加载时,Fresco 才会将解码后的 Bitmap 写入这个分区(辅分区,空间小),目的是避免磁盘空间浪费(Bitmap 比原数据大 10-20 倍)。
原因:原数据是 “通用的”—— 可以解码为任意尺寸的 Bitmap,而 Bitmap 是 “专用的”—— 仅适配某个SimpleDraweeView的尺寸,优先存原数据能提升缓存的通用性和命中率。
(2)淘汰机制(双维度,避免磁盘溢出)
当磁盘缓存达到最大容量(默认约 250MB)或数据过期时,Fresco 会在 **App 空闲时(如后台)**自动执行淘汰,规则是:
LRU 优先:移除最近最少使用的缓存数据(无论未解码 / 已解码);
时间兜底:移除超过 60 天未被使用的缓存数据(即使磁盘缓存没满);
分区独立淘汰:未解码 / 已解码分区各自遵循 LRU,互不影响。
(3)手动清理磁盘缓存(可选 API)
如果业务需要(如用户点击 “清理缓存”),可通过 Fresco 提供的 API 手动清理,无需自己操作文件:
1 | // 清理所有磁盘缓存(未解码+已解码) |
三、Fresco 缓存命中的核心关键:CacheKey(缓存键)
所有的缓存命中 / 写入 / 淘汰,都是基于CacheKey(缓存键)实现的 ——Fresco 通过唯一的 CacheKey标识 “一张图片”,相同 CacheKey 视为同一张图,不同 CacheKey 视为不同图,这是理解缓存命中的底层规则,也是之前 Demo 中 “随机 CacheKey 实现强制刷新” 的原理。
1. 默认 CacheKey 规则
Fresco默认使用图片的「Uri」作为 CacheKey,这是最常用的规则,比如:
网络图片:https://xxx.com/test.png → 以这个 Uri 为 CacheKey;
本地图片:res:///${R.drawable.test}/file:///sdcard/test.png → 以这个 Uri 为 CacheKey;
默认规则的命中效果:只要两次请求的Uri 完全一致,Fresco 就会认为是同一张图片,会按三级缓存流程查找命中;如果 Uri 不同,即使是同一张图片,也会视为新图,重新加载并生成新的 CacheKey。
2. 自定义 CacheKey(解决特殊场景的命中问题)
有些场景下,相同 Uri 但实际是不同图片(比如带参数的图片:https://xxx.com/avatar.png?uid=123/https://xxx.com/avatar.png?uid=456),如果用默认 Uri 作为 CacheKey,会导致缓存命中错误(加载出其他用户的头像)。
此时可以通过ImageRequestBuilder自定义 CacheKey,保证不同图片的 CacheKey 唯一:
1 | // 自定义CacheKey:Uri+参数拼接,避免命中错误缓存val imgUri = Uri.parse("https://xxx.com/avatar.png?uid=123")val request = ImageRequestBuilder |
3. 结合之前的强制刷新代码理解 CacheKey
之前用setCacheKey(System.currentTimeMillis().toString())实现强制刷新,核心原理就是:
每次点击按钮,生成一个「唯一的随机 CacheKey(时间戳)」,Fresco 会认为这是一个从未加载过的新图片,因此会跳过所有缓存的查找(因为没有对应的 CacheKey),直接发起网络请求,从而实现和CachePolicy.IGNORE_CACHE完全一致的效果。
四、CachePolicy(缓存策略):手动干预缓存命中流程
之前学习的CachePolicy(DEFAULT/IGNORE_CACHE/ONLY_CACHE),本质是手动打破 Fresco 的默认命中流程,按业务需求控制 “是否查找缓存”“是否请求网络”,这是机制层面的灵活使用,也是开发中最常用的缓存控制方式。
结合缓存命中流程,讲清楚每个策略的实际执行逻辑(小白易懂版):
1. CachePolicy.DEFAULT(默认策略,无手动干预)
执行完整的三级缓存命中流程(内存快存→慢存→磁盘→网络);
任意层级命中则展示,所有层级未命中则走网络,加载成功后正常回写所有缓存;
开发中 90% 的场景用这个策略,框架自动管理,无需手动设置。
2. CachePolicy.IGNORE_CACHE(忽略所有缓存,强制刷新)
直接跳过所有三级缓存的查找,不检查任何缓存,直接发起网络请求;
网络加载成功后,仍然会正常回写所有缓存(下次用 DEFAULT 策略可命中);
适用场景:用户手动刷新图片、图片实时更新(如验证码、头像修改后)。
3. CachePolicy.ONLY_CACHE(仅从缓存加载,不请求网络)
执行完整的三级缓存命中流程,但任意层级未命中后,直接加载失败,不发起网络请求;
无缓存回写(因为不会走网络);
适用场景:离线模式、无网络时的图片展示、仅展示本地缓存的内容。
五、一次完整的缓存命中 / 使用过程
用三次加载同一张网络图片的场景,串联所有机制,让你直观理解:
场景:加载网络图片https://xxx.com/test.png,SimpleDraweeView 尺寸固定
第一次加载:
所有缓存都无对应的 CacheKey,未命中,发起网络请求;
网络加载成功后,回写缓存:解码 Bitmap→写入内存快存→写入内存慢存→将原数据写入磁盘未解码分区;
展示图片,耗时:网络请求 + 解码 + 展示(最慢的一次)。
第二次加载(同页面 / 新页面,未被淘汰):
查找内存快存,命中对应的 Bitmap,直接展示;
无解码 / IO / 网络操作,耗时:0(几乎瞬间)(最优命中)。
第三次加载(App 后台后切回,快存被回收,慢存 / 磁盘缓存还在):
查找内存快存,未命中;
查找内存慢存,命中原数据,解码为 Bitmap→展示→同步回写内存快存;
无网络 / 磁盘 IO 操作,耗时:解码时间(比第一次快很多)。
第四次加载(App 重启,内存缓存清空,磁盘缓存还在):
查找内存快存 / 慢存,未命中;
查找磁盘缓存,命中原数据→读入内存→解码→展示→回写内存双缓存;
无网络操作,耗时:磁盘 IO + 解码时间(比网络加载快)。
第五次加载(手动点击强制刷新,用 IGNORE_CACHE / 随机 CacheKey):
跳过所有缓存查找,直接走网络→加载成功后回写缓存→展示;
耗时和第一次一致,实现 “强制刷新”。
六、Fresco 缓存机制的核心设计亮点(为什么能极致优化 OOM / 速度)
Ashmem 存储解码后的 Bitmap:内存快存的 Bitmap 不占 Java 堆,彻底避免因 Bitmap 导致的 OOM,这是和 Glide/Picasso 最核心的区别;
内存双缓存的 LRU + 内存感知:强引用快存保证速度,弱引用慢存做兜底,结合 App 内存状态动态淘汰,既保证命中率,又不浪费内存;
磁盘缓存的双分区设计:优先存原数据,提升缓存通用性和空间利用率,避免 Bitmap 占用过多磁盘空间;
CacheKey 的唯一标识:基于 Uri 的默认规则简单易用,自定义规则灵活适配特殊场景,从底层保证缓存命中的准确性;
自动的生命周期绑定:缓存的淘汰 / 回收会和 App / 页面生命周期联动(如后台回收内存缓存),无需手动管理,避免内存泄漏。
七、核心要点总结
Fresco 的缓存命中是自上而下的严格优先级(内存快存→慢存→磁盘→网络),任意层级命中则终止查找,未命中则走网络并回写缓存;
缓存使用的核心是自动的写入 / 淘汰:内存缓存基于 LRU + 内存状态,磁盘缓存基于 LRU + 时间,均由框架管理,无需手动干预;
CacheKey 是缓存命中的底层核心,默认用 Uri,可自定义,相同 CacheKey 才会命中,这也是强制刷新、避免错误命中的关键;
CachePolicy是手动干预命中流程的工具,通过它可实现 “强制刷新”“离线加载” 等业务需求,不改变底层的缓存机制;
Fresco 解决 OOM 的核心是Ashmem 存储快存的 Bitmap,不占用 Java 堆,这是其缓存机制的最独特设计。
理解这些机制后,你再回头看之前的 Demo 代码,就能明白 “为什么这么写”“缓存背后发生了什么”,后续遇到缓存相关的问题(如命中错误、刷新不生效),也能从CacheKey/CachePolicy/缓存淘汰三个角度快速定位问题。