跳到主要内容

网络缓存策略

问题

Android 中如何设计网络缓存策略,OkHttp 的缓存机制是怎样的?

答案

缓存层次模型

OkHttp HTTP 缓存

配置缓存

val client = OkHttpClient.Builder()
.cache(Cache(
directory = File(context.cacheDir, "http_cache"),
maxSize = 10L * 1024 * 1024 // 10MB
))
.build()

缓存控制 Header

Header说明示例
Cache-Control: max-age缓存有效期(秒)max-age=3600
Cache-Control: no-cache需要服务器验证客户端设置
Cache-Control: no-store不缓存敏感数据
Cache-Control: only-if-cached仅使用缓存离线模式
ETag资源指纹"abc123"
Last-Modified最后修改时间Wed, 01 Jan 2025 00:00:00 GMT

缓存判断流程

客户端缓存控制

// 1. 强制使用网络(跳过缓存)
val request = Request.Builder()
.url("https://api.example.com/data")
.cacheControl(CacheControl.FORCE_NETWORK)
.build()

// 2. 强制使用缓存(不发网络请求)
val request = Request.Builder()
.url("https://api.example.com/data")
.cacheControl(CacheControl.FORCE_CACHE)
.build()

// 3. 自定义缓存策略
val cacheControl = CacheControl.Builder()
.maxAge(5, TimeUnit.MINUTES) // 缓存 5 分钟内有效
.maxStale(1, TimeUnit.HOURS) // 过期 1 小时内仍可用
.build()

val request = Request.Builder()
.url("https://api.example.com/data")
.cacheControl(cacheControl)
.build()

离线优先缓存策略

通过拦截器实现「有网用网络,无网用缓存」:

// Application Interceptor:无网络时强制使用缓存
class OfflineInterceptor(private val context: Context) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (!isNetworkAvailable(context)) {
val cacheControl = CacheControl.Builder()
.maxStale(7, TimeUnit.DAYS) // 允许 7 天旧缓存
.build()
request = request.newBuilder()
.cacheControl(cacheControl)
.build()
}
return chain.proceed(request)
}
}

// Network Interceptor:为响应添加缓存头(当服务端未设置时)
class CacheInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
val cacheControl = CacheControl.Builder()
.maxAge(5, TimeUnit.MINUTES)
.build()
return response.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", cacheControl.toString())
.build()
}
}

// 注册
val client = OkHttpClient.Builder()
.cache(Cache(cacheDir, 10 * 1024 * 1024))
.addInterceptor(OfflineInterceptor(context))
.addNetworkInterceptor(CacheInterceptor())
.build()

内存缓存层

对于频繁访问的数据,增加内存缓存层可以避免磁盘 IO:

class MemoryCacheRepository(
private val api: ApiService
) {
// LruCache 内存缓存
private val cache = LruCache<String, CacheEntry>(50)

data class CacheEntry(
val data: Any,
val timestamp: Long = System.currentTimeMillis()
)

suspend fun <T> getCached(
key: String,
maxAge: Long = 5 * 60 * 1000, // 5 分钟
fetcher: suspend () -> T
): T {
// 1. 检查内存缓存
val cached = cache.get(key)
if (cached != null && System.currentTimeMillis() - cached.timestamp < maxAge) {
@Suppress("UNCHECKED_CAST")
return cached.data as T
}

// 2. 网络请求
val data = fetcher()

// 3. 写入缓存
cache.put(key, CacheEntry(data))

return data
}
}
OkHttp 缓存 vs 自定义缓存
  • OkHttp Cache:适用于 GET 请求的 HTTP 响应缓存,遵循 HTTP 缓存语义
  • 自定义缓存:适用于业务层面的数据缓存,可以缓存 POST 请求结果、支持更灵活的失效策略

常见面试问题

Q1: OkHttp 缓存支持哪些请求方法?

答案

OkHttp 默认只缓存 GET 请求的响应。POST、PUT、DELETE 等写操作不会被缓存,这符合 HTTP 规范 —— 只有安全且幂等的请求才应该被缓存。如果需要缓存 POST 请求的结果,应在业务层自行实现。

Q2: 强缓存和协商缓存的区别?

答案

  • 强缓存:通过 Cache-Control: max-ageExpires 控制。缓存未过期时,客户端直接使用缓存,不发送任何网络请求,状态码仍为 200
  • 协商缓存:缓存过期后,客户端发送条件请求(携带 If-None-Match: ETagIf-Modified-Since),服务端判断资源未变化返回 304,客户端继续使用缓存;若资源已变化返回 200 + 新数据

OkHttp 的 CacheInterceptor 完整实现了这两种缓存策略。

Q3: 如何清除 OkHttp 缓存?

答案

// 清除所有缓存
client.cache?.evictAll()

// 清除指定 URL 的缓存
val url = "https://api.example.com/data".toHttpUrl()
val urlIterator = client.cache?.urls()
while (urlIterator?.hasNext() == true) {
if (urlIterator.next() == url.toString()) {
urlIterator.remove()
}
}

相关链接