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(最大字节数)。
OkHttp Cache 架构流程 OkHttp Cache 与 DiskLruCache 关系图 OkHttpClient .cache(cache) Cache 拦截 / 缓存逻辑 DiskLruCache Journal + 实体文件 文件系统 目录 / 权限 HTTP 响应条目 Header + Body 📌 说明: Cache 使用 URL 和请求头生成键,DiskLruCache 通过 journal 文件维护 LRU 顺序; 当总大小超过 maxSize 时,自动淘汰最久未使用的条目。

3.2 配置实现流程

  1. 选择缓存目录:通常使用应用内部缓存目录(context.cacheDir)的子目录。
  2. 计算最大缓存空间:建议根据设备存储容量和网络使用频率设定,例如 10MB ~ 50MB。
  3. 创建 Cache 对象:传入 File 和 long maxSize。
  4. 构建 OkHttpClient:通过 .cache(cache) 绑定。
  5. 可选:清理旧缓存:在应用启动或版本更新时根据业务需要触发 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+ 上已内置,无需额外引入依赖。