Appearance
网络缓存:Android HTTP 缓存完整指南
目录
1. 网络缓存概述
1.1 为什么要使用缓存
缓存的核心价值:
- 减少网络请求次数
- 降低服务器压力
- 提高响应速度
- 节省用户流量
- 支持离线访问1.2 缓存层级
缓存层级结构:
┌─────────────────────────────────────┐
│ 应用层缓存 │ ← 数据库、文件、内存
├─────────────────────────────────────┤
│ 网络层缓存 │ ← OkHttp Cache
├─────────────────────────────────────┤
│ HTTP 协议缓存 │ ← 响应头控制
├─────────────────────────────────────┤
│ CDN 缓存 │ ← 内容分发网络
└─────────────────────────────────────┘1.3 缓存策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 无缓存 | 数据最新 | 每次都请求 | 实时性要求高 |
| 强制缓存 | 速度快,无网络请求 | 可能数据过时 | 静态资源 |
| 协商缓存 | 平衡新旧 | 需要服务器交互 | 动态内容 |
| 多级缓存 | 性能最优 | 实现复杂 | 大部分场景 |
2. HTTP 缓存策略
2.1 强制缓存(强缓存)
强制缓存通过响应头控制,完全跳过服务器请求:
Cache-Control: max-age=3600
Expires: Wed, 21 Oct 2024 07:28:00 GMTCache-Control 指令
kotlin
class CacheControlExample {
// max-age: 资源缓存的秒数
"Cache-Control: max-age=3600" // 缓存 1 小时
// s-maxage: 代理服务器的缓存时间
"Cache-Control: s-maxage=7200" // 代理缓存 2 小时
// private: 只能被浏览器缓存,代理不能缓存
"Cache-Control: private"
// public: 任何地方都可以缓存
"Cache-Control: public"
// no-cache: 必须先验证才能使用缓存
"Cache-Control: no-cache"
// no-store: 完全不缓存
"Cache-Control: no-store"
// must-revalidate: 过期后必须验证
"Cache-Control: must-revalidate"
// immutable: 资源不会改变
"Cache-Control: immutable"
}Expires 响应头
Expires: Wed, 21 Oct 2024 07:28:00 GMT
说明:
- 指定资源过期的具体时间点
- 过时技术,优先级低于 Cache-Control
- 依赖客户端时间,可能不准确2.2 协商缓存(弱缓存)
协商缓存通过验证缓存是否有效:
Etag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Tue, 10 Oct 2015 07:28:00 GMT
请求头:
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-Modified-Since: Tue, 10 Oct 2015 07:28:00 GMTETag 验证
kotlin
// 服务器返回 ETag
Response:
ETag: "abc123"
Cache-Control: max-age=0, must-revalidate
// 客户端再次请求携带 If-None-Match
Request:
If-None-Match: "abc123"
// 服务器比较 ETag
// 如果相同返回 304 Not Modified
// 如果不同返回 200 和完整资源Last-Modified 验证
kotlin
// 服务器返回最后修改时间
Response:
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
Cache-Control: max-age=0
// 客户端再次请求
Request:
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
// 服务器比较时间
// 如果未修改返回 304
// 如果已修改返回 2002.3 缓存优先级
缓存验证优先级:
1. Cache-Control (最高)
2. ETag
3. Last-Modified
4. Expires (最低)
304 响应返回情况:
- ETag 匹配 → 304
- Last-Modified 未变化 → 304
- 其他 → 2002.4 HTTP 缓存流程
请求流程:
┌──────────────┐
│ 发起请求 │
└──────┬───────┘
│
v
┌──────────────┐
│ 检查缓存 │
└──────┬───────┘
│
├─────────────────────┐
│ │
v v
缓存有效? 缓存无效?
│ │
v v
┌──────────────┐ ┌──────────────┐
│ 使用缓存 │ │ 请求服务器 │
└──────────────┘ └──────┬───────┘
│
v
┌──────────────┐
│ 更新缓存 │
└──────────────┘3. OkHttp 缓存实现
3.1 基础配置
kotlin
class OkHttpCacheManager(context: Context) {
// 创建缓存目录
private val cacheDir = File(context.cacheDir, "http_cache")
// 创建缓存(100MB)
private val cache = Cache(cacheDir, 100 * 1024 * 1024)
// 配置 OkHttpClient
private val client = OkHttpClient.Builder()
.cache(cache)
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build()
// 获取缓存统计
fun getCacheStats(): CacheStats {
return cache.stats()
}
// 清理缓存
fun clearCache() {
cache.evictAll()
}
// 关闭缓存
fun closeCache() {
cache.close()
}
}3.2 配置缓存策略
kotlin
class CacheStrategyExample {
private val client = OkHttpClient.Builder()
.cache(Cache(cacheDir, 100 * 1024 * 1024))
.build()
// 1. 强制使用缓存(离线模式)
fun forceCacheRequest() {
val request = Request.Builder()
.url("https://example.com/data")
.cacheControl(CacheControl.FORCE_CACHE)
.build()
client.newCall(request).execute()
}
// 2. 不使用缓存(强制刷新)
fun noCacheRequest() {
val request = Request.Builder()
.url("https://example.com/data")
.cacheControl(CacheControl.FORCE_NETWORK)
.build()
client.newCall(request).execute()
}
// 3. 先尝试缓存,失败则使用网络
fun cacheFirstRequest() {
val cacheControl = CacheControl.Builder()
.maxStale(30, TimeUnit.DAYS) // 允许使用过期 30 天的缓存
.build()
val request = Request.Builder()
.url("https://example.com/data")
.cacheControl(cacheControl)
.build()
client.newCall(request).execute()
}
// 4. 先使用网络,失败则使用缓存
fun networkFirstRequest() {
val request = Request.Builder()
.url("https://example.com/data")
.cacheControl(CacheControl.Builder()
.maxAge(0) // 不使用强缓存
.build())
.build()
client.newCall(request).execute()
}
}3.3 自定义缓存策略
kotlin
class CustomCacheInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
// 根据响应类型设置缓存策略
return when (response.code) {
in 200..299 -> {
// 成功响应,根据资源类型设置缓存
val url = request.url.toString()
if (url.endsWith(".css") || url.endsWith(".js") ||
url.endsWith(".png") || url.endsWith(".jpg")) {
// 静态资源缓存 7 天
response.toBuilder()
.header("Cache-Control", "max-age=604800")
.build()
} else if (url.contains("api")) {
// API 响应不缓存或使用协商缓存
response.toBuilder()
.header("Cache-Control", "no-cache")
.build()
} else {
response
}
}
304 -> {
// 协商缓存命中
response
}
else -> {
// 其他状态码不缓存
response.toBuilder()
.header("Cache-Control", "no-store")
.build()
}
}
}
}
// 使用自定义拦截器
val client = OkHttpClient.Builder()
.addNetworkInterceptor(CustomCacheInterceptor())
.cache(Cache(cacheDir, 100 * 1024 * 1024))
.build()3.4 拦截器实现缓存逻辑
kotlin
class OfflineFirstInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
// 检查网络连接
val isOnline = isConnected()
// 离线模式:强制使用缓存
if (!isOnline) {
val offlineRequest = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build()
return chain.proceed(offlineRequest)
}
// 在线模式:先网络后缓存
val onlineRequest = request.newBuilder()
.cacheControl(CacheControl.Builder()
.maxStale(Integer.MAX_VALUE) // 允许过期缓存
.build())
.build()
return chain.proceed(onlineRequest)
}
private fun isConnected(): Boolean {
val connectivityManager =
ContextCompat.getSystemService(context, ConnectivityManager::class.java)
val network = connectivityManager?.activeNetwork
return network != null
}
}3.5 响应缓存控制
kotlin
class ResponseCacheControlInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
return response.newBuilder()
.removeHeader("Pragma")
.removeHeader("Expires")
.removeHeader("Cache-Control")
.header("Cache-Control", getCacheControl(response))
.build()
}
private fun getCacheControl(response: Response): String {
val url = response.request.url.toString()
return when {
url.endsWith(Regex("\\.(css|js|png|jpg|jpeg|gif|svg|woff2?)$")) -> {
"public, max-age=31536000, immutable" // 1 年
}
url.contains("/api/v1/") -> {
"private, max-age=300" // 5 分钟
}
url.contains("/api/v2/") -> {
"private, max-age=60" // 1 分钟
}
else -> {
"no-cache"
}
}
}
}4. 多级缓存架构
4.1 三级缓存设计
kotlin
class MultiLevelCache {
// 一级缓存:内存(最快)
private val memoryCache = object : LruCache<String, Any>(10 * 1024 * 1024) {
override fun sizeOf(key: String, value: Any): Int {
return value.toString().length
}
}
// 二级缓存:磁盘(OkHttp Cache)
private val diskCache: Cache
// 三级缓存:数据库(持久化)
private val dbCache = SQLiteOpenHelper(...)
init {
diskCache = Cache(File(context.cacheDir, "http_cache"), 50 * 1024 * 1024)
}
// 读取数据(从快到慢)
suspend fun getData(key: String): Data? {
// 1. 检查内存缓存
var data = memoryCache.get(key) as? Data
if (data != null) {
Log.d("Cache", "Hit memory cache: $key")
return data
}
// 2. 检查磁盘缓存
data = readFromDiskCache(key)
if (data != null) {
Log.d("Cache", "Hit disk cache: $key")
memoryCache.put(key, data) // 回填内存
return data
}
// 3. 检查数据库缓存
data = readFromDbCache(key)
if (data != null) {
Log.d("Cache", "Hit db cache: $key")
memoryCache.put(key, data)
writeToDiskCache(key, data)
return data
}
// 4. 网络请求
data = fetchFromNetwork(key)
if (data != null) {
writeToAllCaches(key, data)
}
return data
}
private fun writeToAllCaches(key: String, data: Data) {
memoryCache.put(key, data)
writeToDiskCache(key, data)
writeToDbCache(key, data)
}
// 写入数据(同步到所有缓存)
suspend fun putData(key: String, data: Data) {
writeToAllCaches(key, data)
}
// 清除缓存
fun clearCache(level: CacheLevel) {
when (level) {
CacheLevel.MEMORY -> memoryCache.evictAll()
CacheLevel.DISK -> diskCache.evictAll()
CacheLevel.DB -> clearDbCache()
CacheLevel.ALL -> {
memoryCache.evictAll()
diskCache.evictAll()
clearDbCache()
}
}
}
enum class CacheLevel {
MEMORY, DISK, DB, ALL
}
}4.2 缓存管理器
kotlin
class CacheManager(context: Context) {
private val memoryCache = MemoryCache()
private val diskCache = DiskCache(context)
private val dbCache = DatabaseCache(context)
// 配置
data class CacheConfig(
val memorySize: Long = 10 * 1024 * 1024, // 10MB
val diskSize: Long = 50 * 1024 * 1024, // 50MB
val dbEnabled: Boolean = true,
val ttl: Long = 24 * 60 * 60 * 1000 // 24 小时
)
// 获取数据
suspend fun <T> get(key: String, loader: suspend () -> T?): T? {
// 1. 内存缓存
var data = memoryCache.get<T>(key)
if (data != null && !isExpired(key)) {
return data
}
// 2. 磁盘缓存
data = diskCache.get<T>(key)
if (data != null && !isExpired(key)) {
memoryCache.put(key, data)
return data
}
// 3. 数据库缓存
data = dbCache.get<T>(key)
if (data != null && !isExpired(key)) {
memoryCache.put(key, data)
diskCache.put(key, data)
return data
}
// 4. 网络加载
data = loader()
if (data != null) {
saveToAllCaches(key, data)
}
return data
}
private fun <T> saveToAllCaches(key: String, data: T) {
memoryCache.put(key, data)
diskCache.put(key, data)
if (config.dbEnabled) {
dbCache.put(key, data)
}
}
private fun isExpired(key: String): Boolean {
val timestamp = getTimestamp(key)
return System.currentTimeMillis() - timestamp > config.ttl
}
}4.3 缓存失效策略
kotlin
class CacheInvalidationStrategy {
// 1. TTL(Time To Live)
private val expirationMap = ConcurrentHashMap<String, Long>()
fun setExpiration(key: String, ttl: Long) {
expirationMap[key] = System.currentTimeMillis() + ttl
}
fun isExpired(key: String): Boolean {
return expirationMap[key] ?: 0 < System.currentTimeMillis()
}
// 2. LRU(Least Recently Used)
private val lruCache = object : LruCache<String, Any>(100) {
override fun sizeOf(key: String, value: Any): Int = 1
}
// 3. LFU(Least Frequently Used)
private val frequencyMap = ConcurrentHashMap<String, AtomicInteger>()
fun access(key: String) {
frequencyMap.computeIfAbsent(key) { AtomicInteger(0) }
.incrementAndGet()
}
fun getLeastFrequent(): String? {
return frequencyMap.minByOrNull { it.value }?.key
}
// 4. 手动失效
fun invalidate(key: String) {
memoryCache.remove(key)
diskCache.remove(key)
dbCache.remove(key)
}
// 5. 批量失效
fun invalidateAll(prefix: String) {
keys.filter { it.startsWith(prefix) }.forEach {
invalidate(it)
}
}
}5. 图片缓存
5.1 Glide 缓存
kotlin
class GlideCacheManager {
// 配置 Glide
class GlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
builder.setMemoryCache(MemoryCache())
builder.setDiskCache(DiskCacheFactory())
}
override fun isManifestPlaceholder(): Boolean = false
}
// 加载图片(自动缓存)
fun loadImage(context: Context, url: String, imageView: ImageView) {
Glide.with(context)
.load(url)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL) // 缓存原始和转换后
.memoryCacheStrategy(MemoryCacheStrategy.ALL)
.into(imageView)
}
// 配置缓存大小
fun configureCacheSize(context: Context) {
val glide = Glide.with(context)
// 内存缓存:1/6 的可用内存
val maxMemory = Runtime.getRuntime().maxMemory() / 1024
val memoryCacheSize = maxMemory / 6
glide.memoryCache = LruResourceCache(memoryCacheSize)
}
// 清理缓存
fun clearCache(context: Context) {
Glide.with(context).apply {
clearMemory()
// 清理磁盘缓存
context.externalCacheDir?.listFiles {
it.name.startsWith("glide")
}?.forEach {
it.deleteRecursively()
}
}
}
// 获取缓存统计
fun getCacheStats(context: Context): CacheStats {
val glide = Glide.with(context)
return CacheStats(
memoryCache.size.toLong(),
memoryCache.size.toLong() * 100, // 估算大小
diskCache.size.toLong(),
diskCache.size.toLong() * 100
)
}
}5.2 Picasso 缓存
kotlin
class PicassoCacheManager {
// 配置 Picasso
fun configurePicasso(context: Context) {
val picasso = Picasso.Builder(context)
.memoryCache(MemoryCache())
.diskCache(DiskLruCache(context, 100 * 1024 * 1024)) // 100MB
.build()
Picasso.setSingletonInstance(picasso)
}
// 加载图片
fun loadImage(context: Context, url: String, imageView: ImageView) {
Picasso.get()
.load(url)
.memoryPolicy(MemoryPolicy.NO_STORE, MemoryPolicy.CACHE)
.into(imageView)
}
// 清理缓存
fun clearCache(context: Context) {
Picasso.get().lruCache().clear()
Picasso.get().diskLruCache().delete()
}
}5.3 Coil 缓存(Kotlin 优先)
kotlin
class CoilCacheManager {
// 配置 Coil
fun configureCoil(context: Context) {
ImageLoader.Builder(context)
.memoryCache {
MemoryCache.Builder(context)
.maxSizePercent(0.25) // 25% 可用内存
.build()
}
.diskCache {
DiskCache.Builder(context)
.maxSizeBytes(100 * 1024 * 1024) // 100MB
.build()
}
.build()
}
// Compose 中加载
@Composable
fun ImageLoader(
data: String,
contentDescription: String? = null
) {
Image(
painter = rememberAsyncImagePainter(data),
contentDescription = contentDescription,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
// 清理缓存
fun clearCache(context: Context) {
ImageLoader(context).memoryCache.clear()
ImageLoader(context).diskCache.clear()
}
}6. 数据库缓存
6.1 Room 缓存
kotlin
class RoomCacheManager(context: Context) {
private val database = Room.databaseBuilder(
context,
AppDatabase::class.java,
"cache_database"
)
.build()
// 定义缓存 DAO
@Dao
interface CacheDao {
@Query("SELECT * FROM cached_data WHERE key = :key")
suspend fun getData(key: String): CachedData?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveData(data: CachedData)
@Query("DELETE FROM cached_data WHERE key = :key")
suspend fun deleteData(key: String)
@Query("SELECT * FROM cached_data WHERE expire_time < :currentTime")
suspend fun getExpiredData(currentTime: Long): List<CachedData>
}
// 缓存实体
@Entity(tableName = "cached_data")
data class CachedData(
@PrimaryKey val key: String,
val value: String,
val type: String,
val expireTime: Long,
val createdAt: Long = System.currentTimeMillis()
)
// 保存缓存
suspend fun saveCache(key: String, data: Any, ttl: Long = 24 * 60 * 60 * 1000) {
val cachedData = CachedData(
key = key,
value = Gson().toJson(data),
type = data::class.java.simpleName,
expireTime = System.currentTimeMillis() + ttl
)
database.cacheDao().saveData(cachedData)
}
// 读取缓存
suspend fun <T> getCache(key: String, type: Class<T>): T? {
val cached = database.cacheDao().getData(key)
return if (cached != null && cached.expireTime > System.currentTimeMillis()) {
Gson().fromJson(cached.value, type)
} else {
// 清理过期缓存
database.cacheDao().deleteData(key)
null
}
}
// 清理过期缓存
suspend fun clearExpiredCache() {
val expired = database.cacheDao().getExpiredData(System.currentTimeMillis())
expired.forEach {
database.cacheDao().deleteData(it.key)
}
}
}6.2 网络 + 数据库缓存策略
kotlin
class NetworkWithDbCache(private val api: ApiService, private val db: RoomDatabase) {
// 优先数据库,失败则网络
suspend fun <T> getDataWithCache(
key: String,
networkCall: suspend () -> T,
ttl: Long = 24 * 60 * 60 * 1000
): T? {
// 1. 尝试从数据库读取
var data = db.getCache<T>(key, networkCall::class.java)
if (data != null) {
Log.d("Cache", "Hit database cache: $key")
return data
}
// 2. 网络请求
try {
data = networkCall()
if (data != null) {
db.saveCache(key, data, ttl)
}
} catch (e: Exception) {
Log.e("Cache", "Network failed, returning null", e)
}
return data
}
// 先网络,失败则数据库
suspend fun <T> getNetworkFirst(
key: String,
networkCall: suspend () -> T,
useCacheOnError: Boolean = true
): T? {
try {
val data = networkCall()
if (data != null) {
db.saveCache(key, data)
}
return data
} catch (e: Exception) {
if (useCacheOnError) {
return db.getCache<T>(key, networkCall::class.java)
}
throw e
}
}
// 刷新缓存
suspend fun <T> refreshCache(
key: String,
networkCall: suspend () -> T
): T? {
val data = networkCall()
if (data != null) {
db.saveCache(key, data)
}
return data
}
}7. 最佳实践
7.1 架构设计
kotlin
// Repository 模式
class DataRepository(
private val apiService: ApiService,
private val cacheManager: CacheManager
) {
suspend fun getData(key: String): Data? {
return cacheManager.get(key) {
apiService.fetch(key)
}
}
}
// UseCase 模式
class GetDataUseCase(
private val repository: DataRepository
) {
suspend operator fun invoke(key: String): Data? {
return repository.getData(key)
}
}7.2 监控和统计
kotlin
class CacheMonitor {
private val cacheHitCounter = AtomicInteger(0)
private val cacheMissCounter = AtomicInteger(0)
fun recordHit() {
cacheHitCounter.incrementAndGet()
}
fun recordMiss() {
cacheMissCounter.incrementAndGet()
}
fun getHitRate(): Double {
val total = cacheHitCounter.get() + cacheMissCounter.get()
return if (total > 0) {
cacheHitCounter.get().toDouble() / total * 100
} else {
0.0
}
}
fun getStats(): CacheStats {
return CacheStats(
hits = cacheHitCounter.get(),
misses = cacheMissCounter.get(),
hitRate = getHitRate()
)
}
}7.3 缓存预热
kotlin
class CacheWarmer {
suspend fun warmUpEssentialData() {
// 应用启动时预加载重要数据
listOf("config", "user_profile", "home_banner").forEach { key ->
viewModelScope.launch {
cacheManager.get(key) {
apiService.fetch(key)
}
}
}
}
}8. 面试考点
考点 1:HTTP 缓存响应头
问题: 常见的 HTTP 缓存响应头有哪些?
答案:
- Cache-Control: 控制缓存行为(max-age, no-cache, no-store 等)
- Expires: 缓存过期时间
- ETag: 资源标识符
- Last-Modified: 资源最后修改时间
考点 2:304 响应
问题: 什么情况下会返回 304?
答案:
- 客户端携带 If-None-Match 或 If-Modified-Since
- 服务器验证资源未修改
- 返回 304 Not Modified,客户端使用本地缓存
考点 3:OkHttp 缓存配置
问题: 如何配置 OkHttp 缓存?
答案:
kotlin
val cache = Cache(cacheDir, 100 * 1024 * 1024)
val client = OkHttpClient.Builder()
.cache(cache)
.build()考点 4:多级缓存
问题: 什么是多级缓存?
答案:
- 一级:内存缓存(LruCache)
- 二级:磁盘缓存(OkHttp Cache)
- 三级:数据库缓存(Room)
考点 5:缓存策略
问题: 常见的缓存策略有哪些?
答案:
- Cache-First: 优先使用缓存
- Network-First: 优先使用网络
- Stale-While-Revalidate: 返回缓存同时更新
- Offline-First: 离线优先
考点 6:图片缓存
问题: Glide 如何配置缓存?
答案:
kotlin
.diskCacheStrategy(DiskCacheStrategy.ALL)
.memoryCacheStrategy(MemoryCacheStrategy.ALL)考点 7:缓存失效
问题: 如何实现缓存失效?
答案:
- TTL(过期时间)
- LRU(最近最少使用)
- 手动失效
- 版本控制
考点 8:协商缓存 vs 强缓存
问题: 协商缓存和强缓存的区别?
答案:
- 强缓存:直接使用本地缓存,不请求服务器
- 协商缓存:请求服务器验证缓存是否有效
考点 9:离线模式
问题: 如何实现离线访问?
答案:
- 强制使用缓存(CacheControl.FORCE_CACHE)
- 检查网络连接状态
- 数据本地持久化
考点 10:缓存监控
问题: 如何监控缓存效果?
答案:
- 记录命中率
- 监控缓存大小
- 分析缓存分布
总结
网络缓存是提升应用性能的关键:
✅ HTTP 缓存策略控制
✅ OkHttp 磁盘缓存
✅ 多级缓存架构
✅ 图片缓存优化
✅ 数据库持久化缓存
使用建议:
- 静态资源使用长缓存
- API 响应使用协商缓存
- 重要数据使用数据库缓存
- 图片使用专门的图片库
学习路径: HTTP 缓存 → OkHttp 缓存 → 多级缓存 → 完整缓存方案
本文档涵盖网络缓存的核心知识点,建议配合实际项目练习以加深理解。