ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

网络 - 缓存

网络 - 缓存 一、概念当某些网络访问获取的内容不是每次都变的而是短时间不变的每月榜单或长时间不变的歌曲的信息每次访问都联网获取的话可能响应慢不支持离线浏览浪费用户和公司的流量费用和带宽占用。因此将已请求的内容存储在本地内存或磁盘用于后续复用。服务端侧缓存当客户端重复访问一张图片地址时服务器会判断这个请求有没有缓存有的话就直接返回掉这个请求而不是到达真正的服务器地址依次减轻运算压力。客户端侧缓存客户端将服务器返回的数据缓存在本地再次访问同一地址的时候客户端会检测本地是否有缓存如果有且未过期就直接使用缓存内容。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小时适用于大多数场景。版本号过期为数据设置版本号如服务器返回verson1.0缓存时存储版本号当服务器版本更新时本地缓存失效适用于数据更新有明确标识的场景如配置文件。LRU最近最少使用淘汰当缓存容量达到上限时淘汰最近最少使用的缓存内存常用。2.3 离线加载策略无论网络状态如何先尝试加载本地缓存展示再在有网络时请求最新数据并更新缓存和UI。2.4 缓存与网络数据同步策略以网络数据为准在线时请求网络无论缓存是否存在均用网络数据更新缓存和UI确保数据最新适用于实时性要求高的场景如订单状态。增量更新网络请求只返回变化的数据如新增/修改的条目本地缓存合并增量数据减少传输量例如 服务器返回{ added: [...], updated: [...], deletedIds: [...] } 客户端合并到本地缓存列表新增条目添加、更新条目替换、删除条目移除。冲突处理若缓存数据被本地修改如用户编辑未提交网络请求返回新数据时需提示用户选择保留本地修改或覆盖如“有新数据是否更新”。后台同步对于非紧急数据如用户历史记录可在App启动或网络恢复时后台静默请求网络并更新缓存不干扰用户操作。2.5 大数据缓存优化2.5.1 分页加载将大列表按分页参数如page1size20拆分缓存避免一次性缓存整个列表导致的内存/磁盘占用过高。 缓存key包含分页参数如news_list_page1_size20只缓存用户已浏览的页如当前页、上一页、下一页淘汰更早的页LRU策略。// 缓存第1页数据 newsCache.saveCache(news_page1, page1Data) // 缓存第2页数据 newsCache.saveCache(news_page2, page2Data) // 加载第3页时若缓存满淘汰第1页LRU2.5.2 增量更新只缓存变化的数据而非全量数据减少重复存储。列表服务器返回数据时携带“版本号”或“修改时间”客户端仅缓存版本号大于本地数据的。图片缓存不同分辨率的图片缩略图、中图、原图根据展示场景加载如列表用缩略图详情用原图对于渐进式图片如WebP可先缓存低质量版本再异步缓存高质量版本。//服务器返回增量数据客户端合并增量到本地缓存无需重新缓存整个列表。 { 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 请求有效。拦截器执行顺序应用拦截器 addInterceptor() → OkHttp 内置缓存拦截器 → 网络拦截器 addNetworkInterceptor() → 网络响应回来反向执行网络拦截器 → 缓存拦截器 → 应用拦截器。因此网络拦截器适合修改响应头覆盖服务器的缓存保存策略。应用拦截器适合修改请求头覆盖对缓存的使用策略。响应头中的 Cache-Control代表服务器的规矩。OkHttp 会根据服务器返回的响应按规则存储到磁盘默认路径/data/data/包名/cache/okhttp后续发起相同请求时会先检查本地缓存。请求头中的 Cache-Control代表客户端的需求。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。//缓存目录 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 通过请求头设置缓存读取策略CacheControl 类中提供了两个已经定义好的通过伴生对象调用可根据有无网络访问来返回其一。也可以根据需求通过 CacheControl.Builder() 自定义或是直接手动配置 header。调用已定义好的CacheControl.FORCE_NETWORK只获取网络数据。CacheControl.FORCE_CACHE只读取本地缓存。通过 CacheControl 自定义noStore()不使用缓存也不存储缓存。onlyIfCached()只使用缓存如果缓存没有或过期就直接给我个 504 错误别去联网。用于离线模式。noTransform()禁止转码。maxAge(10, TimeUnit.MILLISECONDS)需要一个 n 秒内的缓存太旧的不要。如果设为 0就相当于强制走网络。maxStale(10, TimeUnit.SECONDS)如果缓存过期了在一定时间内也可以接受。用于网络不佳时增加缓存命中率minFresh(10, TimeUnit.SECONDS)超时时间为当前时间加上10秒钟。手动配置 header.addHeader(Cache-Control, max-age60)不推荐手写。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-age1800) //也可通手写过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()简写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 通过响应头设置缓存保存策略服务器通过响应头的 Cache-Control 指定缓存过期时间客户端在过期前直接使用缓存不向服务器发起请求。也可以通过拦截器自定义来覆盖。max-ageseconds缓存有效期秒从响应生成时间开始计算例如max-age3600表示缓存1小时。public/privatepublic表示缓存可被共享如代理服务器private表示仅客户端可缓存默认。immutable缓存数据不会变化客户端无需验证适用于静态资源。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-age60) .build() } else { chain.proceed(request) } return response } } //配置到OkHttpClient private val okHttpClient OkHttpClient.Builder() //使用网络拦截器只有发生真实网络请求的时候才会执行 //命中本地缓存、直接返回缓存结果时网络拦截器不会跑 .addNetworkInterceptor(cacheResponseIntercept) .build()简写private val cacheResponseIntercept Interceptor { chain - val request chain.request() if (request.method GET) { chain.proceed(request).newBuilder() .removeHeader(pragma) .header(Cache-Control,max-age60) .build() } else { chain.proceed(request) } }3.2.3 为单个请求固定控制Retrofit 提供注解支持会覆盖全局设置。//缓存1小时强制缓存 //Headers(Cache-Control: public, max-age3600) //禁止缓存每次都请求最新数据 //Headers(Cache-Control: no-store) //仅离线时使用缓存在线时必须请求网络 Headers(Cache-Control: public, only-if-cached, max-stale3600) GET(user/{userId}) suspend fun getUserById(Path(userId): User3.2.4 根据条件灵活控制根据网络状态。推荐将请求头和响应头分开写成2个拦截器这样把请求头的拦截器设置到网络拦截器上能避免无网时被调用还会增加无意义的日志打印。//通过响应头设置缓存保存策略 private val cacheSaveInterceptor Interceptor { chain - //无缓存,进行缓存 chain.proceed(chain.request()).newBuilder() .removeHeader(pragma) .addHeader(Cache-Control, max-age60) //对请求进行最大60秒的缓存 .build() } //通过请求头设置缓存读取策略 private val cacheReadInterceptor Interceptor{ chain - val cacheControl if (NetUtils.NO_NETWORK) { //无网络,检查30天内的缓存,即使是过期的缓存 CacheControl.Builder() .onlyIfCached() .maxStale(30, TimeUnit.DAYS) .build() }else { //有网络,检查10秒内的缓存 CacheControl.Builder() .maxAge(60, TimeUnit.SECONDS) .build() } val request chain.request().newBuilder().cacheControl(cacheControl).build() chain.proceed(request) } val okHttpClient OkHttpClient.Builder() .addInterceptor(cacheReadInterceptor) .addNetworkInterceptor(cacheSaveInterceptor) .build()根据URL类型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-age180) .build() } else if (url.contains(/user/)){ //用户信息不缓存 chain.proceed(request).newBuilder() .header(Cache-Control, no-cache) .build() } else { chain.proceed(request) } }
返回列表