第3章 OkHttp缓存策略配置:Cache类、缓存目录大小、max‑age、max‑stale

3.1 原理:OkHttp 缓存分层架构

Retrofit 本身不实现缓存,所有离线缓存能力实际委托给底层的 OkHttpClient。OkHttp 通过内置的 Cache 类与 HTTP 缓存控制协议(RFC 7234)配合,形成三层决策链路:

  1. 存储层:Cache 管理磁盘目录,以键值对形式存放响应体与元数据。
  2. 策略层:根据请求头 Cache-Control 中的 max‑age、max‑stale 等指令判断缓存是否可用。
  3. 拦截层:CacheInterceptor 在请求发出前拦截,返回有效缓存或放行至网络。
关键理解:Cache 类只负责“存”和“读”,而“何时使用缓存”由 Cache‑Control 参数决定。两者共同构成了 OkHttp 的离线缓存引擎。
OkHttp 缓存策略配置流程 OkHttp 缓存策略配置流程 Retrofit 请求 OkHttpClient Cache 类 缓存目录 & 大小 max‑age / max‑stale 缓存有效? (max‑age / max‑stale) 返回缓存响应 发起网络请求 注:Cache 类管理目录与大小;max‑age / max‑stale 控制缓存新鲜度决策

3.2 Cache 类:目录与大小配置

Cache 是 OkHttp 缓存的核心载体,构造时需要指定磁盘目录和最大字节数。超过上限时,OkHttp 会按照 LRU(最近最少使用)策略自动淘汰旧缓存。

3.2.1 构造方式

// 方式一:直接构造
File cacheDir = new File(context.getCacheDir(), "http_cache");
long maxSize = 10 * 1024 * 1024; // 10 MB
Cache diskCache = new Cache(cacheDir, maxSize);

// 方式二:链式添加到 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
        .cache(diskCache)
        .build();

3.2.2 目录与大小最佳实践

场景推荐大小目录位置说明
图片/视频内容 App50–100 MBcontext.getCacheDir()/okhttp文件缓存生命周期随 App 清除
新闻/列表型 App10–30 MBcontext.getExternalCacheDir()/okhttp外置缓存,可被用户清理
工具类轻量 App5–10 MBcontext.getCacheDir()/http避免占用过多用户空间
建议:缓存目录使用 context.getCacheDir() 子目录,系统在存储不足时有权清除该目录下的文件,避免 App 被用户视为“存储杀手”。

3.3 缓存控制参数:max‑age 与 max‑stale

这两个参数通过 HTTP 请求头 Cache-Control 传递给 OkHttp,控制客户端对缓存“新鲜度”的容忍程度。

3.3.1 max‑age(新鲜期)

指定缓存响应在生成后多少秒内被视为“新鲜”,此时间段内直接使用缓存,不发起网络请求。

// 服务端响应头示例(优先级高)
Cache-Control: public, max-age=3600

// 客户端请求头覆盖(使用 OkHttp 拦截器设置)
Request request = original.newBuilder()
        .header("Cache-Control", "max-age=300")
        .build();

3.3.2 max‑stale(过期容忍期)

当缓存已过期,客户端仍允许在 max‑stale 指定的秒数内继续使用该过期缓存。这对弱网或离线场景极为重要。

// 允许使用过期 7 天以内的缓存
Request request = original.newBuilder()
        .header("Cache-Control", "max-stale=604800")
        .build();

// 离线场景:允许任意过期缓存
// Cache-Control: max-stale=2147483647

3.3.3 参数对比

参数含义取值范围典型值使用场景
max‑age缓存新鲜窗口(秒)≥0300(5分钟)正常在线,减少重复请求
max‑stale过期后仍可使用的宽限期(秒)≥0604800(7天)弱网/离线,容忍陈旧数据
min‑fresh要求缓存剩余新鲜时间至少 N 秒≥060对实时性要求高的场景
only‑if‑cached强制只使用缓存,无缓存则报错无值—严格离线模式

3.4 完整配置实现(Retrofit + OkHttp)

以下代码展示如何在 Retrofit 构建时,通过 OkHttpClient 配置 Cache 类并注入缓存策略:

// 1. 创建缓存目录与 Cache 实例
File cacheDir = new File(context.getCacheDir(), "retrofit_cache");
long maxCacheSize = 20 * 1024 * 1024; // 20 MB
Cache responseCache = new Cache(cacheDir, maxCacheSize);

