前言
在上一篇中,主要讲解了OkHttp 连接池复用机制、高并发分发、以及拦截器设计,但没有讲解每一个拦截器在框架中的作用,所以在本篇中会重点讲解每一个拦截器执行流程,以及对应的关系。
在下一篇中,将会手写一份阉割版的OkHttp,用来巩固对OkHttp的认知。话不多说,直接开始。
Response getResponseWithInterceptorChain() throws IOException {
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
源码解读
在上一篇中,我们看源码直接分析到这就结束了,现在我们就从这开始。 根据这一段代码可得拦截器分为(除去开发者自定义):
- RetryAndFollowUpInterceptor (重定向/重试拦截器)
- BridgeInterceptor (桥接拦截器)
- CacheInterceptor (缓存拦截器)
- ConnectInterceptor (连接拦截器)
- CallServerInterceptor(读写拦截器)
1、RetryAndFollowUpInterceptor (重定向/重试拦截器)
第一个拦截器:RetryAndFollowUpInterceptor,主要就是完成两件事情:重试与重定向。
1.1、重试
public final class RetryAndFollowUpInterceptor implements Interceptor {
...略
@Override public Response intercept(Chain chain) throws IOException {
...略
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
int followUpCount = 0;
Response priorResponse = null;
while (true) {
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;
boolean releaseConnection = true;
try {
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
...略
}
..略
}
源码解析
请求阶段发生了 RouteException 或者 IOException会进行判断是否重新发起请求。
catch (RouteException e) {
if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
throw e.getLastConnectException();
}
releaseConnection = false;
continue;
} catch (IOException e) {
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
}
源码解析
这俩个异常都调用了相同的方法 recover() 判断是否重试。那么进入一探究竟。
private boolean recover(IOException e, StreamAllocation streamAllocation,
boolean requestSendStarted, Request userRequest) {
streamAllocation.streamFailed(e);
if (!client.retryOnConnectionFailure()) return false;
if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
if (!isRecoverable(e, requestSendStarted)) return false;
if (!streamAllocation.hasMoreRoutes()) return false;
return true;
}
源码解析
在不禁止重试的前提下,如果出现了某些异常,并且存在更多的路由线路,则会尝试换条线路进行请求的重试。其中某些异常是在isRecoverable中进行判断。
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
if (e instanceof ProtocolException) {
return false;
}
if (e instanceof InterruptedIOException) {
return e instanceof SocketTimeoutException && !requestSendStarted;
}
if (e instanceof SSLHandshakeException) {
if (e.getCause() instanceof CertificateException) {
return false;
}
}
if (e instanceof SSLPeerUnverifiedException) {
return false;
}
return true;
}
源码解析
从这段代码可以看出:
- 协议异常不能重试
- 当socket超时时,并且没有新的连接通道的话,也不能重试
- SSL握手异常不能重试
- SSL未授权不能重试
- 其他情况均可重试请求
1.2、重定向
在讲重定向之前,首先先理解何为重定向? 如图所示 现有一个网络接口,在浏览器访问时,第一次报了错误code 302,但在该地址返回头里,却有一个Location 字段,这个字段里面的值才是我们要正常访问的接口,于是浏览器自动重定向访问了对应字段里面的值,并将最新的接口数据反馈回来。
从这可以看出,浏览器在访问接口时,自动帮我们做了重定向操作。那么吧这个地址在Android上用最原始的方式访问试试,看看没有做重定向的结果是怎样的。
private void testHttp(){
String path = "http://jz.yxg12.cn/old.php";
try {
HttpUrl httpUrl = new HttpUrl(path);
StringBuffer request = new StringBuffer();
request.append("GET ");
request.append(httpUrl.getFile());
request.append(" HTTP/1.1\r\n");
request.append("Host: "+httpUrl.getHost());
request.append("\r\n");
request.append("\r\n");
Socket socket = new Socket();
socket.connect(new InetSocketAddress(httpUrl.getHost(),httpUrl.getPort()));
OutputStream os = socket.getOutputStream();
InputStream is = socket.getInputStream();
new Thread(){
@Override
public void run() {
HttpCodec httpCodec = new HttpCodec();
try {
String responseLine = httpCodec.readLine(is);
System.out.println("响应行:" + responseLine);
Map<String, String> headers = httpCodec.readHeaders(is);
for (Map.Entry<String, String> entry : headers.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
if (headers.containsKey("Content-Length")) {
int length = Integer.valueOf(headers.get("Content-Length"));
byte[] bytes = httpCodec.readBytes(is, length);
content = new String(bytes);
System.out.println("响应:"+content);
} else {
String response = httpCodec.readChunked(is);
content = response;
System.out.println("响应:"+content);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
os.write(request.toString().getBytes());
os.flush();
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
运行效果
I/System.out: 响应行:HTTP/1.1 302 Found
I/System.out: Date: Sun, 03 Oct 2021 10:16:10 GMT
I/System.out: Location: http://jz.yxg12.cn/newInfo.php?page=1&size=100
I/System.out: Server: nginx
I/System.out: Transfer-Encoding: chunked
I/System.out: Content-Type: text/html; charset=UTF-8
I/System.out: Connection: keep-alive
I/System.out: 响应:
可以看出,没有重定向的效果,就把第一次的请求原封不动的返回过来了,根本就没有做再次请求的效果。既然没做重定向是这样的结果(返回错误的数据并且返回头里包含Location字段),那么做了重定向的是不是可以认为重新请求了返回头里面的Location属性?我们带着这样的疑问去阅读OkHttp源码。
public final class RetryAndFollowUpInterceptor implements Interceptor {
...略
@Override public Response intercept(Chain chain) throws IOException {
...略
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
while (true) {
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;
boolean releaseConnection = true;
response = realChain.proceed(request, streamAllocation, null, null);
...略
Request followUp = followUpRequest(response, streamAllocation.route());
if (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}
...略
request = followUp;
priorResponse = response;
}
}
...略
}
源码解析
从这段代码可以看出,重定向调用了方法 followUpRequest ,将方法返回的 Request 再次通过while循环重新请求。那么就去看看 followUpRequest 如何处理的。
private Request followUpRequest(Response userResponse, Route route) throws IOException {
if (userResponse == null) throw new IllegalStateException();
int responseCode = userResponse.code();
final String method = userResponse.request().method();
switch (responseCode) {
...略
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
if (!client.followRedirects()) return null;
String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);
if (url == null) return null;
...略
Request.Builder requestBuilder = userResponse.request().newBuilder();
...略
return requestBuilder.url(url).build();
...略
default:
return null;
}
}
源码解析
看到这,应该明白了吧,和刚刚我们猜想的一样,当遇到需要重定向code的时,需要读取返回头里面的Location属性,然后将对应的属性作为新的Url再次包装请求头。
2、BridgeInterceptor (桥接拦截器)
注:可略过该类源码,直接看这段源码下面的解析,当然你想看,我也不拦着,毕竟源码也贴出来了。
public final class BridgeInterceptor implements Interceptor {
private final CookieJar cookieJar;
public BridgeInterceptor(CookieJar cookieJar) {
this.cookieJar = cookieJar;
}
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
if (body != null) {
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
if (contentLength != -1) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
}
if (userRequest.header("Host") == null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies));
}
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent());
}
Response networkResponse = chain.proceed(requestBuilder.build());
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
GzipSource responseBody = new GzipSource(networkResponse.body().source());
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
String contentType = networkResponse.header("Content-Type");
responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
}
return responseBuilder.build();
}
private String cookieHeader(List<Cookie> cookies) {
StringBuilder cookieHeader = new StringBuilder();
for (int i = 0, size = cookies.size(); i < size; i++) {
if (i > 0) {
cookieHeader.append("; ");
}
Cookie cookie = cookies.get(i);
cookieHeader.append(cookie.name()).append('=').append(cookie.value());
}
return cookieHeader.toString();
}
}
源码解读
这个拦截器,你就理解成网络请求头/响应头的拼接封装。就下面图片这一坨字符串拼接,就由这个拦截器完成。
这个拦截器是最简单的,没啥可说的。
3、CacheInterceptor (缓存拦截器)
做好心理准备,又大又硬的骨头来了。
public final class CacheInterceptor implements Interceptor {
...略
@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body());
}
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 (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
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();
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
}
}
}
return response;
}
}
源码解析
- 分析点1:先把它看成从一个缓存工厂里面拿到缓存(稍后会紧跟着讲如何拿到缓存)
- networkRequest :此调用如果不使用网络则为空,使用网络就不为空
- cacheResponse :此调用如果不使用缓存则为空,使用缓存就不为空
- 分析点2:如果既不使用网络,也不使用缓存,请求就返回code为504的异常
- 分析点3:如果不使用网络,那么就读取缓存,并且直接返回上一次缓存的数据
- 分析点4:如果使用网络,并且也有缓存,那么先去通过网络请求一次,接着更新缓存,最后再返回最新的数据
- 分析点5:如果使用网络,本地也没有缓存,那就去请求网络数据。
- 分析点6:网络数据请求下来了,接着判断是否要缓存,如果缓存,再判断哪些能缓存(稍后会讲,到底哪些状态才会写入缓存)
3.1、如何写入缓存?
我们先来到 分析6 这里,写入缓存的方法毋庸置疑就是 cacheWritingResponse 方法,但是在研究这个方法之前,我们先看看,要哪些条件才能进入if判断。
第一层 if 判断 if (cache != null) ,表示 使用者表示该请求需要缓存; 第二层 if 判断 if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) ,分别调用了俩个方法,hasBody、isCacheable ,这两个都为true的时候,才能写缓存
HttpHeaders.hasBody
public static boolean hasBody(Response response) {
if (response.request().method().equals("HEAD")) {
return false;
}
int responseCode = response.code();
if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
&& responseCode != HTTP_NO_CONTENT
&& responseCode != HTTP_NOT_MODIFIED ) {
return true;
}
if (contentLength(response) != -1
|| "chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
return true;
}
return false;
}
源码解析
从这个方法可以看出
- HEAD 请求不能被缓存
- 响应体code 可以小于100,也可以大于等于200,但不能为204,也不能是304的网络请求才能缓存
- 如果 Content-Length 或 Transfer-Encoding 标头与响应代码一致时,才能缓存
接下来轮到第二个方法了
CacheStrategy.isCacheable
public static boolean isCacheable(Response response, Request request) {
switch (response.code()) {
case HTTP_OK:
case HTTP_NOT_AUTHORITATIVE:
case HTTP_NO_CONTENT:
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_NOT_FOUND:
case HTTP_BAD_METHOD:
case HTTP_GONE:
case HTTP_REQ_TOO_LONG:
case HTTP_NOT_IMPLEMENTED:
case StatusLine.HTTP_PERM_REDIRECT:
break;
case HTTP_MOVED_TEMP:
case StatusLine.HTTP_TEMP_REDIRECT:
if (response.header("Expires") != null
|| response.cacheControl().maxAgeSeconds() != -1
|| response.cacheControl().isPublic()
|| response.cacheControl().isPrivate()) {
break;
}
default:
return false;
}
return !response.cacheControl().noStore() && !request.cacheControl().noStore();
}
源码解析
- 先看最后一句代码,只要是请求头,响应头里面的Cache-Control属性任意一个为 no-store 值,都不能缓存
- 在满足1的情况下,code 在 302、307 重定向的情况下,需要判断是不是存在一些允许缓存的响应头
- 在满足1的情况,并且不满足2的情况下,对应code 的响应都能缓存
现在写入缓存前的判断都分析完了,可以开始分析缓存的写入了。
写入缓存 (cacheWritingResponse)
private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
throws IOException {
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;
@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();
}
throw e;
}
if (bytesRead == -1) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
cacheBody.close();
}
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, HttpCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
cacheRequestClosed = true;
cacheRequest.abort();
}
source.close();
}
};
String contentType = response.header("Content-Type");
long contentLength = response.body().contentLength();
return response.newBuilder()
.body(new RealResponseBody(contentType, contentLength, Okio.buffer(cacheWritingSource)))
.build();
}
源码分析
开头开一段标准的非空判断, 接着分析点7 和分析点 8 都是用了Square公司 另一个 Okio 读写流框架,这里就不过多解读了,反正可以理解为这里通过Okio框架,将缓存写入到SD卡,写入成功后在返回写入的数据就行了。
3.2、如何读取缓存?
为了避免来回翻,这里再贴一次简短的缓存拦截器里面的源码。
@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
...略
}
源码解析
继续刚刚的分析1,我们先看里面的缓存工厂
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
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 = HttpHeaders.parseSeconds(value, -1);
}
}
}
}
源码分析
这里无非就是一个构造函数,有一个for循环,然后在里面有一堆一系列的if条件语句判断。分别解释一下每个判断的意思:
- Date 消息发送的时间
- Expires 资源过期的时间
- Last-Modified 资源最后修改时间
- ETag 资源在服务器的唯一标识
- Age 服务器用缓存响应请求,该缓存从产生到现在经过多长时间(秒)
接着我们再来看get 方法
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
return new CacheStrategy(null, null);
}
return candidate;
}
源码解读
代码很少,通过 方法 getCandidate() 来获取对应的缓存。继续深入。
private CacheStrategy getCandidate() {
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
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;
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());
}
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);
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
}
源码解析
好累啊,这注释快弄吐了,不过离啃完大骨头还差最后一步。这里讲解要结合刚刚上面分析点一起讲,我这就直接贴过来。
- 分析点1:先把它看成从一个缓存工厂里面拿到缓存(稍后会紧跟着讲如何拿到缓存)
- networkRequest :此调用如果不使用网络则为空,使用网络就不为空
- cacheResponse :此调用如果不使用缓存则为空,使用缓存就不为空
- 分析点2:如果既不使用网络,也不使用缓存,请求就返回code为504的异常
- 分析点3:如果不使用网络,那么就读取缓存,并且直接返回上一次缓存的数据
- 分析点4:如果使用网络,并且也有缓存,那么先去通过网络请求一次,接着更新缓存,最后再返回最新的数据
- 分析点5:如果使用网络,本地也没有缓存,那就去请求网络数据。
- 分析点6:网络数据请求下来了,接着判断是否要缓存,如果缓存,再判断哪些能缓存(稍后会讲,到底哪些状态才会写入缓存)
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
public final class CacheStrategy {
CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
}
}
从上面分析点和最近代码段结合看,缓存属性是在构造函数的第二变量,也就是说,要使用缓存的话,那么第二个参数必须不能为空,在 getCandidate方法里,真正使用到缓存的 在对应的 分析点9、分析点10、分析点11 位置。源码里面注释也写得比较全,这里就不总结了。
4、ConnectInterceptor (连接拦截器)
public final class ConnectInterceptor implements Interceptor {
public final OkHttpClient client;
public ConnectInterceptor(OkHttpClient client) {
this.client = client;
}
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
源码解析
这里连接拦截器的代码相对较少,主要连接功能都封装在 streamAllocation.newStream 这里面了,而StreamAllocation 变量又是在 RetryAndFollowUpInterceptor (重定向/重试拦截器) 里面创建的。现在先看newStream 方法。
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
int connectTimeout = chain.connectTimeoutMillis();
int readTimeout = chain.readTimeoutMillis();
int writeTimeout = chain.writeTimeoutMillis();
int pingIntervalMillis = client.pingIntervalMillis();
boolean connectionRetryEnabled = client.retryOnConnectionFailure();
try {
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
源码解析
这里主要逻辑在 findHealthyConnection 方法里,进去看看
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
boolean doExtensiveHealthChecks) throws IOException {
while (true) {
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
pingIntervalMillis, connectionRetryEnabled);
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();
continue;
}
return candidate;
}
}
源码解析
代码较少,这里可以看出开启了一个死循环,然后调用findConnection ,主要逻辑也在这个方法里面,继续深入。
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false;
RealConnection result = null;
Route selectedRoute = null;
Connection releasedConnection;
Socket toClose;
synchronized (connectionPool) {
if (released) throw new IllegalStateException("released");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");
releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {
releasedConnection = null;
}
if (result == null) {
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
}
closeQuietly(toClose);
if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);
}
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
}
if (result != null) {
return result;
}
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
routeSelection = routeSelector.next();
}
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
if (newRouteSelection) {
List<Route> routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
Internal.instance.get(connectionPool, address, this, route);
if (connection != null) {
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}
if (!foundPooledConnection) {
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);
acquire(result, false);
}
}
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
reportedAcquired = true;
Internal.instance.put(connectionPool, result);
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
closeQuietly(socket);
eventListener.connectionAcquired(call, result);
return result;
}
源码解析
连接拦截器的核心就在于这个方法,主要逻辑:先是判断已分配连接是否存在,如果不存在,那么先从线程池里面寻找,线程池里面没有找到然后再去对应线路中寻找,如果都没找到,最后就会开辟新的连接,加入到线程池里并且将连接成功后的对象返回。
5、CallServerInterceptor(读写/请求拦截器)
public final class CallServerInterceptor implements Interceptor {
private final boolean forWebSocket;
public CallServerInterceptor(boolean forWebSocket) {
this.forWebSocket = forWebSocket;
}
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
realChain.eventListener().requestHeadersStart(realChain.call());
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(true);
}
if (responseBuilder == null) {
realChain.eventListener().requestBodyStart(realChain.call());
long contentLength = request.body().contentLength();
CountingSink requestBodyOut =
new CountingSink(httpCodec.createRequestBody(request, contentLength));
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
realChain.eventListener()
.requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
} else if (!connection.isMultiplexed()) {
streamAllocation.noNewStreams();
}
}
httpCodec.finishRequest();
if (responseBuilder == null) {
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(false);
}
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
int code = response.code();
if (code == 100) {
responseBuilder = httpCodec.readResponseHeaders(false);
response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
realChain.eventListener()
.responseHeadersEnd(realChain.call(), response);
if (forWebSocket && code == 101) {
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;
}
...略
}
源码解析
这个类里面包含了上面拦截器所有内容,什么连接拦截器设置的 httpCode、桥接拦截器封装的请求体以及响应体,以及重定向拦截等等。主要功能就是网络接口请求时,起到了网络请求体以及响应体的封装作用。写好的请求体拿去请求网络,写好的响应体一次返回给上一级。
到这里,本章内容差不多结束了,最后来一张流程图做个总结。
6、拦截器流程
如图所示
相信你能够轻易看懂这张流程图,也能全方位熟悉 OkHttp整个拦截器的走向。到这,整个OkHttp的源码已经全部解析完了。
在下一篇里,我将仿照OkHttp结构,手写一份阉割版的OkHttp,用来加固对OkHttp源码的认识。
|