Retrofit与OkHttp集成:Retrofit如何集成OkHttp缓存、自定义OkHttpClient
很多同学刚接触 Retrofit 时,以为它自己能搞定缓存。其实不然。Retrofit 本质上是一个 接口代理工厂,真正干活的底层网络引擎是 OkHttp。说白了,Retrofit 负责把接口注解翻译成 HTTP 请求,而 OkHttp 负责发请求、收响应、管连接池、做缓存。
我刚开始用 Retrofit 时也踩过这个坑——在 Retrofit 层面折腾了半天缓存配置,结果发现 API 文档里根本没提供缓存方法。后来才明白,缓存这件事得交给 OkHttp。你把 OkHttpClient 配好了,Retrofit 自然就跟着有了缓存能力。
1. 理解集成关系:谁负责什么
先理清楚这三层的关系:
- 应用层:你写的 Activity / Fragment / ViewModel,调用 Retrofit 接口方法。
- Retrofit 层:解析注解(如
@GET、@POST),生成 OkHttp 的Request对象,并把 OkHttp 的Response反序列化成 Java 对象。 - OkHttp 层:管理连接池、超时、拦截器链、缓存策略,最终执行真正的网络请求。
你看,缓存这件事从 Retrofit 的角度是透明的。它只管把请求交给 OkHttp,至于 OkHttp 是走缓存还是走网络,Retrofit 不关心,也不需要关心。这种分层设计其实很干净——各司其职。
2. 自定义 OkHttpClient 配置缓存
好,现在来看具体怎么做。配置 OkHttp 的缓存,你需要三样东西:缓存目录、缓存大小、Cache 对象。
我个人的习惯是把缓存大小控制在 10MB 到 50MB 之间。太小了缓存命中率低,太大了容易挤占用户存储空间,尤其在国内的 Android 生态下,用户对存储敏感。
// 1. 创建缓存目录
File cacheDir = new File(context.getCacheDir(), "http_cache");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
// 2. 设置缓存大小 10MB
long cacheSize = 10 * 1024 * 1024;
// 3. 构建 Cache 对象
Cache cache = new Cache(cacheDir, cacheSize);
// 4. 构建自定义 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build();
context.getCacheDir(),不要用外部存储。我之前有个项目用了外部存储路径,结果在 Android 11 的 scoped storage 政策下直接翻车,缓存写不进去,接口全部走网络。切记!
缓存大小怎么定?我一般按这个标准来:
| 应用类型 | 建议缓存大小 | 缓存有效期 (max-age) |
|---|---|---|
| 新闻 / 内容类 | 20MB ~ 50MB | 300s ~ 600s |
| 电商 / 商品类 | 10MB ~ 30MB | 60s ~ 300s |
| IM / 实时类 | 5MB ~ 10MB | 10s ~ 60s |
| 工具 / 轻量类 | 2MB ~ 5MB | 600s ~ 3600s |
3. 将 OkHttpClient 注入 Retrofit
OkHttpClient 配好后,注入 Retrofit 就一行代码的事。通过 Retrofit.Builder.client() 传进去就行。
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.yourapp.com/")
.client(client) // 👈 注入自定义 OkHttpClient
.addConverterFactory(GsonConverterFactory.create())
.build();
// 创建 API 接口实例
ApiService api = retrofit.create(ApiService.class);
注意了,client() 方法只能调用一次。如果你多次调用,后面会覆盖前面。我见过有同事在 Builder 链式调用里不小心调了两次,结果第一次配的缓存被冲掉了。
Builder.client() 是 setter 覆盖语义,不是 add 语义。如果你有多个拦截器要加,请在 OkHttpClient.Builder 阶段全部配完,不要在 Retrofit 层面做文章。
4. 拦截器与缓存策略控制
缓存目录和大小配好只是第一步。实际项目中,你还需要通过 拦截器 来控制缓存策略。比如无网络时用缓存、有网络时按服务端头部的缓存指令来。
举个例子,我曾在某个户外巡检项目里遇到这种情况——用户在隧道里没信号,页面上数据全是白的。后来加了个缓存拦截器,无网络时自动读取本地缓存,用户体验瞬间好多了。
public class CacheInterceptor implements Interceptor {
private final Context context;
public CacheInterceptor(Context context) {
this.context = context;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (!isNetworkAvailable()) {
// 无网络:强制走缓存,最多容忍过期 7 天
request = request.newBuilder()
.header("Cache-Control", "only-if-cached, max-stale=" + (7 * 86400))
.build();
} else {
// 有网络:允许缓存 60 秒
request = request.newBuilder()
.header("Cache-Control", "max-age=60")
.build();
}
Response response = chain.proceed(request);
// 统一设置缓存过期时间(覆盖服务器返回的 Cache-Control)
return response.newBuilder()
.header("Cache-Control", "max-age=60")
.removeHeader("Pragma")
.build();
}
private boolean isNetworkAvailable() {
ConnectivityManager cm = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getActiveNetworkInfo();
return info != null && info.isConnected();
}
}
然后把拦截器加到 OkHttpClient 里:
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(new CacheInterceptor(context)) // 👈 应用拦截器
.build();
addInterceptor() 是应用拦截器,在请求发出前和响应回来后各触发一次;addNetworkInterceptor() 是网络拦截器,只在真正走网络时触发。缓存策略建议用应用拦截器,因为它能覆盖「命中缓存不走网络」的场景。
5. 避坑指南:我踩过的三个坑
讲几个真实教训,你遇到类似问题可以直接排查。
-
缓存文件没写入权限 —— 我曾经在 Android 6.0 的平板上遇到过,
getCacheDir()返回的是/data/data/包名/cache,但应用没配WRITE_EXTERNAL_STORAGE权限,缓存一直写不进去。后来发现是测试机 ROM 的 bug,升级系统就好了。嗯,这种问题排查起来最耗时间。 -
缓存被 Pragma 头覆盖 —— 有些老接口返回
Pragma: no-cache,这个头的优先级高于Cache-Control。你明明配了max-age=60,结果每次还是走网络。解决方式就是拦截器里removeHeader("Pragma"),上面代码里已经写了。 - 多个 Retrofit 实例用了同一个 OkHttpClient —— 这不是 bug,但要注意:如果多个 Retrofit 实例共享同一个 OkHttpClient,那它们的缓存策略也是共享的。如果你想给登录模块和首页模块配不同的缓存策略,记得创建不同的 OkHttpClient 实例。
① 用
context.getCacheDir() 建缓存目录;② 缓存大小按应用类型选择 10MB~50MB;
③ 通过应用拦截器统一控制缓存策略;
④ 覆盖服务端返回的
Cache-Control,移除 Pragma;⑤ 无网络时用
only-if-cached + max-stale 保底。