一、概念
当某些网络访问获取的内容不是每次都变的,而是短时间不变的(每月榜单)或长时间不变的(歌曲的信息),每次访问都联网获取的话,可能响应慢,不支持离线浏览,浪费用户和公司的流量费用和带宽占用。因此将已请求的内容存储在本地(内存或磁盘)用于后续复用。
|--------|-----------------------------------------------------------------------|
| 服务端侧缓存 | 当客户端重复访问一张图片地址时,服务器会判断这个请求有没有缓存,有的话就直接返回掉这个请求,而不是到达真正的服务器地址,依次减轻运算压力。 |
| 客户端侧缓存 | 客户端将服务器返回的数据缓存在本地,再次访问同一地址的时候,客户端会检测本地是否有缓存,如果有且未过期就直接使用缓存内容。 |
1.1 客户端缓存方式
内存缓存详见:LruCache。磁盘缓存详见:DiskLruCache。
|--------|-----------------------------------------------------------|---------------------------|
| 内存缓存 | 读写速度最快,但容量有限,进程销毁后失效。 | 频繁访问的小数据(首页列表、用户信息)。 |
| 磁盘缓存 | 容量大,读写速度较慢,进程销毁后仍存在。 | 不频繁变动的大数据(图片、离线数据包)。 |
| HTTP缓存 | 基于HTTP协议标准(如 Cache-Control 头),由网络库(OkHttp)自动管理。通常也是存在磁盘上。 | 符合HTTP缓存规则的接口响应(GET请求结果)。 |
二、 非 HTTP 缓存
非 HTTP 缓存适用于复杂数据结构、非GET请求结果,需设计合理的 key 和过期策略。key需唯一标识缓存数据,确保"相同请求对应相同key,不同请求对应不同key"。需根据数据更新频率和重要性设置过期规则。
2.1 key命名
key需唯一标识缓存数据,确保"相同请求对应相同key,不同请求对应不同key"。
|--------|------------------------------------------------|
| 基于请求参数 | 对于接口请求,用URL + 方法 + 参数组合生成key(避免因参数不同导致缓存冲突)。 |
| 哈希优化 | 若key过长(如URL+参数复杂),可通过MD5/SHA-1哈希缩短key(避免存储冗余)。 |
2.2 过期策略
|---------------|------------------------------------------------------------------------------|
| 时间戳过期 | 存储缓存时记录时间戳,读取时判断是否超过有效期(如1小时),适用于大多数场景。 |
| 版本号过期 | 为数据设置版本号(如服务器返回verson=1.0),缓存时存储版本号,当服务器版本更新时,本地缓存失效,适用于数据更新有明确标识的场景(如配置文件)。 |
| LRU(最近最少使用)淘汰 | 当缓存容量达到上限时,淘汰最近最少使用的缓存(内存常用)。 |
2.3 离线加载策略
无论网络状态如何,先尝试加载本地缓存展示,再在有网络时请求最新数据并更新缓存和UI。
2.4 缓存与网络数据同步策略
|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| 以网络数据为准 | 在线时请求网络,无论缓存是否存在,均用网络数据更新缓存和UI确保数据最新,适用于实时性要求高的场景(如订单状态)。 |
| 增量更新 | 网络请求只返回变化的数据(如新增/修改的条目),本地缓存合并增量数据(减少传输量),例如: 服务器返回{ "added": [...], "updated": [...], "deletedIds": [...] } ,客户端合并到本地缓存列表:新增条目添加、更新条目替换、删除条目移除。 |
| 冲突处理 | 若缓存数据被本地修改(如用户编辑未提交),网络请求返回新数据时,需提示用户选择保留本地修改或覆盖(如"有新数据,是否更新?")。 |
| 后台同步 | 对于非紧急数据(如用户历史记录),可在App启动或网络恢复时,后台静默请求网络并更新缓存,不干扰用户操作。 |
2.5 大数据缓存优化
2.5.1 分页加载
将大列表按分页参数(如page=1&size=20)拆分缓存,避免一次性缓存整个列表导致的内存/磁盘占用过高。 缓存key包含分页参数(如news_list_page1_size20),只缓存用户已浏览的页(如当前页、上一页、下一页),淘汰更早的页(LRU策略)。
Kotlin
// 缓存第1页数据 newsCache.saveCache("news_page1", page1Data)
// 缓存第2页数据 newsCache.saveCache("news_page2", page2Data)
// 加载第3页时,若缓存满,淘汰第1页(LRU)
2.5.2 增量更新
只缓存变化的数据,而非全量数据,减少重复存储。
|----|------------------------------------------------------------------------------------|
| 列表 | 服务器返回数据时携带"版本号"或"修改时间",客户端仅缓存版本号大于本地数据的。 |
| 图片 | 缓存不同分辨率的图片(缩略图、中图、原图),根据展示场景加载(如列表用缩略图,详情用原图),对于渐进式图片(如WebP),可先缓存低质量版本,再异步缓存高质量版本。 |
Kotlin
//服务器返回增量数据,客户端合并增量到本地缓存,无需重新缓存整个列表。
{
"version": 5,
"added": [{"id": 101, "content": "..."}], // 新增条目
"updated": [{"id": 10, "content": "..."}], // 更新条目
"deletedIds": [5, 8] // 删除条目ID
}
2.5.3 压缩存储
|----|--------------------------------------------------------------------------------|
| 列表 | 用高效序列化方式(如Protocol Buffers替代JSON),减少磁盘占用(通常比JSON小30%-50%)。 |
| 图片 | 存储压缩格式(如WebP,比JPEG小25%-35%),按显示尺寸压缩(如ImageView大小为200x200,缓存200x200的图片,而非原图尺寸)。 |
2.5.4 预热与预加载
|-----|-----------------------------------------------|
| 预热 | App启动时,提前缓存高频访问的基础数据(如首页Banner、分类列表)。 |
| 预加载 | 用户浏览当前页时,后台预加载下一页数据(如滑动列表到80%时,预加载下一页),提升流畅度。 |
2.5.5 内存缓存限制
对大体积数据(如图片),内存缓存设置严格的容量上限(如最多缓存20张图片),避免OOM。优先缓存最近访问的图片(LRU策略),后台自动释放内存(如onTrimMemory时清理部分缓存)。
2.5.6 异步写入
大体积数据的磁盘写入(如图片)放在后台线程,避免阻塞UI线程。
三、HTTP缓存

