网络缓存策略是一个提升应用性能和用户体验的关键技术,Cache-Control
和 DiskLruCache
是其中两个重要但不同层面的概念。让我们来详细解析它们以及如何协同工作。
核心目标: 减少不必要的网络请求,加快内容加载速度,节省用户流量,降低服务器负载。
1. HTTP 缓存策略:Cache-Control (协议层/网络库层)
- 定位: HTTP 协议定义的机制,用于浏览器、网络库(如 OkHttp)和代理服务器之间控制缓存行为的标准。
- 工作原理: 通过 HTTP 响应头 (
Cache-Control
,Expires
,ETag
,Last-Modified
) 和请求头 (If-Modified-Since
,If-None-Match
) 来协商资源的缓存有效性。 - 关键指令 (Cache-Control 头):
public
:响应可以被任何中间缓存(CDN、代理)和客户端缓存。private
:响应仅可被客户端(浏览器、App)缓存,中间缓存不应存储。no-cache
:可以缓存 ,但在每次使用前必须 向原始服务器发起验证请求(使用ETag
/Last-Modified
)。验证通过(304 Not Modified)则使用缓存;验证失败(200 OK)则使用新响应并更新缓存。不是"不缓存"的意思!no-store
:绝对禁止缓存任何响应内容。每次都必须从服务器获取完整响应。max-age=
: 指定资源在客户端缓存中的最大有效时间(秒) 。例如max-age=3600
表示缓存 1 小时内有效,无需请求服务器。s-maxage=
: 类似于max-age
,但仅适用于共享缓存(如 CDN、代理) 。优先级高于max-age
。must-revalidate
:一旦缓存过期,必须向服务器验证其有效性,不能直接使用过期的缓存(即使离线)。immutable
:资源在其max-age
内被视为永不变更 ,客户端不会发送验证请求。适合长期不变的静态资源(如带版本戳的文件)。
- 验证机制:
ETag
(响应头):服务器为资源生成的唯一标识符(哈希值)。客户端在验证请求中使用If-None-Match
头带上缓存的 ETag。Last-Modified
(响应头):资源最后修改时间。客户端在验证请求中使用If-Modified-Since
头带上缓存的时间。- 服务器收到验证请求后,检查资源是否变更:
- 未变更:返回
304 Not Modified
(空 body),客户端使用缓存。 - 已变更:返回
200 OK
和新资源,客户端更新缓存。
- 未变更:返回
- 在 App 中的实现:
- 由网络库(如 OkHttp, Retrofit)自动处理。OkHttp 内部有一个基于
Cache-Control
等头的内存+磁盘响应缓存。 - 开发者需要配置 OkHttp 的
Cache
实例(指定缓存目录和大小),并确保服务器正确设置响应头。 - 开发者通常不直接操作缓存逻辑,而是依赖库对 HTTP 缓存语义的实现。
- 由网络库(如 OkHttp, Retrofit)自动处理。OkHttp 内部有一个基于
2. 应用层磁盘缓存:DiskLruCache (App 逻辑层)
- 定位: 一个由开发者在应用层面主动实现的、基于文件系统的 LRU (Least Recently Used) 缓存库/策略。用于存储任何需要持久化的数据(图片、解析后的 JSON、下载的文件等)。
- 工作原理:
- LRU 算法: 当缓存空间满时,优先淘汰最久未被使用的数据。
- 文件存储: 将数据(通常是字节流)存储到 App 的缓存目录或私有文件目录下的特定文件中。
- 键值对: 通过唯一的
key
(通常是 URL 的哈希值、资源 ID 等) 来存储和检索数据。 - 快照 (Snapshot): 提供对缓存条目内容的只读访问。
- 编辑器 (Editor): 提供对缓存条目的写入/更新操作(原子性写入)。
- 日志 (Journal): 记录缓存的操作(添加、删除、访问),用于在启动时重建缓存状态和实现 LRU 策略。
- 典型用途:
- 图片缓存: Glide, Picasso, Fresco 等图片库的核心磁盘缓存实现。
- API 响应缓存: 存储解析后的结构化数据(如 JSON 对象),避免重复解析网络响应。
- 文件下载缓存: 缓存下载的文档、音频、视频等。
- 复杂计算结果缓存: 缓存耗时计算的结果。
- 优点:
- 完全控制: 开发者决定缓存什么、何时缓存、如何生成 key、缓存多久(可以超出 HTTP 头限制)。
- 数据格式灵活: 可以缓存原始字节、序列化对象、图片 Bitmap 等任何可序列化的数据。
- 持久化: 数据存储在磁盘,App 重启后依然存在(直到被 LRU 淘汰或主动清除)。
- 减少网络请求/解析开销: 直接从本地磁盘读取处理好的数据,速度极快。
- 缺点/注意点:
- 需要手动管理: 需要开发者编写代码进行存储、读取、失效(删除)操作。
- 缓存失效策略复杂: 需要设计机制处理数据更新(如基于时间戳、版本号、监听网络更新通知等)。
- 磁盘 I/O 开销: 读写磁盘比内存慢,需注意性能。
- 空间管理: 需要设置合理的缓存大小上限,依赖 LRU 自动清理。
Cache-Control vs DiskLruCache:区别与联系
特性 | Cache-Control (HTTP 缓存) | DiskLruCache (App 磁盘缓存) |
---|---|---|
层级 | 网络协议层 / 网络库层 | App 逻辑层 |
控制者 | 服务器 (通过响应头) / 网络库 (自动处理语义) | App 开发者 (主动编码管理) |
存储内容 | 原始的 HTTP 响应 (Headers + Body) | 任何 App 需要的数据 (原始字节、对象、图片等) |
格式 | 原始网络响应流 | 开发者定义的数据格式 |
失效策略 | 基于 HTTP 头 (max-age , ETag , 验证请求) |
开发者自定义 (LRU, 时间, 版本, 事件触发删除等) |
访问 | 网络库自动使用 (对 App 透明) | App 显式调用读写接口 |
典型用途 | 减少重复获取未变更的原始网络资源 | 缓存处理后的结果,避免重复下载、解析、计算 |
例子 | OkHttp 内置的 Cache |
Glide/Picasso 的磁盘缓存,自定义 API 响应缓存 |
协同工作:构建高效的网络缓存策略
一个健壮的 App 通常会同时利用两者,形成多级缓存:
-
HTTP 缓存 (OkHttp Cache):
- 作为第一道防线,处理协议层面的缓存。
- 服务器设置合理的
Cache-Control
(如public, max-age=3600
) 和ETag
。 - OkHttp 自动存储响应,并在有效期内 (
max-age
) 或通过验证 (ETag
/304
) 直接返回缓存,避免真正的网络请求。 - 缓存的是原始响应。
-
应用层磁盘缓存 (DiskLruCache):
- 处理 HTTP 缓存之上或之外的需求。
- 场景 1:缓存处理结果
- 从 OkHttp 获取到原始响应(可能是新的,也可能是缓存验证过的)。
- 解析/处理这个响应(如 JSON 解析成对象列表、下载图片解码成 Bitmap)。
- 将处理后的结果 (对象、Bitmap) 用
DiskLruCache
存储起来,Key 可以是 URL + 参数哈希。 - 下次需要相同数据时:
- 先检查
DiskLruCache
。命中则直接使用处理好的结果(最快路径,省去网络和解析)。 - 未命中,则发起网络请求(该请求本身可能被 OkHttp HTTP 缓存拦截)。
- 先检查
- 场景 2:缓存 HTTP 缓存不适用或失效的数据
- 缓存服务器可能不支持或不正确设置
Cache-Control
的资源。 - 缓存需要更长时间或不同策略的数据(即使 HTTP 缓存过期,App 可能仍想保留一段时间)。
- 缓存非 HTTP 获取的数据(如从其他来源下载的文件)。
- 缓存服务器可能不支持或不正确设置
总结关键点:
Cache-Control
: 是标准,由服务器定义策略 ,网络库自动执行 ,缓存原始网络响应 。目标是减少不必要的网络传输。DiskLruCache
: 是工具,由开发者主动实现 ,缓存应用需要的任何格式的数据 (通常是处理后的结果)。目标是减少重复处理开销和提供离线能力。- 最佳实践: 优先利用好 HTTP 缓存 (
Cache-Control
) 减少网络请求。在其基础上,使用DiskLruCache
缓存处理后的结果以最大化性能。两者结合能显著提升 App 的响应速度和离线体验。
选择哪种策略或如何组合,取决于具体需求、服务器支持情况以及对缓存数据的控制要求。理解它们的区别和联系是设计高效缓存架构的基础。