Fresco的缓存命中和使用策略
Published in:2026-03-07 |

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
2
3
4
5
6
7
发起图片请求 → 1. 检查【内存快存(BitmapMemoryCache)】→ 命中/未命中
↓ 未命中
2. 检查【内存慢存(EncodedMemoryCache)】→ 命中/未命中
↓ 未命中
3. 检查【磁盘缓存】→ 先查未解码原数据分区 → 再查已解码Bitmap分区 → 命中/未命中
↓ 未命中
4. 发起网络请求加载图片 → 加载成功/失败

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
2
3
4
// 清理所有磁盘缓存(未解码+已解码)
Fresco.getImagePipeline().clearDiskCaches()// 清理所有内存缓存(快存+慢存)
Fresco.getImagePipeline().clearMemoryCaches()// 清理所有缓存(内存+磁盘)
Fresco.getImagePipeline().clearCaches()

三、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
2
// 自定义CacheKey:Uri+参数拼接,避免命中错误缓存val imgUri = Uri.parse("https://xxx.com/avatar.png?uid=123")val request = ImageRequestBuilder
.newBuilderWithSource(imgUri).setCacheKey(imgUri.toString() + "_uid123") // 自定义唯一CacheKey.build()

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/缓存淘汰三个角度快速定位问题。

Next:
如何做一台专属的“豆包”手机