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 核心配置参数
| 参数 | 类型 | 说明 | 建议值 |
|---|---|---|---|
| directory | File | 缓存文件存放的目录,必须可读写且唯一 | context.cacheDir / "okhttp_cache" |
| maxSize | long | 缓存最大字节数,超限后触发 LRU 淘汰 | 10MB ~ 100MB(根据图片/资源量调整) |
| deleteStrategy | 内置 | DiskLruCache 自动执行 LRU 算法,无手动策略接口 | — |
💡 关键理解: maxSize 不是硬限制,而是“软水位”。OkHttp 在每次写入新缓存条目后检查总大小,若超出则异步执行 LRU 清理。因此实际占用可能短暂略高于 maxSize,但最终收敛到设定值以下。
3.3 实现流程
- 确定目录——使用
context.cacheDir创建子目录,确保路径唯一且应用私有。 - 计算容量——根据业务场景估算缓存上限(图片多则 50MB+,纯 API 响应 10MB 足够)。
- 构造 Cache——调用
Cache(directory, maxSize)初始化 DiskLruCache。 - 注入 OkHttpClient——通过
Builder.cache(cache)关联客户端。 - 请求响应——OkHttp 自动判断缓存头、读写磁盘、触发 LRU 淘汰。
⚠️ 禁止在多处重复创建 Cache 实例指向同一目录: 多个 Cache 对象操作同一目录会导致文件锁冲突、元数据损坏。必须全局单例共享一个 Cache 对象。
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 之间选择。
小于 1MB 会导致频繁 LRU 淘汰,几乎无法命中缓存;超过 100MB 可能挤占应用可用空间,触发系统低空间警告。建议根据业务在 10MB~100MB 之间选择。
🚨 错误3:多个 OkHttpClient 实例共享同一缓存目录
DiskLruCache 使用文件锁确保一致性。多实例操作同一目录会抛出
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(取决于文件大小和磁盘性能),远优于网络请求的数百毫秒。