OkHttp3源码详解之HTTP重定向&缓存的处理(二)

: null;

long now = System.currentTimeMillis();
//构造缓存策略,然后进行策略判断
cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
//策略判定后的网络请求和缓存响应
networkRequest = cacheStrategy.networkRequest;
cacheResponse = cacheStrategy.cacheResponse;

if (responseCache != null) {
//使用缓存响应的话,记录一下使用记录
responseCache.trackResponse(cacheStrategy);
}

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.
if (networkRequest == null && cacheResponse == null) {
//强制使用缓存,又找不到缓存,就报不合理请求响应了
userResponse = new Response.Builder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.protocol(Protocol.HTTP_1_1)
.code(504)
.message(“Unsatisfiable Request (only-if-cached)”)
.body(EMPTY_BODY)
.build();
return;
}

//上面情况处理之后,就是使用缓存返回,还是网络请求的情况了

// If we don’t need the network, we’re done.
if (networkRequest == null) {
//使用缓存返回响应
userResponse = cacheResponse.newBuilder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.cacheResponse(stripBody(cacheResponse))
.build();
userResponse = unzip(userResponse);
return;
}

//使用网络请求
//下面就是网络请求流程了,略

}
}

接下来我们分析

  1. Cache是如何获取缓存的。
  2. 缓存策略是如何判断的。
Cache获取缓存

从Cache的get方法开始。它按以下步骤进行。

  1. 计算request对应的key值,md5加密请求url得到。
  2. 根据key值去DiskLruCache查找是否存在缓存内容。
  3. 存在缓存的话,创建缓存Entry实体。ENTRY_METADATA代表响应头信息,ENTRY_BODY代表响应体信息。
  4. 然后根据缓存Entry实体得到响应,其中包含了缓存的响应头和响应体信息。
  5. 匹配这个缓存响应和请求的信息是否匹配,不匹配的话要关闭资源,匹配的话返回。

public final class Cache implements Closeable, Flushable {
//获取缓存
Response get(Request request) {
//计算请求对应的key
String key = urlToKey(request);
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
//这里从DiskLruCache中读取缓存信息
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}

try {
//这里读取缓存的响应头信息
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
//然后得到响应信息,包含了缓存响应头和响应体信息
Response response = entry.response(snapshot);
//判断缓存响应和请求是否匹配,匹配url,method,和其他响应头信息
if (!entry.matches(request, response)) {
//不匹配的话,关闭响应体
Util.closeQuietly(response.body());
return null;
}

//返回缓存响应
return response;
}

//这里md5加密url得到key值
private static String urlToKey(Request request) {
return Util.md5Hex(request.url().toString());
}

}

如果存在缓存的话,在指定的缓存目录中,会有两个文件“.0”和“.1”,分别存储某个请求缓存的响应头和响应体信息。(“****”是url的md5加密值)对应的ENTRY_METADATA响应头和ENTRY_BODY响应体。缓存的读取其实是由DiskLruCache来读取的,DiskLruCache是支持Lru(最近最少访问)规则的用于磁盘存储的类,对应LruCache内存存储。它在存储的内容超过指定值之后,就会根据最近最少访问的规则,把最近最少访问的数据移除,以达到总大小不超过限制的目的。

接下来我们分析CacheStrategy缓存策略是怎么判定的。

CacheStrategy缓存策略

直接看CacheStrategy的get方法。缓存策略是由请求和缓存响应共同决定的。

  1. 如果缓存响应为空,则缓存策略为不使用缓存。
  2. 如果请求是https但是缓存响应没有握手信息,同上不使用缓存。
  3. 如果请求和缓存响应都是不可缓存的,同上不使用缓存。
  4. 如果请求是noCache,并且又包含If-Modified-Since或If-None-Match,同上不使用缓存。
  5. 然后计算请求有效时间是否符合响应的过期时间,如果响应在有效范围内,则缓存策略使用缓存。
  6. 否则创建一个新的有条件的请求,返回有条件的缓存策略。
  7. 如果判定的缓存策略的网络请求不为空,但是只使用缓存,则返回两者都为空的缓存策略。

