网络缓存策略
问题
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-age或Expires控制。缓存未过期时,客户端直接使用缓存,不发送任何网络请求,状态码仍为 200 - 协商缓存:缓存过期后,客户端发送条件请求(携带
If-None-Match: ETag或If-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()
}
}