Ok3 源码学习是我去年给自己立的flag,同时也是我去年难得兑现的几个flag之一,这里我想再重温下之前的学习状态,整理下我当时学习的思路。OK3的源码非常多,如果算上Okio,那就更多了,而且还涉及到很多我了解很少的网络知识(比如这篇要讲的OK3 缓存,前半部分我几乎先把http 的缓存套路说明白),庆幸的是OK3 的源码注释写的非常详尽,代码结构设计的也不错,理解起来不难,学起来收获满满。这个系列我准备分三篇博客来讲,今天先聊缓存。
这里先给大家看一张图,如果大家学过计算机组成原理这门课的话,想必一定眼熟:
简单描述下,Cache高速缓冲存储器,其作用是为了更好的利用局部性原理(这个可以不用理解),减少CPU访问主存的次数(重点)。简单地说,CPU正在访问的指令和数据,其可能会被以后多次访问到,或者是该指令和数据附近的内存区域,也可能会被多次访问。因此,第一次访问这一块区域时,将其复制到cache中,以后访问该区域的指令或者数据时,就不用再从主存中取出。
总结一句话:
缓存:方便用户快速获取数据的一种存储方式
提升用户体验
OKhttp 3的缓存策略原理,本质上就是对HTTP缓存机制的代码实现,因此只要我们把HTTP缓存机制理解了,那么后面在去看OKhttp 3的缓存代码会非常非常的容易,比如在第三部分我会重点分析的CacheControl、CacheInterceptor以及CacheStrategy。
1、按照**“端”**分类:
2、按照“是否向服务器发起请求进行对比”分类:
2.2.1、实现原理:当客户端第一次请求数据时,服务端返回了缓存的过期时间(Expires与Cache-Control)。在第二次请求数据时,如果已存在缓存数据,且缓存有效命中,则使用缓存数据库中的数据,无需请求网络。如果缓存失效或者没有命中,则请求网络,并将网络数据和缓存规则一并存入缓存数据库中。
文字看起来有点绕,那么用图例来说明下:
我将请求的三种情况用了三种颜色做了标注。这里我说下关于缓存失效的问题,在服务端返回的响应头信息中,有个max-age 字段,对应的值就是最大响应时间,小于这个时间内,缓存是有效的。超过这个时间,缓存是无效的。
2.2.2 设置缓存。关于设置强制缓存,我们需要在请求头中,设置expires/Cache-Control:服务器端返回的到期时间,即下一次请求时,请求时间小于服务器返回的到期时间。Cache-Control默认是private。这里我分享下常用的指令信息:
最后再说下,强制缓存的优先级要比接下来要说的对比缓存优先级高。
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缓存机制用的就是Http 缓存的套路。因此,这一节我们重点聊聊实现。来,先奉上一张OkHttp 3 总原理图。
总的原理图我这次先不分析,下篇我会单独阐述这图的原理。放上这张图我是想让大家明白,接下来我们要讲的缓存策略实现,就是上图中六大拦截器中的CacheInterceptor,以及它的好兄弟们(CacheControl、CacheStrategy、Cache以及DiskLruCache)之间的故事,看这几个类的类名,发现他们都和Cache有关,受至于篇幅原因,这里我仅部分类重点讲。
这个类是对HTTP的Cache-Control头部的描述,内部通过一个Build进行设置值,获取值可以通过CacheControl对象进行获取。在前面讲强制、对比缓存的设置时,提到了设置请求头关键字。而这个类的作用就是对那些关键字就行描述。这里我再补充些:
// (返回缓存指令,缓存控制。主要用在解析请求头、响应头,最后交给缓存策略类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类中实现的。
这个类封装了缓存策略,里面实现的策略本质都是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 + age
long ageMillis = cacheResponseAge();
// 刷新时间,一般服务器设置为max-age
long 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);
}
总结下上面的代码原理:
缓存策略的最终的使用,是在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;
}
负责读取缓存以及更新缓存.
先放上一张这个类的执行的流程图
接下来分析下代码的实现,拦截器的核心方法就是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源码解析(七)–中阶之缓存机制