public final class CacheStrategy {

public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
//网络请求和缓存响应
this.request = request;
this.cacheResponse = cacheResponse;

if (cacheResponse != null) {
//找到缓存响应的响应头信息
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {
//查看响应头信息中是否有以下字段信息
String fieldName = headers.name(i);
String value = headers.value(i);
if (“Date”.equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if (“Expires”.equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
} else if (“Last-Modified”.equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if (“ETag”.equalsIgnoreCase(fieldName)) {
etag = value;
} else if (“Age”.equalsIgnoreCase(fieldName)) {
ageSeconds = HeaderParser.parseSeconds(value, -1);
} else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {
sentRequestMillis = Long.parseLong(value);
} else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
receivedResponseMillis = Long.parseLong(value);
}
}
}
}

public CacheStrategy get() {
//获取判定的缓存策略
CacheStrategy candidate = getCandidate();

if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
// 如果判定的缓存策略的网络请求不为空,但是只使用缓存,则返回两者都为空的缓存策略。
return new CacheStrategy(null, null);
}

return candidate;
}

/** Returns a strategy to use assuming the request can use the network. */
private CacheStrategy getCandidate() {
// No cached response.
//如果没有缓存响应,则返回没有缓存响应的策略
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}

// Drop the cached response if it’s missing a required handshake.
//如果请求是https,而缓存响应的握手信息为空,则返回没有缓存响应的策略
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.
//如果请求对应的响应不能被缓存,则返回没有缓存响应的策略
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}

//获取请求头中的CacheControl信息
CacheControl requestCaching = request.cacheControl();
//如果请求头中的CacheControl信息是不缓存的,则返回没有缓存响应的策略
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}

//获取响应的年龄
long ageMillis = cacheResponseAge();
//计算上次响应刷新的时间
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;
//获取缓存响应头中的CacheControl信息
CacheControl responseCaching = cacheResponse.cacheControl();
//如果缓存响应不是必须要再验证,并且请求有最大过期时间,则用请求的最大过期时间作为最大过期时间
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}

//如果支持缓存,并且持续时间+最短刷新时间<上次刷新时间+最大验证时间 则可以缓存
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());
}

//构造一个新的有条件的Request,添加If-None-Match,If-Modified-Since等信息
Request.Builder conditionalRequestBuilder = request.newBuilder();

if (etag != null) {
conditionalRequestBuilder.header(“If-None-Match”, etag);
} else if (lastModified != null) {
conditionalRequestBuilder.header(“If-Modified-Since”, lastModifiedString);
} else if (servedDate != null) {
conditionalRequestBuilder.header(“If-Modified-Since”, servedDateString);
}

Request conditionalRequest = conditionalRequestBuilder.build();
//根据是否有If-None-Match,If-Modified-Since信息,返回不同的缓存策略
return hasConditions(conditionalRequest)
? new CacheStrategy(conditionalRequest, cacheResponse)
: new CacheStrategy(conditionalRequest, null);
}

/**

  • Returns true if the request contains conditions that save the server from sending a response
  • that the client has locally. When a request is enqueued with its own conditions, the built-in
  • response cache won’t be used.
    */
    private static boolean hasConditions(Request request) {
    return request.header(“If-Modified-Since”) != null || request.header(“If-None-Match”) != null;
    }
    }

接来下我们看看CacheControl类里有些什么。

CacheControl

public final class CacheControl {

//表示这是一个优先使用网络验证,验证通过之后才可以使用缓存的缓存控制,设置了noCache
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();

//表示这是一个优先先使用缓存的缓存控制,设置了onlyIfCached和maxStale的最大值
public static final CacheControl FORCE_CACHE = new Builder()
.onlyIfCached()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build();

//以下的字段都是HTTP中Cache-Control字段相关的值
private final boolean noCache;
private final boolean noStore;
private final int maxAgeSeconds;
private final int sMaxAgeSeconds;
private final boolean isPrivate;
private final boolean isPublic;
private final boolean mustRevalidate;
private final int maxStaleSeconds;
private final int minFreshSeconds;
private final boolean onlyIfCached;
private final boolean noTransform;

//解析头文件中的相关字段,得到该缓存控制类
public static CacheControl parse(Headers headers) {

}

}

可以发现,它就是用于描述响应的缓存控制信息。

然后我们再看看Okhttp存储缓存是怎么进行的。

Okhttp存储缓存流程

存储缓存的流程从HttpEngine的readResponse发送请求开始的。

public final class HttpEngine {
/**

  • Flushes the remaining request header and body, parses the HTTP response headers and starts
  • reading the HTTP response body if it exists.
    */
    public void readResponse() throws IOException {
    //读取响应,略

// 判断响应信息中包含响应体
if (hasBody(userResponse)) {
// 如果缓存的话,缓存响应头信息
maybeCache();
//缓存响应体信息,同时zip解压缩响应数据
userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
}
}

// 如果缓存的话,缓存响应头信息
private void maybeCache() throws IOException {
InternalCache responseCache = Internal.instance.internalCache(client);
if (responseCache == null) return;

// Should we cache this response for this request?
if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
responseCache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
return;
}

// Offer this request to the cache.
//这里将响应头信息缓存到缓存文件中,对应缓存文件“****.0”
storeRequest = responseCache.put(stripBody(userResponse));
}

