缓存策略选型:缓存优先、网络优先、先网络后缓存、先缓存后网络
离线缓存的核心是选择合适的缓存策略,以平衡数据新鲜度与可用性。四种常用策略分别适用于不同业务场景:
| 策略 | 描述 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 缓存优先 Cache First |
有缓存直接返回,不触发网络请求 | 新闻列表、配置信息等不频繁变化的数据 | 响应极快、零网络消耗 | 数据可能陈旧 |
| 网络优先 Network First |
无条件发起网络请求,失败时回退到缓存 | 支付状态、实时评分等需要最新数据的场景 | 确保数据最新 | 离线时等待超时才会降级 |
| 先网络后缓存 Network → Cache |
先请求网络,成功后更新缓存并返回;失败则返回缓存 | 用户首页 Feed、商品详情(允许短暂延迟) | 平衡新老数据、降低网络失败影响 | 增加一次网络等待 |
| 先缓存后网络 Cache → Network |
立即返回缓存,同时异步发起网络请求更新缓存(Stale-While-Revalidate) | 列表页、搜索建议、可接受后台刷新的内容 | 首屏秒开、后台自动刷新 | 可能出现短暂的不一致 |
关键原理:四种策略本质上是 读取缓存 与 发起网络请求 两个操作的不同排列组合。在 OkHttp 拦截器中通过判断请求头中的
@CacheStrategy 注解或自定义 header 来路由到对应逻辑分支。
决策流程
下图展示了拦截器中根据策略标志进行分支的通用逻辑。每个策略对应一组缓存的读取/写入时机和优先级。
拦截器实现核心代码
在自定义 Interceptor 中,通过读取请求头 X-Cache-Strategy 来分支。以下为关键逻辑骨架:
class CacheStrategyInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val strategy = request.header("X-Cache-Strategy") ?: "cache_first"
return when (strategy) {
"cache_first" -> cacheFirst(chain, request)
"network_first" -> networkFirst(chain, request)
"network_then_cache" -> networkThenCache(chain, request)
"cache_then_network" -> cacheThenNetwork(chain, request)
else -> cacheFirst(chain, request)
}
}
private fun cacheFirst(chain: Interceptor.Chain, request: Request): Response {
val cacheResponse = CacheManager.get(request.url.toString())
if (cacheResponse != null) {
return cacheResponse
}
return chain.proceed(request).also {
CacheManager.put(request.url.toString(), it)
}
}
private fun networkFirst(chain: Interceptor.Chain, request: Request): Response {
try {
val response = chain.proceed(request)
CacheManager.put(request.url.toString(), response)
return response
} catch (e: IOException) {
val cached = CacheManager.get(request.url.toString())
if (cached != null) return cached
throw e
}
}
private fun networkThenCache(chain: Interceptor.Chain, request: Request): Response {
try {
val response = chain.proceed(request)
CacheManager.put(request.url.toString(), response)
return response
} catch (e: IOException) {
val cached = CacheManager.get(request.url.toString())
if (cached != null) return cached
throw e
}
}
private fun cacheThenNetwork(chain: Interceptor.Chain, request: Request): Response {
val cached = CacheManager.get(request.url.toString())
if (cached != null) {
// 异步更新缓存
GlobalScope.launch(Dispatchers.IO) {
try {
val response = chain.proceed(request)
CacheManager.put(request.url.toString(), response)
} catch (_: IOException) {}
}
return cached
}
// 无缓存时直接走网络
val response = chain.proceed(request)
CacheManager.put(request.url.toString(), response)
return response
}
}
提示:实际项目推荐使用
Retrofit 的注解(如 @Headers("X-Cache-Strategy: cache_first"))来声明策略,或通过动态拦截器根据 URL 匹配规则自动选择策略。
调试方法
- 在拦截器中添加日志,打印
请求URL、策略值和是否命中缓存。 - 使用 OkHttp 的
addNetworkInterceptor与addInterceptor分别观察网络层和缓存层的执行日志。 - 测试离线场景:Android 模拟器中关闭网络,观察策略降级行为。
常见错误
⚠️ 错误1:缓存与网络数据混淆 — 未区分缓存键的唯一性(如未加上请求参数),导致不同请求读取到同一缓存。
⚠️ 错误2:先缓存后网络时未处理并发 — 异步网络回写缓存时未加锁,多次快速请求可能导致缓存覆盖或崩溃。
⚠️ 错误3:网络优先策略中无超时处理 — 离线时
chain.proceed() 会抛出异常但等待时间过长,建议设置 OkHttp 的 connectTimeout 和 readTimeout。
性能与稳定性优化
- 缓存大小与淘汰策略:使用
LruCache或DiskLruCache限制内存/磁盘占用,避免 OOM。 - 网络请求取消:在
cache_then_network策略中,当 Activity/Fragment 销毁时取消异步网络请求,防止更新已销毁页面的缓存。 - 缓存有效期:为不同策略设置不同的过期时间(例如缓存优先策略的 TTL 较短,网络优先的缓存 TTL 较长备用)。
- 避免死锁:确保缓存读取与网络请求不在同一线程阻塞,使用
Dispatchers.IO或自定义线程池。
总结:四种策略各有适用场景,实际项目中可组合使用。例如首页使用 先缓存后网络 实现秒开,详情页使用 网络优先 保证最新,配置信息使用 缓存优先 提升性能。理解其原理后,可灵活定制约束条件(如弱网下自动切换为缓存优先)来增强应用的离线能力。