OkHttp 缓存策略:缓存目录、缓存策略、离线缓存
说到 OkHttp 的缓存,我觉得有必要先聊一个亲身踩过的坑。有一次公司 App 在弱网环境下频繁闪退,查了半天才发现——缓存目录没配好,OkHttp 把缓存写到了外部存储一个临时路径,系统一回收,缓存就崩了。从那以后我对缓存这块就格外较真。
OkHttp 的缓存机制其实不复杂,但要真正用对,你得理清三个维度:缓存目录(存哪)、缓存策略(怎么存、存多久)、离线缓存(没网时怎么办)。咱们一个一个说。
1. 缓存目录
OkHttp 使用磁盘缓存,数据存在本地文件里。你需要在构建 OkHttpClient 时配置一个 Cache 对象,它会自动管理缓存文件的读写和释放。
context.cacheDir),既安全又不会被用户误删。放到 context.externalCacheDir 虽然空间大,但容易被清理掉,稳定性差一些。
配置示例:
// 缓存目录大小 10MB
long cacheSize = 10 * 1024 * 1024;
File cacheDirectory = new File(context.getCacheDir(), "okhttp_cache");
Cache cache = new Cache(cacheDirectory, cacheSize);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();
这里有个细节:缓存大小必须是 64KB 的倍数,否则 OkHttp 会直接抛异常。曾经我在项目里写过 10 * 1024 * 1024 - 1,编译能过,跑起来就崩。嗯,这种边界问题很容易忽略。
缓存目录的结构其实很简单:
journal文件:记录缓存索引,类似数据库的日志。xxxxx.0文件:缓存响应体数据。xxxxx.1文件:缓存元数据(HEADERS)。
如果你发现缓存目录里的 journal 文件损坏了,OkHttp 会自动重建,但代价是丢失所有缓存。所以不建议在运行时手动去修改目录里的文件。
SharedPreferences 里动态读取,最好在 Application.onCreate() 中一次性初始化。我见过有人每次请求前都 new 一个 Cache 对象……后果就是内存泄漏加缓存永远不生效。
2. 缓存策略(Cache-Control)
缓存策略的核心就是 HTTP 协议里的 Cache-Control 头。OkHttp 完全遵循这个标准,你不需要额外写逻辑,只要正确设置请求/响应的 header 就行。
常用的指令有哪些?我整理了一张表:
| 指令 | 含义 | 放在哪 |
|---|---|---|
max-age=3600 |
缓存 1 小时内有效,过期后必须重新验证 | 响应头 / 请求头 |
no-cache |
每次使用缓存前必须向服务器验证(ETag/Last-Modified) | 响应头 / 请求头 |
no-store |
完全禁止缓存,不写磁盘 | 响应头 / 请求头 |
only-if-cached |
仅从缓存获取,没有缓存就返回 504 | 请求头 |
min-fresh=60 |
缓存有效期至少在 60 秒以上才使用 | 请求头 |
实际项目中怎么用? 举个栗子。我们做新闻类 App,列表页数据 5 分钟内不变化,后端就可以在响应里返回 Cache-Control: public, max-age=300。客户端第一次请求后,5 分钟内相同请求直接走缓存,不联网。
但有些接口(比如用户余额)不能缓存,后端返回 Cache-Control: no-store, private。你想想看,要是用户看到别人钱包里的余额,那是多大的事故。
最容易被忽略的一点:如果服务器返回了 no-cache,并不代表完全不缓存,而是缓存必须在下次使用时重新验证。验证的方法就是 ETag 或 Last-Modified。OkHttp 会帮你自动发条件请求(If-None-Match / If-Modified-Since),如果服务器返回 304,就直接用缓存。
有时候我们需要覆盖服务器策略。比如开发阶段关掉缓存,可以给请求添加 CacheControl.FORCE_NETWORK:
Request request = new Request.Builder()
.url(url)
.cacheControl(CacheControl.FORCE_NETWORK)
.build();
反之,想要离线模式就得用 CacheControl.FORCE_CACHE,这个我下面详细说。
3. 离线缓存
离线缓存说白了就是——没有网络时,从缓存里拿旧数据展示,而不是让用户看到白屏。这个场景在电商、资讯类 App 里太常见了。我去年做海外市场 App 时,很多地区的网络很不稳定,离线缓存几乎成了核心体验。
OkHttp 本身不提供“自动离线切换”,你需要自己判断网络状态,然后动态调整 CacheControl。
基本思路:
- 联网时:正常请求,尊重服务器
Cache-Control。 - 断网时:给所有请求加上
CacheControl.FORCE_CACHE,强制只用缓存。
代码实现示例:
CacheControl cacheControl;
if (isNetworkAvailable(context)) {
cacheControl = CacheControl.FORCE_NETWORK;
} else {
cacheControl = CacheControl.FORCE_CACHE;
}
Request request = new Request.Builder()
.url(url)
.cacheControl(cacheControl)
.build();
FORCE_CACHE 时,如果缓存里根本没有这个请求的数据,OkHttp 会返回 504 错误。所以你要做好兜底——要么展示默认占位图,要么显示“网络未连接”提示。
还有一个进阶技巧:预缓存。在用户有网络时,主动请求一些大概率会用到的数据,把缓存养起来。比如用户刷完列表后,App 可以在后台静默请求前 5 条内容的详情接口,确保离线后也能看到详情。
FORCE_CACHE。像“用户的登录状态”“配置开关”这类强依赖实时性的接口,离线时应该直接显示错误,而不是展示旧数据。你想想看,如果用户都退出登录了,还展示之前的个人首页,那是功能 bug。
OkHttp 的缓存有效期是和服务端 Cache-Control 挂钩的。如果你想要一个更灵活的离线策略——比如“离线时即使缓存过期也先用着”——可以自己实现一个 Interceptor 来覆盖缓存规则。这里给个简化版的思路:
class OfflineInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (!isNetworkAvailable()) {
// 离线时允许使用过期缓存
request = request.newBuilder()
.cacheControl(new CacheControl.Builder()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build())
.build();
}
return chain.proceed(request);
}
}
maxStale 这个参数很多人不知道——它告诉 OkHttp,即使缓存过期了,只要还在 maxStale 时间内,仍然可以返回。你可以设成非常长的值(比如 7 天),这样离线缓存就变成了“只要曾经缓存过,都能看”。
小结一下:缓存核心逻辑
我画了一张图,把 OkHttp 原生的缓存决策流程整理出来了:
看完这张图你会发现,OkHttp 的缓存本质上是一个状态机:有缓存 → 看是否过期 → 过期则验证 / 未过期则直接返回。离线模式不过是把这个状态机里的“网络检查”分支全部堵死,强制走缓存路径。
最后聊一句:别为了 Mock 测试而把缓存搞复杂。很多团队在做 CI 分层测试时,直接给测试环境的请求加上 Cache-Control: no-cache,这样每次都是真实请求,保证 Mock 数据能落到被测接口上。我个人觉得这是一种省心又安全的做法。