3. 配置OkHttp缓存目录与大小:File、maxSize、删除策略

3.1 原理概述

OkHttp 的缓存子系统基于 DiskLruCache 实现,所有缓存响应以文件形式持久化到磁盘。配置缓存的核心三要素为:

  • 缓存目录(File)——决定缓存文件存储在哪个路径,通常指向应用专属的内部存储目录,避免被用户或系统随意清理。
  • 最大大小(maxSize)——限制缓存占用的总字节数,当写入新缓存导致总大小超标时,触发淘汰机制。
  • 删除策略(LRU)——OkHttp 内置 最近最少使用(Least Recently Used) 淘汰算法:优先删除最久未被访问的缓存条目,直到总容量降至 maxSize 以下。

这三个配置项通过 Cache(directory, maxSize) 构造函数绑定,再由 OkHttpClient.Builder.cache(cache) 注入到 HTTP 客户端。正确设置后,OkHttp 自动拦截响应、写入缓存、执行 LRU 淘汰,开发者无需手动管理删除逻辑。

3.2 核心配置参数

参数类型说明建议值
directoryFile缓存文件存放的目录,必须可读写且唯一context.cacheDir / "okhttp_cache"
maxSizelong缓存最大字节数,超限后触发 LRU 淘汰10MB ~ 100MB(根据图片/资源量调整)
deleteStrategy内置DiskLruCache 自动执行 LRU 算法,无手动策略接口—
💡 关键理解: maxSize 不是硬限制,而是“软水位”。OkHttp 在每次写入新缓存条目后检查总大小,若超出则异步执行 LRU 清理。因此实际占用可能短暂略高于 maxSize,但最终收敛到设定值以下。

3.3 实现流程

  1. 确定目录——使用 context.cacheDir 创建子目录,确保路径唯一且应用私有。
  2. 计算容量——根据业务场景估算缓存上限(图片多则 50MB+,纯 API 响应 10MB 足够)。
  3. 构造 Cache——调用 Cache(directory, maxSize) 初始化 DiskLruCache。
  4. 注入 OkHttpClient——通过 Builder.cache(cache) 关联客户端。
  5. 请求响应——OkHttp 自动判断缓存头、读写磁盘、触发 LRU 淘汰。
⚠️ 禁止在多处重复创建 Cache 实例指向同一目录: 多个 Cache 对象操作同一目录会导致文件锁冲突、元数据损坏。必须全局单例共享一个 Cache 对象。
OkHttp 缓存配置与工作流程 创建缓存目录 (File对象) 设置最大大小 (maxSize) 构建Cache对象 关联OkHttpClient 发起HTTP请求 缓存是否有效? 是 否 ✅ 返回缓存响应 执行网络请求 写入缓存 / LRU淘汰 📌 LRU淘汰策略:当缓存大小超过maxSize时,OkHttp自动删除最近最少使用的缓存条目

3.4 代码实现

下面是一个完整的 OkHttp 缓存配置示例(Kotlin):

// 1. 创建缓存目录(应用私有路径)
val cacheDir = File(context.cacheDir, "okhttp_cache")
if (!cacheDir.exists()) cacheDir.mkdirs()

// 2. 设置最大缓存大小:50MB
val maxSize = 50 * 1024 * 1024L

// 3. 构建 Cache 对象
val cache = Cache(cacheDir, maxSize)

// 4. 关联 OkHttpClient(全局单例)
val okHttpClient = OkHttpClient.Builder()
    .cache(cache)
    .addNetworkInterceptor(HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.HEADERS
    })
    .build()
📌 生产建议: 将 okHttpClient 声明为 object 单例或使用依赖注入容器管理,确保整个应用进程只持有一个 Cache 实例指向同一目录。

3.5 调试与验证

通过 OkHttp 的日志拦截器观察缓存命中状态。关键响应头字段:

D/OkHttp: <-- 200 OK https://api.example.com/users (378ms)
D/OkHttp: Content-Type: application/json
D/OkHttp: Content-Length: 2148
D/OkHttp: Cache-Control: public, max-age=3600
D/OkHttp: OkHttp-Sent-Millis: 1712345678000
D/OkHttp: OkHttp-Received-Millis: 1712345678378
D/OkHttp: X-Cache: HIT from disk    ← 关键标识:缓存命中
  • X-Cache: HIT from disk —— 响应来自磁盘缓存。
  • X-Cache: MISS —— 未命中,回源网络请求。

若未看到 X-Cache 头,可通过自定义拦截器手动标记:

class CacheStatusInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val response = chain.proceed(request)
        val cacheStatus = if (response.cacheResponse() != null) "HIT" else "MISS"
        return response.newBuilder()
            .header("X-Cache", "$cacheStatus from disk")
            .build()
    }
}

3.6 常见错误与避坑

🚨 错误1:缓存目录使用外部存储或公共路径
外部存储可能被用户或系统应用清理,导致缓存失效或权限异常。务必使用 context.cacheDir 下的子目录。
🚨 错误2:maxSize 设置过小或过大
小于 1MB 会导致频繁 LRU 淘汰,几乎无法命中缓存;超过 100MB 可能挤占应用可用空间,触发系统低空间警告。建议根据业务在 10MB~100MB 之间选择。
🚨 错误3:多个 OkHttpClient 实例共享同一缓存目录
DiskLruCache 使用文件锁确保一致性。多实例操作同一目录会抛出 java.io.IOException: journal is locked。必须使用单例 Cache 对象。

3.7 性能与稳定性优化

  • 预创建目录:在 Application.onCreate 中提前调用 cacheDir.mkdirs(),避免首次网络请求的延迟。
  • 合理设置 maxSize:根据缓存条目平均大小估算条目数。例如平均每个响应 20KB,50MB 可容纳约 2500 个条目,足以覆盖大多数 App 的 API 缓存需求。
  • 监控缓存命中率:通过 cache.hitCount() 和 cache.missCount() 定期上报,若命中率低于 30% 则需检查 Cache-Control 头或增大 maxSize。
  • 避免缓存目录 IO 竞争:不要在 UI 线程执行 Cache.evictAll() 或 Cache.initialize() 等阻塞操作,DiskLruCache 内部使用异步清理线程。
  • 更新后重置缓存:若 API 数据结构变更,调用 cache.evictAll() 清空旧缓存,但务必在后台线程执行:
// 后台线程清空缓存
lifecycleScope.launch(Dispatchers.IO) {
    okHttpClient.cache?.evictAll()
    Log.d("Cache", "旧缓存已全部清除")
}
📈 性能指标参考: 在 50MB 缓存、2000 个条目的场景下,DiskLruCache 的 LRU 淘汰耗时通常 < 15ms,对 UI 线程无影响。单次缓存读取约 0.3~1.2ms(取决于文件大小和磁盘性能),远优于网络请求的数百毫秒。