Retrofit与OkHttp集成:Retrofit如何集成OkHttp缓存、自定义OkHttpClient

很多同学刚接触 Retrofit 时,以为它自己能搞定缓存。其实不然。Retrofit 本质上是一个 接口代理工厂,真正干活的底层网络引擎是 OkHttp。说白了,Retrofit 负责把接口注解翻译成 HTTP 请求,而 OkHttp 负责发请求、收响应、管连接池、做缓存。

我刚开始用 Retrofit 时也踩过这个坑——在 Retrofit 层面折腾了半天缓存配置,结果发现 API 文档里根本没提供缓存方法。后来才明白,缓存这件事得交给 OkHttp。你把 OkHttpClient 配好了,Retrofit 自然就跟着有了缓存能力。

核心结论: Retrofit 不提供缓存 API,缓存能力完全来自 OkHttp。你要做的是自定义 OkHttpClient,配好缓存,再注入到 Retrofit 里。

1. 理解集成关系:谁负责什么

先理清楚这三层的关系:

  • 应用层:你写的 Activity / Fragment / ViewModel,调用 Retrofit 接口方法。
  • Retrofit 层:解析注解(如 @GET、@POST),生成 OkHttp 的 Request 对象,并把 OkHttp 的 Response 反序列化成 Java 对象。
  • OkHttp 层:管理连接池、超时、拦截器链、缓存策略,最终执行真正的网络请求。

你看,缓存这件事从 Retrofit 的角度是透明的。它只管把请求交给 OkHttp,至于 OkHttp 是走缓存还是走网络,Retrofit 不关心,也不需要关心。这种分层设计其实很干净——各司其职。

Retrofit 与 OkHttp 集成架构 ▸ 调用层 ▸ 桥接层 ▸ 执行层(含缓存与网络) 应用层 Activity / ViewModel Retrofit 层 接口定义 / 注解解析 OkHttp 层 自定义 Client / 缓存配置 缓存拦截器 缓存策略判断 磁盘缓存 缓存存储 / 读取 网络层 远程服务器请求 有缓存? 无缓存?

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 ~ 50MB300s ~ 600s
电商 / 商品类10MB ~ 30MB60s ~ 300s
IM / 实时类5MB ~ 10MB10s ~ 60s
工具 / 轻量类2MB ~ 5MB600s ~ 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 链式调用里不小心调了两次,结果第一次配的缓存被冲掉了。

⚠️ 重要: Retrofit 的 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 保底。