缓存基础:为什么我这么看重它?

做 Android 网络层好些年了,我越发觉得——缓存不是锦上添花,而是核心性能的底线。今天开篇第一课,我们就来聊聊 HTTP 缓存最基础的东西,以及它跟 Retrofit 到底有什么关系。

什么是 HTTP 缓存?

说白了,HTTP 缓存就是客户端把服务器返回的响应「暂存」一份。下次再请求同一个资源时,如果条件允许,直接用本地的副本,不再跑一趟网络。

核心思想:拿空间换时间,用存储开销降低网络延迟。

但这不意味着「所有响应都能缓存」。服务器会在响应头里明确告诉客户端:
Cache-Control: max-age=3600 —— 这个资源 1 小时内有效。
ETag: "abc123" —— 资源版本标识,用来做条件请求。

我遇到过不少开发以为「加了缓存接口就快」,结果乱配过期时间,导致用户看到旧数据。嗯,这里要特别注意:缓存协议是服务器控制的,客户端只是遵守者。

缓存与网络请求的关系

整个过程可以拆成三步:

  1. 判断缓存是否存在:如果本地没有缓存,直接发起网络请求。
  2. 判断缓存是否新鲜:根据 max-age 或 Expires 判断。如果新鲜,直接使用缓存,零网络开销。
  3. 缓存过期了怎么办?:发起条件请求(带上 If-None-Match 或 If-Modified-Since)。服务器比对后,如果资源没变,返回 304 Not Modified,客户端刷新缓存有效期;如果变了,返回 200 和新数据。

你想想看,最理想的情况是「缓存命中」——直接本地读,延迟从几百毫秒降到几毫秒。最差的情况是「缓存未命中」且服务器数据变了——完整走一次网络,但也能顺便更新缓存。真正让人头疼的是「缓存过期但资源没变却走全量请求」——浪费带宽,还慢。

我的习惯:在设计接口时,尽量让服务器返回 ETag。因为即使 max-age 设得很短,条件请求的 304 响应体为空,流量几乎为零。

为什么缓存对 Retrofit 至关重要?

Retrofit 本身不处理缓存。它只是一个声明式 HTTP 接口封装,真正干活的是底层的 OkHttp。而 OkHttp 内置了一套完整的 HTTP 缓存实现——基于 Cache 类和 CacheStrategy。

我记得有一次线上反馈:某列表页加载总是白屏几秒钟。查了日志发现,用户每次进入页面都发起了完整网络请求,接口数据近一周都没变过。加上一天有效期的缓存后,冷启动加载时间从 3s 降到了 300ms。这就是缓存的力量。

给 Retrofit 配置缓存,本质上就是给 OkHttp 的 OkHttpClient 加一个 Cache 对象:

// 创建缓存目录
File cacheDir = new File(context.getCacheDir(), "http_cache");
// 缓存大小:10MB
Cache cache = new Cache(cacheDir, 10 * 1024 * 1024);

// 配置OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
        .cache(cache)
        .addInterceptor(new MyCacheInterceptor()) // 可选的自定义拦截器
        .build();

// 创建Retrofit实例
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .client(client)
        .build();

就这么简单,Retrofit 的所有请求就自动走缓存逻辑了。但说实话,90% 的问题都不在配置,而在 服务器的缓存响应头没配对,或者客户端没理解对。

缓存命中率的维度

我们通常用以下几个指标衡量:

指标 含义 理想值
缓存命中率(Hit Rate) 请求中直接使用缓存的比例 > 60%
字节命中率(Byte Hit Rate) 从缓存获取的字节占总字节的比例 > 80%
条件请求成功率(304 Rate) 发起条件请求后返回 304 的比例 > 90%

我曾经接手过一个项目,缓存命中率不足 10%。排查发现:所有接口都没配 Cache-Control,OkHttp 默认对无缓存的响应直接不缓存。加了响应头后,命中率飙到 70%。你看,一个小头字段差这么远。

一张图看懂本章知识体系

下面这张图总结了 HTTP 缓存在 Retrofit 链路中的完整流程,以及影响命中率的关键节点。

HTTP缓存 + Retrofit 完整链路 应用发起HTTP请求 OkHttp缓存策略判断 ✅ 缓存命中(新鲜) 条件请求(ETag/If-Modified) 完整网络请求 返回304或200,并更新/使用缓存 关键:缓存命中率 = 直接使用缓存次数 ÷ 总请求次数 理想状态下应达到60%以上,字节命中率80%以上 直接返回,0网络开销 资源未变返回304 资源变化返回200

常见误区与避坑

我曾经踩过的坑:

  • 误以为添加了 Cache 对象就万事大吉,结果接口返回了 Cache-Control: no-store,根本存不下来。
  • 在调试模式下频繁清理缓存,导致「命中率」数据失真。生产环境一定要用真实的用户行为数据。
  • 忘记给 OkHttp 配 cache 对象——Retrofit 不会自动帮你加,必须手工配置。

我的建议:在开发阶段,先让后端同事确认每个接口的缓存策略。推荐使用 Cache-Control: public, max-age=86400 这类常见配置,配合 ETag 做验证。初期宁可保守一点,设长过期时间,后续再根据业务调整。

好了,这一章我们把缓存的基础概念、与网络请求的配合方式、以及 Retrofit 里怎么搭架子都走了一遍。记住一句话:缓存不是玄学,是工程。后面我会带你实战提升命中率,从拦截器到策略定制,一步步攻破。