// 2. 自定义拦截器:动态设置 Cache-Control
Interceptor cacheInterceptor = chain -> {
    Request original = chain.request();

    // 离线时使用 max-stale,在线时使用 max-age
    String cacheControl = NetworkUtil.isOnline(context)
            ? "max-age=300"
            : "max-stale=604800";

    Request modified = original.newBuilder()
            .header("Cache-Control", cacheControl)
            .removeHeader("Pragma") // 移除旧协议干扰
            .build();

    Response response = chain.proceed(modified);

    // 服务端未指定缓存时,客户端强制设置
    if (!NetworkUtil.isOnline(context)) {
        return response.newBuilder()
                .header("Cache-Control", "max-stale=604800")
                .build();
    }
    return response;
};

// 3. 构建 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
        .cache(responseCache)
        .addInterceptor(cacheInterceptor)       // 应用层拦截器
        .addNetworkInterceptor(cacheInterceptor) // 网络层拦截器
        .build();

// 4. 构建 Retrofit 实例
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .client(client)
        .addConverterFactory(GsonConverterFactory.create())
        .build();

3.5 调试:验证缓存是否生效

OkHttp 的 Cache 类提供两个直观的统计接口,适合在调试面板或日志中输出:

// 在 OkHttpClient 构建后获取缓存统计
Cache cache = client.cache();
CacheStats stats = cache.getCacheStats();

Log.d("CacheDebug", "请求数: " + stats.requestCount());
Log.d("CacheDebug", "网络请求数: " + stats.networkCount());
Log.d("CacheDebug", "缓存命中数: " + stats.hitCount());
Log.d("CacheDebug", "缓存命中率: " + String.format("%.1f%%",
        stats.hitCount() * 100.0 / stats.requestCount()));

// 也可以直接检查磁盘占用
Log.d("CacheDebug", "当前磁盘占用: " + cache.size() + " bytes");
Log.d("CacheDebug", "最大容量: " + cache.maximumSize() + " bytes");
调试技巧:在 Android Studio Logcat 中过滤 CacheDebug 标签,观察每次请求的命中率变化。若命中率长期低于 20%,说明 max‑age 设置过短或缓存目录过大导致淘汰频繁。

3.6 常见错误与解决方案

错误现象根因解决方案
缓存从未命中未正确设置 Cache‑Control 或服务端返回 no‑cache在拦截器中强制添加 max‑age;服务端配合返回 Cache‑Control: public
缓存文件占用异常大maxSize 设置过大,且无淘汰机制将 maxSize 控制在 10–50 MB,OkHttp 自动 LRU 淘汰
离线时无法加载缓存未设置 max‑stale 或 max‑stale 值过小离线场景下使用 max‑stale=604800 或 only‑if‑cached
缓存数据与服务器不一致max‑age 过长,导致客户端使用陈旧数据缩短 max‑age;启用 ETag 或 Last‑Modified 校验
Cache 初始化报错 IOException缓存目录不可写或磁盘空间不足使用 context.getCacheDir() 确保目录可写;添加异常捕获

3.7 性能与稳定性优化

  • 目录隔离:不同业务模块使用独立的 Cache 实例(例如图片缓存与接口缓存分离),避免大文件挤压 API 缓存空间。
  • 动态调节 max‑age:根据网络质量动态调整新鲜期——WiFi 下用 60 秒,4G 下用 300 秒,离线时用 max‑stale。
  • 监控淘汰率:通过 Cache.evictCount() 监控每秒淘汰数,若淘汰率超过 10%,及时增大 maxSize 或优化缓存策略。
  • 避免重复创建:Cache 实例应在 Application 级单例化,切勿在每次请求时新建,否则会导致缓存目录重建、已有缓存失效。
警告:不要在生产环境中将 max‑stale 设置为极大值(如 Integer.MAX_VALUE),这会导致缓存永久不过期,用户永远看到旧数据。建议结合时间戳或版本号做二次校验。

3.8 本章小结

  • Cache 类通过磁盘目录和 LRU 策略管理缓存物理存储。
  • max‑age 定义缓存新鲜窗口,max‑stale 定义过期后仍可使用的宽限期。
  • 通过 OkHttp 拦截器可动态注入 Cache‑Control 头,实现离线/在线自适应。
  • 利用 CacheStats 可量化缓存命中率,指导参数调优。
  • 合理配置缓存目录大小与淘汰策略,是保障离线缓存稳定性的基石。