/**

  • Returns a new source that writes bytes to {@code cacheRequest} as they are read by the source
  • consumer. This is careful to discard bytes left over when the stream is closed; otherwise we
  • may never exhaust the source stream and therefore not complete the cached response.
    */
    //缓存响应体信息
    private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
    throws IOException {
    // Some apps return a null body; for compatibility we treat that like a null cache request.
    if (cacheRequest == null) return response;
    Sink cacheBodyUnbuffered = cacheRequest.body();
    if (cacheBodyUnbuffered == null) return response;

final BufferedSource source = response.body().source();
final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);

Source cacheWritingSource = new Source() {
boolean cacheRequestClosed;

//这里就是从响应体体读取数据,保存到缓存文件中,对应缓存文件“****.1”
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead;
try {
bytesRead = source.read(sink, byteCount);
} catch (IOException e) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
cacheRequest.abort(); // Failed to write a complete cache response.
}
throw e;
}

if (bytesRead == -1) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
cacheBody.close(); // The cache response is complete!
}
return -1;
}

sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
cacheBody.emitCompleteSegments();
return bytesRead;
}

@Override public Timeout timeout() {
return source.timeout();
}

@Override public void close() throws IOException {
if (!cacheRequestClosed
&& !discard(this, HttpStream.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
cacheRequestClosed = true;
cacheRequest.abort();
}
source.close();
}
};

return response.newBuilder()
.body(new RealResponseBody(response.headers(), Okio.buffer(cacheWritingSource)))
.build();
}
}

可以看到这里先通过maybeCache写入了响应头信息,再通过cacheWritingResponse写入了响应体信息。我们再进去看Cache的put方法实现。

private CacheRequest put(Response response) throws IOException {
String requestMethod = response.request().method();

// 响应的请求方法不支持缓存,只有GET方法支持缓存
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
// 同样,请求只支持GET方法的缓存
if (!requestMethod.equals(“GET”)) {
// Don’t cache non-GET responses. We’re technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
return null;
}

//缓存不支持通配符
if (OkHeaders.hasVaryAll(response)) {
return null;
}

//开始缓存
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(urlToKey(response.request()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}

我们继续看Cache的writeTo方法,可以看到是写入一些响应头信息。

public void writeTo(DiskLruCache.Editor editor) throws IOException {
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));

sink.writeUtf8(url);
sink.writeByte(‘\n’);
sink.writeUtf8(requestMethod);
sink.writeByte(‘\n’);
sink.writeDecimalLong(varyHeaders.size());
sink.writeByte(‘\n’);
for (int i = 0, size = varyHeaders.size(); i < size; i++) {
sink.writeUtf8(varyHeaders.name(i));
sink.writeUtf8(": ");
sink.writeUtf8(varyHeaders.value(i));
sink.writeByte(‘\n’);
}

sink.writeUtf8(new StatusLine(protocol, code, message).toString());
sink.writeByte(‘\n’);
sink.writeDecimalLong(responseHeaders.size());
sink.writeByte(‘\n’);
for (int i = 0, size = responseHeaders.size(); i < size; i++) {
sink.writeUtf8(responseHeaders.name(i));
sink.writeUtf8(": ");
sink.writeUtf8(responseHeaders.value(i));
sink.writeByte(‘\n’);
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

尾声

你不踏出去一步,永远不知道自己潜力有多大,千万别被这个社会套在我们身上的枷锁给捆住了,30岁我不怕,35岁我一样不怕,去做自己想做的事,为自己拼一把吧!不试试怎么知道你不行呢?

改变人生,没有什么捷径可言,这条路需要自己亲自去走一走,只有深入思考,不断反思总结,保持学习的热情,一步一步构建自己完整的知识体系,才是最终的制胜之道,也是程序员应该承担的使命。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

ttps://img2.imgtp.com/2024/03/13/H4lCoPEF.jpg" />

尾声

你不踏出去一步,永远不知道自己潜力有多大,千万别被这个社会套在我们身上的枷锁给捆住了,30岁我不怕,35岁我一样不怕,去做自己想做的事,为自己拼一把吧!不试试怎么知道你不行呢?

改变人生,没有什么捷径可言,这条路需要自己亲自去走一走,只有深入思考,不断反思总结,保持学习的热情,一步一步构建自己完整的知识体系,才是最终的制胜之道,也是程序员应该承担的使命。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-JNKOUjHG-1712292415762)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 9
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值