4. 缓存键生成规则:URL规范化、忽略查询参数、自定义key策略
4.1 缓存键的作用与生成原理
OkHttp 的磁盘缓存以 键值对 形式存储每个网络响应。缓存键通常由请求 URL 经过特定规则处理后生成。默认情况下,OkHttp 会对原始 URL 进行规范化,并使用其作为键。本章将深入剖析缓存键生成规则,并指导你如何通过自定义策略应对特殊场景。
| 概念 | 说明 |
|---|---|
| URL 规范化 | 将 URL 转换为标准形式,包括协议降级(http→https)、默认端口移除、百分号解码、路径归一化等 |
| 忽略查询参数 | 许多 REST API 的查询参数(如 ?=xxx)不影响资源内容,应被忽略避免缓存膨胀 |
| 自定义 Key 策略 | 允许开发者完全控制缓存键的生成逻辑,例如组合多个请求属性或使用哈希值 |
4.2 URL 规范化流程
OkHttp 内置的 HttpUrl 类在构造时会自动执行规范化。核心步骤按顺序如下:
- 协议标准化:统一转为小写,例如
HTTP://→http://,默认使用 HTTP。 - 移除默认端口:80(http)和 443(https)会被移除。
- 路径归一化:解析
.和..,将路径精简到最短形式。 - 百分号解码:对非保留字符进行解码,例如
%20→ 空格。 - 主机名小写:域名转为小写。
调试验证:如果你需要确认最终缓存键,可以在 Cache.put() 或 Cache.get() 中加日志输出键的值,或直接查看缓存目录下的文件名。
4.3 忽略查询参数的重要性
默认情况下,OkHttp 将整个 URL(包括查询参数)作为缓存键的一部分。这会导致以下问题:
- 同一资源的多个不同参数版本占满磁盘缓存,降低命中率。
- 安全敏感参数(如 、sessionId)意外成为键的一部分,可能暴露信息。
常见错误:直接使用
Request.Builder.url() 传入带有随机参数的 URL,每次请求都生成不同的缓存键,导致缓存完全失效。
4.4 自定义 Key 策略的实现方案
OkHttp 的 Cache 类并未直接提供修改键的接口。但可以通过 拦截器 在写入缓存前重写请求 URL,或自定义 CacheStrategy 实现。最常用方案:
- 使用拦截器统一去除查询参数
- 替换 URL 的 Host/Path 部分为固定格式
- 对 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 缓存键生成流程示意
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());