前言
Ok3 源码学习是我去年给自己立的flag,同时也是我去年难得兑现的几个flag之一,这里我想再重温下之前的学习状态,整理下我当时学习的思路。OK3的源码非常多,如果算上Okio,那就更多了,而且还涉及到很多我了解很少的网络知识(比如这篇要讲的OK3 缓存,前半部分我几乎先把http 的缓存套路说明白),庆幸的是OK3 的源码注释写的非常详尽,代码结构设计的也不错,理解起来不难,学起来收获满满。这个系列我准备分三篇博客来讲,今天先聊缓存。
一、缓存概述
1.1、什么是缓存?
这里先给大家看一张图,如果大家学过计算机组成原理这门课的话,想必一定眼熟:
简单描述下,Cache高速缓冲存储器,其作用是为了更好的利用局部性原理(这个可以不用理解),减少CPU访问主存的次数(重点)。简单地说,CPU正在访问的指令和数据,其可能会被以后多次访问到,或者是该指令和数据附近的内存区域,也可能会被多次访问。因此,第一次访问这一块区域时,将其复制到cache中,以后访问该区域的指令或者数据时,就不用再从主存中取出。
总结一句话:
缓存:方便用户快速获取数据的一种存储方式
1.2、缓存的特点?
- 1、缓存载体与持久载体总是相对的,体量远远小于持久载体,成本高于持久载体,速度高于持久载体。
- 2、缓存需要页面置换算法,将旧页面去掉换成新页面,如最久未使用算法(LRU)、先进先出算法(FIFO)、最紧最小使用算法(LFU)、非最紧使用算法(NMRU)。
- 3、可溯源,如果没有命中缓存,就需要从原始地址获取,这个步骤叫做”回源头",CDN厂商会标注"回源率"。
1.3、移动端网络使用缓存的意义
- 1、减少请求次数,减轻服务器压力。
- 2、本地数据读取更快,让页面不会空白几百毫秒。
- 3、在无网络的情况下提供数据。
总之,核心就是:
提升用户体验
二、HTTP缓存机制
OKhttp 3的缓存策略原理,本质上就是对HTTP缓存机制的代码实现,因此只要我们把HTTP缓存机制理解了,那么后面在去看OKhttp 3的缓存代码会非常非常的容易,比如在第三部分我会重点分析的CacheControl、CacheInterceptor以及CacheStrategy。
2.1、缓存的分类
1、按照**“端”**分类:
- 服务器缓存(也称网关缓存),广泛使用的CDN也是一种服务端缓存,目的都是让用户的请求走"捷径",并且都是缓存图片、文件等静态资源。
- 客户端缓存,一般是浏览器缓存,目的就是加速各种静态资源的访问,提升用户体验。
2、按照“是否向服务器发起请求进行对比”分类:
- 强制缓存
- 对比缓存
OkHttp 3的缓存策略实现就是从按照“是否向服务器发起请求进行对比”分类进行实现的。因此接下来我们将重点介绍“强制缓存”、“对比缓存”的实现原理。
2.2、强制缓存
2.2.1、实现原理:当客户端第一次请求数据时,服务端返回了缓存的过期时间(Expires与Cache-Control)。在第二次请求数据时,如果已存在缓存数据,且缓存有效命中,则使用缓存数据库中的数据,无需请求网络。如果缓存失效或者没有命中,则请求网络,并将网络数据和缓存规则一并存入缓存数据库中。
文字看起来有点绕,那么用图例来说明下:
我将请求的三种情况用了三种颜色做了标注。这里我说下关于缓存失效的问题,在服务端返回的响应头信息中,有个max-age 字段,对应的值就是最大响应时间,小于这个时间内,缓存是有效的。超过这个时间,缓存是无效的。
2.2.2 设置缓存。关于设置强制缓存,我们需要在请求头中,设置expires/Cache-Control:服务器端返回的到期时间,即下一次请求时,请求时间小于服务器返回的到期时间。Cache-Control默认是private。这里我分享下常用的指令信息:
最后再说下,强制缓存的优先级要比接下来要说的对比缓存优先级高。
2.3、对比缓存
2.3.1、实现原理:当客户端第一请求数据时,服务端会将缓存标识(Last-Modified/If-Modified-Since与Etag/If-None-Match)与数据一起返回给客户端,客户端将两种数据。都备份到缓存中,再次请求数据时,客户端将上次备份的缓存标识发送给服务端,服务端根据缓存标识进行判断,如果返回304,则表示通知客户端可以继续使用缓存。文字看起来不明白,直接上图:
这里我还是将请求的三种情况,使用三种颜色进行标注。在介绍强制缓存的最后,我提到了强制缓存的优先级是最高的。如果强制缓存的判断是有效的,则直接读取缓存即可。而对比缓存的存在就是在强制缓存失效的情况下做的二次判断。如果我们配置了对比缓存的策略,且服务端校验当前缓存标识是有效的,即使在强制缓存阶段判断不能使用使用缓存,只要服务端确定当前内容资源服务端没有修改,那么还是可以继续使用缓存的,因此看图也发现,对比缓存相比强制缓存,又多了一次服务端校验缓存的步骤 ,这样做的好处就是可以节约流量,避免资源浪费。
2.3.2 设置缓存。
设置对比缓存有两种方式。
-
1、通过设置Last-Modified/If-Modified-Since。Last-Modified:资源最后的修改日期。在Response Header中,服务器在响应请求时,告诉客户端资源最后的修改时间。If-Modified-Since:比较资源的更新时间。在Request Header中,当客户端再次请求时,通知服务器上次请求时返回的资源最后修改的时间。
服务器收到请求后发现有If-Modified-Since,则与被请求资源的最后修改时间进行对比。若资源的最后修改时间大于If-Modified-Since,说明资源又被改动过,则响应整个内容,返回状态码是200.如果资源的最后修改时间小于或者等于If-Modified-Since,说明资源没有修改,则响应状态码为304,告诉客户端继续使用cache。
-
2、ETag/If-None-Match(优先级高于Last-Modified/If-Modified-Since )。ETag:资源的匹配信息。在Response Header中,服务器在响应请求时,告诉客户端当前资源在服务器的唯一标识(生成规则由服务器决定)。If-None-Match:比较实体标记符。在Request Header中,当客户端再次请求时,通过此字段通知服务器客户端缓存数据的唯一标识.
服务器收到请求后发现有头部If-None-Match则与被请求的资源的唯一标识进行对比,不同则说明资源被改过,则响应整个内容,返回状态码是200,相同则说明资源没有被改动过,则响应状态码304,告知客户端可以使用缓存
最后,我将强制缓存和对比缓存结合在一起用一张图来理解Http缓存执行流程。
三、OKHttp 3缓存机制与实现
OKHttp 3缓存机制用的就是Http 缓存的套路。因此,这一节我们重点聊聊实现。来,先奉上一张OkHttp 3 总原理图。
总的原理图我这次先不分析,下篇我会单独阐述这图的原理。放上这张图我是想让大家明白,接下来我们要讲的缓存策略实现,就是上图中六大拦截器中的CacheInterceptor,以及它的好兄弟们(CacheControl、CacheStrategy、Cache以及DiskLruCache)之间的故事,看这几个类的类名,发现他们都和Cache有关,受至于篇幅原因,这里我仅部分类重点讲。
3.1、CacheControl
这个类是对HTTP的Cache-Control头部的描述,内部通过一个Build进行设置值,获取值可以通过CacheControl对象进行获取。在前面讲强制、对比缓存的设置时,提到了设置请求头关键字。而这个类的作用就是对那些关键字就行描述。这里我再补充些:
- 1、noCache() 对应于“no-cache”,如果出现在响应的头部,表示客户端需要与服务器进行再次验证,进行一个额外的GET请求得到最新的响应;如果出现请求头部,则表示不使用缓存响应,使用网络请求获取响应。
- 2、noStore() 对应于"no-store",如果出现在响应头部,则表明该响应不能被缓存
- 3、maxAge(int maxAge,TimeUnit timeUnit) 对应"max-age",设置缓存响应的最大缓存时间。如果缓存响满足了到了最大存活时间,那么将不会再进行网络请求
- 4、maxStale(int maxStale,TimeUnit timeUnit) 对应“max-stale”,缓存响应可以接受的最大过期时间,如果没有指定该参数,那么过期缓存响应将不会被使用
- 5、minFresh(int minFresh,TimeUnit timeUnit) 对应"min-fresh",设置一个响应将会持续刷新最小秒数,如果一个响应当minFresh过去后过期了,那么缓存响应不能被使用,需要重新进行网络请求
- 6、onlyIfCached() 对应“onlyIfCached”,用于请求头部,表明该请求只接受缓存中的响应。如果缓存中没有响应,那么返回一个状态码为504的响应。
这个类最核心的方法就是parse,重点看下这个:
// (返回缓存指令,缓存控制。主要用在解析请求头、响应头,最后交给缓存策略类CacheStrategy来处理,最 后在CacheInterceptor调用缓存策略来判断是否要写缓存)public static CacheControl parse(Headers headers) {......// 上面的不是重点,省略for (int i = 0, size = headers.size(); i < size; i++) {String name = headers.name(i);String value = headers.value(i);......// 根据对请求头信息的解析,判断此次请求是否设置了缓存校验if ("no-cache".equalsIgnoreCase(directive)) {noCache = true;} else if ("no-store".equalsIgnoreCase(directive)) {noStore = true;} else if ("max-age".equalsIgnoreCase(directive)) {maxAgeSeconds = HttpHeaders.parseSeconds(parameter, -1);} else if ("s-maxage".equalsIgnoreCase(directive)) {sMaxAgeSeconds = HttpHeaders.parseSeconds(parameter, -1);} else if ("private".equalsIgnoreCase(directive)) {isPrivate = true;} else if ("public".equalsIgnoreCase(directive)) {isPublic = true;} else if ("must-revalidate".equalsIgnoreCase(directive)) {mustRevalidate = true;} else if ("max-stale".equalsIgnoreCase(directive)) {maxStaleSeconds = HttpHeaders.parseSeconds(parameter, Integer.MAX_VALUE);} else if ("min-fresh".equalsIgnoreCase(directive)) {minFreshSeconds = HttpHeaders.parseSeconds(parameter, -1);} else if ("only-if-cached".equalsIgnoreCase(directive)) {onlyIfCached = true;} else if ("no-transform".equalsIgnoreCase(directive)) {noTransform = true;} else if ("immutable".equalsIgnoreCase(directive)) {immutable = true;}}}
关于请求、响应头信息是设置和读取,在BridgeInterceptor类中实现的。
3.2、CacheStrategy
这个类封装了缓存策略,里面实现的策略本质都是RFC标准文档里面写死的。同时利用networkRequest、cacheResponse这两个参数生成最终的策略,将networkRequest与cacheResponse这两个值输入,处理之后再将这两个值输出。这个类重点看下下面的这个方法:
/** Returns a strategy to use assuming the request can use the network. */private CacheStrategy getCandidate() {// No cached response.// 1. 如果缓存没有命中,就直接进行网络请求。if (cacheResponse == null) {return new CacheStrategy(request, null);}// Drop the cached response if it's missing a required handshake.// 2. 如果TLS握手信息丢失,则返回直接进行连接。if (request.isHttps() && cacheResponse.handshake() == null) {return new CacheStrategy(request, null);}// If this response shouldn't have been stored, it should never be used// as a response source. This check should be redundant as long as the// persistence store is well-behaved and the rules are constant.// 3. 根据response状态码,Expired时间和是否有no-cache标签就行判断是否进行直接访问if (!isCacheable(cacheResponse, request)) {return new CacheStrategy(request, null);}// 4. 如果请求header里有"no-cache"或者右条件GET请求(header里带有ETag/Since标签),则直接连接。CacheControl requestCaching = request.cacheControl();if (requestCaching.noCache() || hasConditions(request)) {return new CacheStrategy(request, null);}// immutable : 不可变的,表示当前的缓存有效CacheControl responseCaching = cacheResponse.cacheControl();if (responseCaching.immutable()) {return new CacheStrategy(null, cacheResponse);}// 计算当前Age的时间戳:now - sent + agelong ageMillis = cacheResponseAge();// 刷新时间,一般服务器设置为max-agelong freshMillis = computeFreshnessLifetime();// 如果请求里面有最大持久时间要求,则两者选择最短时间的要求if (requestCaching.maxAgeSeconds() != -1) {freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));}// 用请求中的最小更新时间来更新最小时间限制long minFreshMillis = 0;if (requestCaching.minFreshSeconds() != -1) {minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());}// 最大验证时间,如果响应(服务器)那边不是必须验证并且存在最大验证秒数,则更新最大验证时间long maxStaleMillis = 0;if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());}// 5.持续时间+最短刷新时间<上次刷新时间+最大验证时间 则直接返回上次缓存,即:缓存在过期时间内则可以直接使用// 也可以理解就是现在时间(now)-已经过去的时间(sent)+可以存活的时间<最大存活时间(max-age)if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {Response.Builder builder = cacheResponse.newBuilder();if (ageMillis + minFreshMillis >= freshMillis) {builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");}long oneDayMillis = 24 * 60 * 60 * 1000L;if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");}return new CacheStrategy(null, builder.build());}// Find a condition to add to the request. If the condition is satisfied, the response body// will not be transmitted.// 6. 如果缓存过期,且有ETag等信息,则发送If-None-Match、If-Modified-Since、If-Modified-Since等条件请求,交给服务端判断处理String conditionName;String conditionValue;if (etag != null) {conditionName = "If-None-Match";conditionValue = etag;} else if (lastModified != null) {conditionName = "If-Modified-Since";conditionValue = lastModifiedString;} else if (servedDate != null) {conditionName = "If-Modified-Since";conditionValue = servedDateString;} else {return new CacheStrategy(request, null); // No condition! Make a regular request.}Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);Request conditionalRequest = request.newBuilder().headers(conditionalRequestHeaders.build()).build();// 返回有条件的缓存request策略return new CacheStrategy(conditionalRequest, cacheResponse);}
总结下上面的代码原理:
- 1、如果networkRequest为null,cacheResponse为null:only-if-cached(表明不进行网络请求,且缓存不存在或者过期,一定会返回503错误)。
- 2、如果networkRequest为null,cacheResponse为non-null:不进行网络请求,而且缓存可以使用,直接返回缓存,不用请求网络。
- 3、如果networkRequest为non-null,cacheResponse为null:需要进行网络请求,而且缓存不存在或者过期,直接访问网络。
- 4、如果networkRequest为non-null,cacheResponse为non-null:Header中含有ETag/Last-Modified标签,需要在条件请求下使用,还是需要访问网络。
缓存策略的最终的使用,是在CacheInterceptor类中。在CacheStrategy中提供了一个方法可以获取当前的策略:
/*** Returns a strategy to satisfy {@code request} using the a cached response {@code response}.*/public CacheStrategy get() {CacheStrategy candidate = getCandidate();if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {// We're forbidden from using the network and the cache is insufficient.return new CacheStrategy(null, null);}return candidate;}
3.3、CacheInterceptor
负责读取缓存以及更新缓存.
先放上一张这个类的执行的流程图
接下来分析下代码的实现,拦截器的核心方法就是intercept。
@Override public Response intercept(Chain chain) throws IOException {// 1. 读取候选缓存Response cacheCandidate = cache != null? cache.get(chain.request()): null;long now = System.currentTimeMillis();// 2. 创建缓存策略,获取策略中的请求、响应CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();Request networkRequest = strategy.networkRequest;Response cacheResponse = strategy.cacheResponse;if (cache != null) {// 跟踪满足缓存策略CacheStrategy的响应cache.trackResponse(strategy);}if (cacheCandidate != null && cacheResponse == null) {closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.}// If we're forbidden from using the network and the cache is insufficient, fail.// 3. 根据策略,不使用网络,又没有缓存的直接报错,并返回错误码504。if (networkRequest == null && cacheResponse == null) {return new Response.Builder().request(chain.request()).protocol(Protocol.HTTP_1_1).code(504).message("Unsatisfiable Request (only-if-cached)").body(Util.EMPTY_RESPONSE).sentRequestAtMillis(-1L).receivedResponseAtMillis(System.currentTimeMillis()).build();}// If we don't need the network, we're done.// 4. 根据策略,不使用网络,有缓存的直接返回if (networkRequest == null) {return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();}// 5. 前面两个都没有返回,继续执行下一个Interceptor,即ConnectInterceptor。Response networkResponse = null;try {networkResponse = chain.proceed(networkRequest);} finally {// If we're crashing on I/O or otherwise, don't leak the cache body.if (networkResponse == null && cacheCandidate != null) {closeQuietly(cacheCandidate.body());}}// If we have a cache response too, then we're doing a conditional get.// 6. 接收到网络结果,如果响应code式304,则使用缓存,返回缓存结果。if (cacheResponse != null) {if (networkResponse.code() == HTTP_NOT_MODIFIED) {Response response = cacheResponse.newBuilder().headers(combine(cacheResponse.headers(), networkResponse.headers())).sentRequestAtMillis(networkResponse.sentRequestAtMillis()).receivedResponseAtMillis(networkResponse.receivedResponseAtMillis()).cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build();networkResponse.body().close();// Update the cache after combining headers but before stripping the// Content-Encoding header (as performed by initContentStream()).// 跟踪一个满足缓存条件的GET请求 ,并更新缓存cache.trackConditionalCacheHit();cache.update(cacheResponse, response);return response;} else {closeQuietly(cacheResponse.body());}}Response response = networkResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build();// 读取网络结果,这里有两种情况// 1、根据服务端响应的结果,允许使用缓存(!response.cacheControl().noStore() && !request.cacheControl().noStore() )// 2、当前的网络请求适合使用缓存,即仅Get请求才会缓存,其他请求就不缓存if (cache != null) {if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {// Offer this request to the cache.CacheRequest cacheRequest = cache.put(response);return cacheWritingResponse(cacheRequest, response);}if (HttpMethod.invalidatesCache(networkRequest.method())) {try {cache.remove(networkRequest);} catch (IOException ignored) {// The cache cannot be written.}}}return response;}
关于OK3的缓存实现就分析到这里。至于缓存的写入和读取,主要是在Cache、DiskLruCache类中。这些操作这里我就不分析了,DiskLruCache很多开源项目中都会使用,只要涉及到有缓存功能的,比如一些开源的图片框架,后面我会单独聊下这个类的实现原理。
总结
OK3的缓存原理相比于它的其它重要模块,理解起来要简单的多,关于缓存原理,重点就是理解Http的缓存原理实现,以及上面说的那三个类的作用。
参考:
OKHttp源码解析(七)–中阶之缓存机制