OkHttp 缓存架构:它到底怎么存的
讲到 Retrofit 缓存,绕不开 OkHttp。毕竟 Retrofit 只是把网络请求封装成了接口,真正干活的是 OkHttp。缓存这块更是彻底交给了 OkHttp。我当初刚接手公司项目时,发现缓存配置乱七八糟,有的接口重复请求几十次——说白了,就是没搞懂 OkHttp 的缓存架构。
咱们先画个图,把整体脉络理清楚。
Cache 类——入口也是总控
OkHttp 的缓存入口非常干净。你只需要构建 OkHttpClient 的时候扔一个 Cache 对象进去。这个 Cache 类干了三件事:
- 管理存储:内部包装了 DiskLruCache,负责把响应数据写到磁盘。
- 决策缓存:根据请求头和响应头,判断「能不能用缓存」「缓存是否过期」。
- 提供状态:我可以随时查 hit count、miss count、network count——方便调优。
小提示:我刚用 OkHttp 时以为 Cache 类会维护内存缓存,其实它只在磁盘干活。内存里的缓存条目只有个 LRU 索引,数据还是从磁盘读的。所以别指望它像图片缓存一样毫秒级响应。
看看最常用的构造方式:
// 指定目录 /data/data/包名/cache/http
// 最大缓存 10 MB
int cacheSize = 10 * 1024 * 1024;
File cacheDir = new File(context.getCacheDir(), "http");
Cache cache = new Cache(cacheDir, cacheSize);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();
注意:cacheSize 是硬盘空间的硬限制。达到上限后,OkHttp 会按照 LRU 策略逐步淘汰旧数据。我在项目中遇到过有人把缓存设成 0 字节,其实等于没缓存——因为 OkHttp 的实现里,如果 maxSize == 0 会直接跳过所有缓存逻辑。
缓存目录——别乱放
缓存目录的选择挺讲究。我个人习惯放在 context.getCacheDir() 下,因为 Android 会在系统空间不足时自动清理这个目录。很多新手直接写死路径 /sdcard/myapp/cache,结果应用被系统清理时数据残留,甚至引发 Crash。
OkHttp 在缓存目录里会创建这些文件:
| 文件/目录 | 作用 |
|---|---|
journal |
日志文件,记录所有缓存操作的索引。DiskLruCache 靠它维持 LRU 顺序。 |
journal.tmp |
临时文件,写 journal 时使用。读写异常时可用来恢复。 |
journal.bkp |
journal 的备份,当 journal 损坏时会用这个恢复。 |
[0-9a-f]{16}.0 |
缓存条目文件,每个 URL 对应一个随机命名的文件,存储响应 Header 和 Body。 |
⚠ 我曾经踩过的坑: 有用户反馈缓存目录下生成了一堆 .0 文件无法清理。查了半天发现是 DiskLruCache 的 journal 文件被系统 kill 时写坏了,导致缓存条目无法回收。后来我加了一行代码:cache.evictAll() 在应用启动时检测 journal 损坏就清空。
缓存大小——设多少才合适?
这是一个被问烂了的问题。知乎上有人说 10MB,有人说 50MB。说实话,没有标准答案。我个人的经验值是:
- 图文类应用:10~20MB。图片已经在 Glide 那边做了磁盘缓存,OkHttp 只缓存 API 响应。
- 列表类应用:5~10MB。首页数据、分类列表,响应体通常很小。
- 音视频元数据应用:1~2MB。真的不需要大,元数据而已。
你可能会问:设大了会不会有副作用?会。DiskLruCache 启动时需要读 journal 文件重建 LRU 索引,journal 大小和缓存条目数成正比。我试过把缓存设成 100MB,应用冷启动时 journal 文件解析花了 200ms——用户能明显感觉到白屏。
技巧:想要实时监控缓存大小?通过 Cache.size() 方法获取当前已使用的字节数,配合 maxSize() 算一下使用率。我一般在开发版首页展示这个指标,方便调优。
缓存目录和大小设置的完整示例
把上面的知识串起来,给一个我自认为比较健壮的做法:
public class OkHttpCacheProvider {
private static final int MAX_CACHE_SIZE = 10 * 1024 * 1024; // 10MB
public static Cache provideCache(Context context) {
File cacheDir = new File(context.getCacheDir(), "okhttp_cache");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
Cache cache = new Cache(cacheDir, MAX_CACHE_SIZE);
// 检测 journal 是否损坏
try {
cache.initialize(); // 内部会尝试读取 journal
} catch (IOException e) {
// journal 损坏,清空重建
try {
cache.evictAll();
} catch (IOException ex) {
// 不理了
}
}
return cache;
}
}
嗯,注意 cache.initialize() 这个方法是 OkHttp 4.x 内部暴露的,外部调用需要用反射或者升级到 OkHttp 5.x。如果不想折腾,最简单的判断就是:启动后取 cache.size() 如果返回负数或者抛出异常,就 evictAll()。
说了这么多,你可能会想:怎么知道我设的缓存大小到底够不够?其实 OkHttp 提供了几个有用的统计方法:
| 方法 | 说明 |
|---|---|
cache.hitCount() |
缓存命中的次数 |
cache.missCount() |
缓存未命中的次数 |
cache.networkCount() |
实际发送网络请求的次数 |
cache.requestCount() |
所有请求的总次数 |
我有个习惯:在 Release 版本里埋点上报这些数据。如果hitCount / requestCount 长期低于 30%,说明缓存策略太保守或者缓存太小,该调了。
核心结论:OkHttp 缓存架构本质上就是「DiskLruCache + 策略决策」。Cache 类把这两块包了一层,暴露给开发者一个清爽的接口。缓存目录和大小虽然基础,但踩坑概率不低。我这几年遇到的线上问题,有一小半都能追溯到「缓存目录被系统清掉但 journal 没同步」或者「缓存大小设得太大导致启动卡顿」。
最后提一下,看到这里你应该对 OkHttp 的缓存骨架有数了。下一层咱们就要深入缓存策略——那些 CacheControl、max-age、must-revalidate 到底是啥意思?不急,先把基础打牢。