4. 缓存键生成规则:URL规范化、忽略查询参数、自定义key策略

4.1 缓存键的作用与生成原理

OkHttp 的磁盘缓存以 键值对 形式存储每个网络响应。缓存键通常由请求 URL 经过特定规则处理后生成。默认情况下,OkHttp 会对原始 URL 进行规范化,并使用其作为键。本章将深入剖析缓存键生成规则,并指导你如何通过自定义策略应对特殊场景。

概念说明
URL 规范化将 URL 转换为标准形式,包括协议降级(http→https)、默认端口移除、百分号解码、路径归一化等
忽略查询参数许多 REST API 的查询参数(如 ?=xxx)不影响资源内容,应被忽略避免缓存膨胀
自定义 Key 策略允许开发者完全控制缓存键的生成逻辑,例如组合多个请求属性或使用哈希值

4.2 URL 规范化流程

OkHttp 内置的 HttpUrl 类在构造时会自动执行规范化。核心步骤按顺序如下:

  1. 协议标准化:统一转为小写,例如 HTTP:// → http://,默认使用 HTTP。
  2. 移除默认端口:80(http)和 443(https)会被移除。
  3. 路径归一化:解析 . 和 ..,将路径精简到最短形式。
  4. 百分号解码:对非保留字符进行解码,例如 %20 → 空格。
  5. 主机名小写:域名转为小写。

调试验证:如果你需要确认最终缓存键,可以在 Cache.put() 或 Cache.get() 中加日志输出键的值,或直接查看缓存目录下的文件名。

4.3 忽略查询参数的重要性

默认情况下,OkHttp 将整个 URL(包括查询参数)作为缓存键的一部分。这会导致以下问题:

  • 同一资源的多个不同参数版本占满磁盘缓存,降低命中率。
  • 安全敏感参数(如 、sessionId)意外成为键的一部分,可能暴露信息。
常见错误:直接使用 Request.Builder.url() 传入带有随机参数的 URL,每次请求都生成不同的缓存键,导致缓存完全失效。

4.4 自定义 Key 策略的实现方案

OkHttp 的 Cache 类并未直接提供修改键的接口。但可以通过 拦截器 在写入缓存前重写请求 URL,或自定义 CacheStrategy 实现。最常用方案:

  1. 使用拦截器统一去除查询参数
  2. 替换 URL 的 Host/Path 部分为固定格式
  3. 对 URL 进行 MD5 或 SHA1 哈希后作为键(配合自定义 Cache 实现)

推荐实现:重写 Interceptor,在请求发出前根据规则生成新的 URL,并保存在 Request.tag() 中供后续调试。

public class CacheKeyInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request originalRequest = chain.request();
        HttpUrl originalUrl = originalRequest.url();

        // 1. 构建新 URL:仅保留 scheme + host + path,去掉所有查询参数
        HttpUrl newUrl = originalUrl.newBuilder()
                .scheme(originalUrl.scheme())
                .host(originalUrl.host())
                .port(originalUrl.port())
                .encodedPath(originalUrl.encodedPath())
                .query(null)          // 清空查询参数
                .build();

        // 2. 创建新请求(保留 headers 和 body)
        Request newRequest = originalRequest.newBuilder()
                .url(newUrl)
                .tag("originalUrl", originalUrl.toString())  // 保留原始 URL 用于调试
                .build();

        return chain.proceed(newRequest);
    }
}

将此拦截器添加到 OkHttpClient 时,需放在其他拦截器之前(尤其是日志拦截器前),且不要与自定义 Cache 重复修改 URL。

4.5 缓存键生成流程示意

缓存键生成流程 (URL规范化 & 自定义策略) 原始请求 URL 步骤1 自动 URL 规范化 步骤2 规范化 URL A. 忽略查询参数 B. 自定义Key策略 最终缓存键 说明:默认情况下 OkHttp 仅执行步骤1-2,不会自动忽略查询参数或自定义 Key

4.6 部署与配置注意事项

场景推荐做法避免
纯静态资源(图片、CSS)忽略所有查询参数,仅用 path 作为键保留版本号参数(如 ?v=1)导致重复缓存
REST API 带分页参数将 ?page=1 纳入键(不同 page 是不同资源)忽略所有参数导致数据错乱
安全敏感 必须移除或以哈希方式处理直接作为键暴露在缓存文件名中
微服务多域名保留 host 但可对路径做合规化使用全局自定义 Key 忘记包含 host,造成跨域缓存污染

4.7 性能与稳定性优化

  • 降低磁盘 I/O:通过合理忽略无关参数,减少缓存键数量,从而减少目录扫描和文件创建次数。
  • 防止哈希碰撞:如果使用 MD5 作为 Key,建议配合 URL 长度或其他标志位避免不同 URL 映射到同一文件。
  • 缓存预热:在应用启动时,可以遍历缓存目录,使用自定义 Key 策略重建索引,提升后续命中速度。
  • 监控与可视化:定期导出缓存键列表,观察是否有异常的大量重复或缺失。

性能测试小工具:使用 okhttp-eventsource 或自定义 EventListener 监听 cacheHit/cacheMiss 事件,结合第3章的缓存大小配置,量化优化效果。

4.8 常见错误与排错方法

错误现象可能原因解决方案
缓存始终不命中URL 包含随机参数(如时间戳)且未忽略使用拦截器清除或移除该参数
缓存键冲突导致返回错误数据自定义 Key 规则未包含足够的标识(如 method、host)将请求方法、host、path 拼接为复合键
磁盘上出现大量同名但内容不同的文件自定义拦截器未正确处理重定向后的 URL确保在拦截器内处理 chain.request() 的重定向链
调试时无法定位缓存键未定义 tag 或日志级别过高在拦截器中添加 Log.d("CacheKey", newUrl.toString())

建议在集成测试中用固定参数验证缓存键生成结果,例如:

// 测试代码片段
HttpUrl url = HttpUrl.parse("HTTP://Example.com:80/path/to/resource?=abc&page=2");
HttpUrl normalized = url.newBuilder()
        .scheme("https")
        .host("example.com")
        .port(443)
        .encodedPath("/path/to/resource")
        .query(null)
        .build();
assertEquals("https://example.com/path/to/resource", normalized.toString());