3. OkHttp 默认缓存配置
3.1 缓存机制概述
OkHttp 的缓存层基于 磁盘缓存(DiskLruCache) 实现,以 URL 为缓存键,结合响应头中的 Cache-Control、Expires、ETag、Last-Modified 等字段判断缓存的有效性。默认情况下缓存是关闭的,必须显式创建 Cache 对象并关联到 OkHttpClient 才能启用。
缓存配置涉及三个核心要素:
- 缓存目录 — 存储缓存文件的
File对象 - 最大大小 — 缓存占用磁盘空间的硬上限(字节)
- Cache 对象 — 封装目录与大小,提供读写接口
3.2 配置核心步骤
启用 OkHttp 缓存只需以下 4 步:
- 创建缓存目录 — 在应用的内部存储中分配一个专用目录
- 设定最大大小 — 根据业务数据量估算合理的磁盘占用上限
- 构造 Cache 对象 — 将目录和大小封装为
Cache实例 - 关联 OkHttpClient — 通过
Builder.cache()注入,使全局请求共享缓存
关键约束:Cache 对象是线程安全的,建议整个应用只创建一个 OkHttpClient 实例并复用,避免多次初始化缓存目录导致状态不一致。
3.3 核心 API 详解
| 参数 / API | 类型 | 说明 | 建议值 |
|---|---|---|---|
cacheDir | File | 缓存文件存放的目录,必须可读写 | context.getCacheDir() + "/okhttp" |
maxSize | long | 缓存最大字节数,超出后按 LRU 淘汰 | 10MB ~ 100MB(视业务而定) |
new Cache(dir, maxSize) | Cache | 构造缓存实例,初始化 journal 文件 | — |
Builder.cache(cache) | OkHttpClient.Builder | 将缓存注入客户端构建器 | 非 null 即可 |
client.cache() | Cache | 返回当前客户端关联的缓存对象(调试用) | — |
完整配置代码(Android 环境):
// 1. 创建缓存目录
File cacheDir = new File(context.getCacheDir(), "okhttp_cache");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
// 2. 设定最大大小:10 MB
long maxSize = 10 * 1024 * 1024;
// 3. 构造 Cache 对象
Cache cache = new Cache(cacheDir, maxSize);
// 4. 关联到 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();
// 验证:打印缓存目录与大小
System.out.println("Cache directory: " + client.cache().directory());
System.out.println("Cache max size: " + client.cache().maxSize());
纯 Java 服务端配置(无 Android Context):
File cacheDir = new File("/tmp/okhttp-cache");
long maxSize = 50 * 1024 * 1024; // 50 MB
Cache cache = new Cache(cacheDir, maxSize);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();
3.4 调试与验证
缓存是否生效可以通过响应头或 OkHttp 提供的 缓存状态标记 来判断:
方式一:检查响应头
Request request = new Request.Builder()
.url("https://api.example.com/data")
.build();
Response response = client.newCall(request).execute();
// 打印缓存相关头
System.out.println("Cache-Control: " + response.header("Cache-Control"));
System.out.println("Date: " + response.header("Date"));
System.out.println("Expires: " + response.header("Expires"));
System.out.println("ETag: " + response.header("ETag"));
// 关键:判断缓存来源
if (response.cacheResponse() != null) {
System.out.println("[CACHE HIT] 响应来自本地缓存");
} else {
System.out.println("[CACHE MISS] 响应来自网络");
}
if (response.networkResponse() != null) {
System.out.println("网络状态码: " + response.networkResponse().code());
}
response.close();
方式二:通过 Cache 统计信息
Cache cache = client.cache();
System.out.println("请求总数: " + cache.requestCount());
System.out.println("网络请求数: " + cache.networkCount());
System.out.println("缓存命中数: " + cache.hitCount());
System.out.println("缓存命中率: " + (cache.hitCount() * 100.0 / cache.requestCount()) + "%");
调试技巧:首次请求必定 MISS,第二次相同 URL 请求若响应头允许缓存则会 HIT。可以在 response.cacheResponse() 不为 null 时确认缓存生效。
3.5 常见错误与避坑
- 缓存目录不可写 — 使用外部存储时可能没有写入权限,导致
Cache构造失败或写入异常。始终优先使用context.getCacheDir()。 - maxSize 设置过小 — 缓存频繁被淘汰,导致命中率极低。建议根据单次响应平均大小 × 期望缓存条目数 × 1.5 安全系数来估算。
- 多实例共享同一目录 — 多个 OkHttpClient 指向同一个缓存目录会造成 journal 文件冲突,抛出
IOException。确保全局唯一客户端或唯一目录。 - 未配置缓存即调用 cache() —
client.cache()返回 null,在未判空时调用方法引发 NPE。使用前始终判空。 - 忽略响应头约束 — 服务器返回
Cache-Control: no-store或private时,即使配置了缓存也不会存储。需后端配合。
典型异常日志:java.io.IOException: journal is not writable 或 java.io.FileNotFoundException: /okhttp_cache/journal (Permission denied)。这两个错误均与缓存目录权限或多进程冲突有关。
3.6 性能与稳定性优化
| 优化方向 | 具体措施 | 预期收益 |
|---|---|---|
| 缓存大小 | 根据业务数据的日增量设置 maxSize,预留 50% 缓冲区 | 减少 LRU 淘汰频率,提升命中率 |
| 目录选择 | Android 使用 context.getCacheDir();服务端使用独立临时目录 | 避免权限问题,系统可自动清理 |
| 客户端复用 | 使用单例或依赖注入维护唯一的 OkHttpClient 实例 | 避免重复创建 Cache,降低 journal 损坏风险 |
| 监控告警 | 定期采集 cache.hitCount() 和 cache.networkCount() | 及时发现缓存命中率异常,辅助调优 |
| 缓存预热 | 应用启动时对关键 API 发起一次请求 | 让高频数据提前进入缓存,减少首屏网络耗时 |
稳定性建议:
- 在 Application 初始化阶段完成缓存配置,避免运行时动态切换。
- 对
Cache构造过程添加 try-catch,降级为无缓存模式:
Cache cache = null;
try {
cache = new Cache(cacheDir, maxSize);
} catch (IOException e) {
// 降级:不使用缓存
System.err.println("缓存初始化失败,降级为无缓存模式: " + e.getMessage());
}
OkHttpClient.Builder builder = new OkHttpClient.Builder();
if (cache != null) {
builder.cache(cache);
}
OkHttpClient client = builder.build();
最佳实践:在 Android 中,系统可能在低存储空间时自动清除 getCacheDir() 下的文件,应用无需手动管理。但建议监控 cache.size() 与 maxSize 的比例,超过 90% 时考虑动态调整上限。