3. 添加 OkHttp Cache 目录与大小配置
OkHttp 的缓存机制依赖于底层的 DiskLruCache 实现。本章将深入讲解如何为 OkHttpClient 配置缓存目录路径与最大空间,分析 DiskLruCache 的工作原理,并给出最佳实践。
3.1 核心原理:OkHttp Cache 与 DiskLruCache 的关系
关键概念:OkHttp 的 Cache 类封装了 DiskLruCache,后者负责将 HTTP 响应持久化到本地文件系统。每个缓存条目对应一个唯一的键(由 URL、请求方法、头部等生成),值存储在文件系统的 journal 文件和实体文件中。
- Cache:负责拦截响应、判断是否可缓存、管理缓存命中/未命中逻辑。
- DiskLruCache:提供 LRU(最近最少使用)淘汰策略,保证缓存大小不超过设定上限。
- 配置要素:
File directory(缓存目录)、long maxSize(最大字节数)。
3.2 配置实现流程
- 选择缓存目录:通常使用应用内部缓存目录(
context.cacheDir)的子目录。 - 计算最大缓存空间:建议根据设备存储容量和网络使用频率设定,例如 10MB ~ 50MB。
- 创建 Cache 对象:传入
File和long maxSize。 - 构建 OkHttpClient:通过
.cache(cache)绑定。 - 可选:清理旧缓存:在应用启动或版本更新时根据业务需要触发
cache.evictAll()。
3.3 核心代码示例
// 1. 定义缓存目录(推荐在 Application 初始化时完成)
File cacheDirectory = new File(context.getCacheDir(), "okhttp_cache");
if (!cacheDirectory.exists()) {
cacheDirectory.mkdirs();
}
// 2. 设定最大缓存空间(示例 20MB)
long maxCacheSize = 20 * 1024 * 1024L; // 20 MB
// 3. 构建 Cache 对象
Cache cache = new Cache(cacheDirectory, maxCacheSize);
// 4. 构建 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.addNetworkInterceptor(CACHE_CONTROL_INTERCEPTOR) // 可选
.addInterceptor(OFFLINE_CACHE_INTERCEPTOR) // 可选
.build();
3.4 参数对比与最佳实践
| 参数 | 类型 | 说明 | 推荐值 |
|---|---|---|---|
| directory | File |
缓存文件的存放目录,需确保应用有读写权限 | context.cacheDir + "/okhttp_cache" |
| maxSize | long |
缓存占用磁盘的最大字节数 | 10MB ~ 50MB(根据业务调整) |
| 写入粒度 | — | DiskLruCache 内部按 1MB 分块管理,减少碎片 | — |
| journal 文件 | File |
记录所有缓存操作日志,用于恢复 LRU 状态 | 自动生成于 directory 下 |
3.5 DiskLruCache 原理速览
工作原理:DiskLruCache 使用一个 journal 文件(类似日志)记录每次读取、写入、删除操作。启动时回放 journal 重建 LRU 链表。写操作时先写入临时文件,提交后原子性加入链表;当总大小超过 maxSize 时,从链表尾部删除最久未使用的条目。
- journal 内容示例:每行记录操作类型(
CLEAN/DIRTY/REMOVE/READ)及条目信息。 - 线程安全:DiskLruCache 内部使用
ReadWriteLock和FileLock保证多进程安全。 - 注意事项:journal 损坏会导致缓存重建(弃用当前数据),但不会崩溃。
3.6 调试与验证
调试技巧:启用 OkHttp 的日志拦截器(HttpLoggingInterceptor),设置级别为 HEADERS 或 BODY,观察响应头中的 Cache-Control 和 X-Cache(自定义)字段。
// 添加日志拦截器
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(logging)
.build();
// 日志输出示例(击中缓存):
// <-- 200 https://api.example.com/data (268ms, 从缓存获取)
// Cache-Control: max-age=3600
// X-Cache: HIT
也可通过 Cache 实例的统计方法获取命中率:
Log.d("CacheStats", "请求数: " + cache.requestCount());
Log.d("CacheStats", "命中数: " + cache.hitCount());
Log.d("CacheStats", "命中率: " + (float) cache.hitCount() / cache.requestCount());
3.7 常见错误与解决方案
| 错误现象 | 原因 | 解决方案 |
|---|---|---|
缓存未生效,日志显示 NO_CACHE |
响应头缺少 Cache-Control: max-age 或 ETag,或 OkHttpClient 未配置 cache() |
确保服务端返回可缓存响应头;检查 client 构建代码 |
应用崩溃:java.io.IOException: No space left on device |
缓存目录空间不足,或 maxSize 设置过大 |
适当减小 maxSize;在写入前检查剩余存储 |
| 多进程共享同一缓存目录导致 journal 损坏 | DiskLruCache 默认使用 FileLock,但部分设备文件锁不可靠 |
为每个进程分配独立缓存子目录,或使用 process 维度隔离 |
| 缓存目录权限错误 | mkdirs() 未执行或路径指向外部存储但无权限 |
使用 context.getCacheDir() 保证内部存储可写 |
3.8 性能与稳定性优化
- 合理设置 maxSize:过大会占用用户存储,过小影响命中率;建议根据业务数据量取 10MB ~ 30MB。
- 避免频繁初始化:
Cache对象应在 Application 中单例创建,减少 journal 重建开销。 - 版本升级清理:如果缓存数据格式发生变化,应在新版本启动时调用
cache.evictAll()避免脏数据。 - 监控磁盘 I/O:缓存写操作在子线程进行,不会阻塞主线程;但大量并发写入可能导致 journal 膨胀,可定期(如每月)主动清理。
- 配合网络拦截器:使用
addNetworkInterceptor对可缓存响应添加Cache-Control头,控制缓存有效时间。
注意:不要将 maxSize 设为 0 或负数,否则 Cache 构造会抛出 IllegalArgumentException。另外,DiskLruCache 在 Android 4.0+ 上已内置,无需额外引入依赖。