OkHttp 基于 HTTP 协议规范(RFC 7234)通过 Cache 类实现磁盘缓存。当发起GET请求时,会将服务器返回的响应按规则存储到磁盘(默认路径:/data/data/包名/cache/okhttp),后续发起相同请求时会先检查本地缓存。
- 缓存未过期:直接使用缓存,不发起网络请求。
- 缓存已过期:发起"条件请求",携带 If-Modified-Since 或 If-None-Match 头,服务器判断缓存是否有效:
- 有效(返回 304 Not Modified):使用本地缓存。
- 无效(返回 200 OK):更新缓存并使用新响应。
3.1 配置缓存目录和大小
设置好Cache后,同一个地址访问两次打印Log:
- 第一次访问的response.networkResponse( )有内容(来自网络)而response.cacheResponse( ) = null;
- 第二次访问相反,response.cacheResponse( )有内容(来自缓存)而response.networkResponse( ) = null。
Kotlin
//缓存目录
private val cacheFile = File(APP.context.externalCacheDir, "HttpCache")
//缓存大小100mb
private val cacheSize = (100 * 1024 * 1024).toLong()
//创建Cache类
private val cache = Cache(cacheFile, cacheSize)
//配置到OkHttpClient
private val okHttpClient = OkHttpClient.Builder()
.cache(cache) //关联缓存
.build()
3.2 配置缓存策略
缓存策略主要通过HTTP请求头/响应头的Cache-Control字段控制。
|------|-------------------------------------------------|
| 固定配置 | Retrofit 可以对接口方法使用 @Headers 注解配置 Cache-Control。 |
| 灵活配置 | 可根据网络状态、URL类型等进行动态配置。 |
3.2.1 通过请求头全局控制(自行配置Request)
CacheControl 类中提供了两个已经定义好的,通过伴生对象调用,可根据有无网络访问来返回其一。也可以根据需求通过 CacheControl.Builder() 自定义,或是直接配置 header。
|----------------------------|----------|
| CacheControl.FORCE_NETWORK | 只获取网络数据。 |
| CacheControl.FORCE_CACHE | 只读取本地缓存。 |
|-----------------------------------|------------------|
| noStore() | 不使用缓存,也不存储缓存。 |
| onlyIfCached() | 只使用缓存。 |
| noTransform() | 禁止转码。 |
| maxAge(10, TimeUnit.MILLISECONDS) | 超时时间为10ms。 |
| maxStale(10, TimeUnit.SECONDS) | 超时之外的超时时间为10s。 |
| minFresh(10, TimeUnit.SECONDS) | 超时时间为当前时间加上10秒钟。 |
Kotlin
private val cacheRequestIntercept = object : Interceptor{
val cacheControl = CacheControl.Builder()
.maxAge(60, TimeUnit.SECONDS) //缓存缓存有效期60秒
.build()
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
// .header("Cache-Control", "public, max-age=1800") //也可通过header配置
// .cacheControl(CacheControl.FORCE_CACHE) //使用定义好的CacheControl
.cacheControl(cacheControl) //使用自定义的CacheControl
.build()
val response = chain.proceed(request)
return response
}
}
//配置到OkHttpClient
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(cacheRequestIntercept) //添加应用拦截器(走缓存不会调用网络拦截器)
.build()
简写
Kotlin
private val cacheRequestIntercept = Interceptor { chain ->
val cacheControl = CacheControl.Builder()
.maxAge(60, TimeUnit.SECONDS)
.build()
val request = chain.request().newBuilder()
.cacheControl(cacheControl)
.build()
chain.proceed(request)
}
3.2.2 通过响应头全局控制(自行配置Response)
服务器通过响应头的Cache-Control指定缓存过期时间,客户端在过期前直接使用缓存,不向服务器发起请求。也可以通过拦截器自定义来覆盖。
|---------------------|----------------------------------------------|
| max-age=<seconds> | 缓存有效期(秒),从响应生成时间开始计算,例如max-age=3600表示缓存1小时。 |
| public/private | public表示缓存可被共享(如代理服务器),private表示仅客户端可缓存(默认)。 |
| immutable | 缓存数据不会变化,客户端无需验证(适用于静态资源)。 |
Kotlin
private val cacheResponseIntercept = object : Interceptor{
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
//仅针对GET请求设置缓存
val response = if(request.method == "GET") {
chain.proceed(request).newBuilder()
.removeHeader("pragma") //移除干扰缓存的头(如no-cache)
.header("Cache-Control","max-age=60")
.build()
} else {
chain.proceed(request)
}
return response
}
}
//配置到OkHttpClient
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(cacheResponseIntercept) //添加应用拦截器(走缓存不会调用网络拦截器)
.build()
简写
Kotlin
private val cacheResponseIntercept = Interceptor { chain ->
val request = chain.request()
if (request.method == "GET") {
chain.proceed(request).newBuilder()
.removeHeader("pragma")
.header("Cache-Control","max-age=60")
.build()
} else {
chain.proceed(request)
}
}
3.2.3 为单个请求固定控制
Kotlin
//缓存1小时(强制缓存)
//@Headers("Cache-Control: public, max-age=3600")
//禁止缓存(每次都请求最新数据)
//@Headers("Cache-Control: no-store")
//仅离线时使用缓存(在线时必须请求网络)
@Headers("Cache-Control: public, only-if-cached, max-stale=3600")
@GET("user/{userId}")
suspend fun getUserById(@Path("userId"): User
3.2.4 根据条件灵活控制
根据网络状态
Kotlin
private val cacheIntercepter = Interceptor { chain ->
val request = if (NetUtils.NO_NETWORK) {
//无网络,检查30天内的缓存,即使是过期的缓存
chain.request().newBuilder()
.cacheControl(CacheControl.Builder().maxAge(60, TimeUnit.SECONDS).build())
.build()
} else {
//有网络,检查10秒内的缓存
chain.request().newBuilder()
.cacheControl(CacheControl.Builder().maxAge(60, TimeUnit.SECONDS).build())
.build()
}
chain.proceed(request)
}
根据URL类型
Kotlin
private val cacheIntercepter = Interceptor { chain ->
val request = chain.request()
val url = request.url.toString()
if (url.contains("/news/")) {
//新闻列表缓存3分钟
chain.proceed(request).newBuilder()
.header("Cache-Control", "public, max-age=180")
.build()
} else if (url.contains("/user/")){
//用户信息不缓存
chain.proceed(request).newBuilder()
.header("Cache-Control", "no-cache")
.build()
} else {
chain.proceed(request)
}
}