实战:为 Retrofit 添加基础缓存
好,咱们直接动手。说到缓存,我习惯在项目初期就把它加进去 —— 因为后期再补,往往要改不少代码。Retrofit 本身不直接管缓存,全靠底层的 OkHttp。所以给 Retrofit 加缓存,本质上就是给 OkHttp 配个 Cache 对象,再设置合理的拦截策略。
1. 缓存到底放在哪?
先想一个问题:缓存在哪存?说白了,就是本地磁盘上的一个目录。OkHttp 会在这个目录下维护一个 journal 文件,以及一堆缓存实体文件。我见过有人直接把缓存丢到 /sdcard 上 — 嗯,那权限问题够你喝一壶的。正确的路径应该是 context.cacheDir 或者 context.externalCacheDir。
internalCacheDir 也会被清掉,这符合用户预期。
2. 缓存需要多大?
别随便写个 10MB 就完事。我通常在项目里根据实际图片和 JSON 大小来算:
- 纯 API 响应(JSON):一个接口响应平均 2-5KB,缓存 500 个响应大概 2.5MB
- 如果有小图(缩略图):每个 50-100KB,缓存 100 张 ≈ 10MB
- 总缓存大小建议取两者的和再乘 1.5,留点余量
我个人习惯设 20MB,对大多数 App 够用,还不占太多空间。
3. 核心代码:三步搞定基础缓存
下面这段代码,你在项目里直接拷贝就能用。不过别只复制 — 看懂每一步的作用才是关键。
// 1. 创建缓存目录
File cacheDir = File(context.cacheDir, "http_cache")
val cacheSize = 20L * 1024 * 1024 // 20MB
// 2. 构建带缓存的 OkHttpClient
val cache = Cache(cacheDir, cacheSize)
val okHttpClient = OkHttpClient.Builder()
.cache(cache)
.addInterceptor { chain ->
val request = chain.request()
// 这里可以设置统一的缓存策略
val response = chain.proceed(request)
response.newBuilder()
.header("Cache-Control", "public, max-age=60") // 缓存 60 秒
.build()
}
.build()
// 3. 构建 Retrofit
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient) // 注入 OkHttpClient
.addConverterFactory(GsonConverterFactory.create())
.build()
注意看 addInterceptor 那段。我在项目中遇到过一个坑:如果不显式添加 Cache-Control 头,服务端返回的缓存策略可能会被客户端忽略。说白了,客户端和服务端得“商量好”缓存规则。
4. 缓存策略到底怎么设?
你想想看,如果一个接口的数据一天内不会变,你给它设个 1 分钟缓存,那跟没设一样。我们得区分场景:
| 场景 | 推荐缓存策略 | 说明 |
|---|---|---|
| 用户信息 / 配置 | max-age=300 |
5 分钟刷新一次,基本够用 |
| 新闻列表 / 推荐流 | max-age=60 |
1 分钟,用户来回切也能看旧数据 |
| 商品详情页 | max-age=0, must-revalidate |
每次都校验,但可以走条件请求(304) |
| 图片资源 | max-age=86400 |
1 天,很少变动 |
| 直播预告等实时信息 | no-cache |
不缓存,但可以存 ETag |
说实话,大部分团队一开始都只设个统一的 max-age。我建议尽早拆分策略,否则缓存命中率上不去 — 你跟老板解释“为什么刷新了还是旧数据”时会很头疼。
5. 避坑指南
OkHttpClient 构建前创建好。如果你在子线程创建目录,而主线程已经在构建客户端了,会导致 FileNotFoundException。我曾经因为这个 bug 查了一下午。
Android Studio 的 Device File Explorer 里查看 /data/data/包名/cache/http_cache/ 目录下的 journal 文件。里面记录了每个请求的缓存 key、大小和状态,是排查缓存不命中的利器。
6. 怎么验证缓存命中率?
加完缓存,心里没底?我一般用两种方式:
- 日志法: 在拦截器里打印
response.cacheResponse()和response.networkResponse()。如果cacheResponse不为 null,说明命中了缓存。 - 抓包法: 用 Charles 或 Stetho 看请求是否真的出去了。没发网络请求,就是缓存生效了。
下面这张图把整个配置流程串了起来,你可以对照着梳理自己的代码。
这份基础配置跑起来后,你就能在日志里看到缓存命中的打印。后面我们再深入聊怎么针对不同接口精细调节策略、怎么处理离线缓存、以及怎么统计命中率曲线。
app/build.gradle 中加入 implementation("com.squareup.okhttp3:okhttp:4.11.0") 依赖,然后照着上面的代码配一遍。你会看到第二次请求明显变快。