第3章 OkHttp缓存策略配置:Cache类、缓存目录大小、max‑age、max‑stale
3.1 原理:OkHttp 缓存分层架构
Retrofit 本身不实现缓存,所有离线缓存能力实际委托给底层的 OkHttpClient。OkHttp 通过内置的 Cache 类与 HTTP 缓存控制协议(RFC 7234)配合,形成三层决策链路:
- 存储层:
Cache 管理磁盘目录,以键值对形式存放响应体与元数据。 - 策略层:根据请求头
Cache-Control中的max‑age、max‑stale等指令判断缓存是否可用。 - 拦截层:
CacheInterceptor在请求发出前拦截,返回有效缓存或放行至网络。
关键理解:
Cache 类只负责“存”和“读”,而“何时使用缓存”由 Cache‑Control 参数决定。两者共同构成了 OkHttp 的离线缓存引擎。
3.2 Cache 类:目录与大小配置
Cache 是 OkHttp 缓存的核心载体,构造时需要指定磁盘目录和最大字节数。超过上限时,OkHttp 会按照 LRU(最近最少使用)策略自动淘汰旧缓存。
3.2.1 构造方式
// 方式一:直接构造
File cacheDir = new File(context.getCacheDir(), "http_cache");
long maxSize = 10 * 1024 * 1024; // 10 MB
Cache diskCache = new Cache(cacheDir, maxSize);
// 方式二:链式添加到 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
.cache(diskCache)
.build();
3.2.2 目录与大小最佳实践
| 场景 | 推荐大小 | 目录位置 | 说明 |
|---|---|---|---|
| 图片/视频内容 App | 50–100 MB | context.getCacheDir()/okhttp | 文件缓存生命周期随 App 清除 |
| 新闻/列表型 App | 10–30 MB | context.getExternalCacheDir()/okhttp | 外置缓存,可被用户清理 |
| 工具类轻量 App | 5–10 MB | context.getCacheDir()/http | 避免占用过多用户空间 |
建议:缓存目录使用
context.getCacheDir() 子目录,系统在存储不足时有权清除该目录下的文件,避免 App 被用户视为“存储杀手”。
3.3 缓存控制参数:max‑age 与 max‑stale
这两个参数通过 HTTP 请求头 Cache-Control 传递给 OkHttp,控制客户端对缓存“新鲜度”的容忍程度。
3.3.1 max‑age(新鲜期)
指定缓存响应在生成后多少秒内被视为“新鲜”,此时间段内直接使用缓存,不发起网络请求。
// 服务端响应头示例(优先级高)
Cache-Control: public, max-age=3600
// 客户端请求头覆盖(使用 OkHttp 拦截器设置)
Request request = original.newBuilder()
.header("Cache-Control", "max-age=300")
.build();
3.3.2 max‑stale(过期容忍期)
当缓存已过期,客户端仍允许在 max‑stale 指定的秒数内继续使用该过期缓存。这对弱网或离线场景极为重要。
// 允许使用过期 7 天以内的缓存
Request request = original.newBuilder()
.header("Cache-Control", "max-stale=604800")
.build();
// 离线场景:允许任意过期缓存
// Cache-Control: max-stale=2147483647
3.3.3 参数对比
| 参数 | 含义 | 取值范围 | 典型值 | 使用场景 |
|---|---|---|---|---|
max‑age | 缓存新鲜窗口(秒) | ≥0 | 300(5分钟) | 正常在线,减少重复请求 |
max‑stale | 过期后仍可使用的宽限期(秒) | ≥0 | 604800(7天) | 弱网/离线,容忍陈旧数据 |
min‑fresh | 要求缓存剩余新鲜时间至少 N 秒 | ≥0 | 60 | 对实时性要求高的场景 |
only‑if‑cached | 强制只使用缓存,无缓存则报错 | 无值 | — | 严格离线模式 |
3.4 完整配置实现(Retrofit + OkHttp)
以下代码展示如何在 Retrofit 构建时,通过 OkHttpClient 配置 Cache 类并注入缓存策略:
// 1. 创建缓存目录与 Cache 实例
File cacheDir = new File(context.getCacheDir(), "retrofit_cache");
long maxCacheSize = 20 * 1024 * 1024; // 20 MB
Cache responseCache = new Cache(cacheDir, maxCacheSize);
// 2. 自定义拦截器:动态设置 Cache-Control
Interceptor cacheInterceptor = chain -> {
Request original = chain.request();
// 离线时使用 max-stale,在线时使用 max-age
String cacheControl = NetworkUtil.isOnline(context)
? "max-age=300"
: "max-stale=604800";
Request modified = original.newBuilder()
.header("Cache-Control", cacheControl)
.removeHeader("Pragma") // 移除旧协议干扰
.build();
Response response = chain.proceed(modified);
// 服务端未指定缓存时,客户端强制设置
if (!NetworkUtil.isOnline(context)) {
return response.newBuilder()
.header("Cache-Control", "max-stale=604800")
.build();
}
return response;
};
// 3. 构建 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
.cache(responseCache)
.addInterceptor(cacheInterceptor) // 应用层拦截器
.addNetworkInterceptor(cacheInterceptor) // 网络层拦截器
.build();
// 4. 构建 Retrofit 实例
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
3.5 调试:验证缓存是否生效
OkHttp 的 Cache 类提供两个直观的统计接口,适合在调试面板或日志中输出:
// 在 OkHttpClient 构建后获取缓存统计
Cache cache = client.cache();
CacheStats stats = cache.getCacheStats();
Log.d("CacheDebug", "请求数: " + stats.requestCount());
Log.d("CacheDebug", "网络请求数: " + stats.networkCount());
Log.d("CacheDebug", "缓存命中数: " + stats.hitCount());
Log.d("CacheDebug", "缓存命中率: " + String.format("%.1f%%",
stats.hitCount() * 100.0 / stats.requestCount()));
// 也可以直接检查磁盘占用
Log.d("CacheDebug", "当前磁盘占用: " + cache.size() + " bytes");
Log.d("CacheDebug", "最大容量: " + cache.maximumSize() + " bytes");
调试技巧:在 Android Studio Logcat 中过滤
CacheDebug 标签,观察每次请求的命中率变化。若命中率长期低于 20%,说明 max‑age 设置过短或缓存目录过大导致淘汰频繁。
3.6 常见错误与解决方案
| 错误现象 | 根因 | 解决方案 |
|---|---|---|
| 缓存从未命中 | 未正确设置 Cache‑Control 或服务端返回 no‑cache | 在拦截器中强制添加 max‑age;服务端配合返回 Cache‑Control: public |
| 缓存文件占用异常大 | maxSize 设置过大,且无淘汰机制 | 将 maxSize 控制在 10–50 MB,OkHttp 自动 LRU 淘汰 |
| 离线时无法加载缓存 | 未设置 max‑stale 或 max‑stale 值过小 | 离线场景下使用 max‑stale=604800 或 only‑if‑cached |
| 缓存数据与服务器不一致 | max‑age 过长,导致客户端使用陈旧数据 | 缩短 max‑age;启用 ETag 或 Last‑Modified 校验 |
Cache 初始化报错 IOException | 缓存目录不可写或磁盘空间不足 | 使用 context.getCacheDir() 确保目录可写;添加异常捕获 |
3.7 性能与稳定性优化
- 目录隔离:不同业务模块使用独立的
Cache实例(例如图片缓存与接口缓存分离),避免大文件挤压 API 缓存空间。 - 动态调节 max‑age:根据网络质量动态调整新鲜期——WiFi 下用 60 秒,4G 下用 300 秒,离线时用 max‑stale。
- 监控淘汰率:通过
Cache.evictCount()监控每秒淘汰数,若淘汰率超过 10%,及时增大maxSize或优化缓存策略。 - 避免重复创建:
Cache实例应在 Application 级单例化,切勿在每次请求时新建,否则会导致缓存目录重建、已有缓存失效。
警告:不要在生产环境中将
max‑stale 设置为极大值(如 Integer.MAX_VALUE),这会导致缓存永久不过期,用户永远看到旧数据。建议结合时间戳或版本号做二次校验。
3.8 本章小结
Cache类通过磁盘目录和 LRU 策略管理缓存物理存储。max‑age定义缓存新鲜窗口,max‑stale定义过期后仍可使用的宽限期。- 通过 OkHttp 拦截器可动态注入
Cache‑Control头,实现离线/在线自适应。 - 利用
CacheStats可量化缓存命中率,指导参数调优。 - 合理配置缓存目录大小与淘汰策略,是保障离线缓存稳定性的基石。