(面试必备-源码系列)OkHttp3
一般面试的时候,只要简历写了开源库,面试官一般都会问源码,所以如何读源码,如何应对面试中的源码问题呢?
今天开始分析OkHttp3源码,希望对大家有所帮助。
首先,带着问题看源码
1.分析OkHttp请求流程
2.知道请求流程之后,拦截链的各个拦截器分析
一、请求流程之拦截链
okhttp发一个同步请求的流程
OkHttpClient client = new OkHttpClient();
Request.Builder builder = new Request.Builder().url("http:.//www.baidu.com");
Request request = builder.build();
//前面是参数构造,主要是下面这一句一句
Response response = client.newCall(request).execute();
RealCall:请求的真正执行者
// class:OkHttpClient
@Override public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}
//class:RealCall
@Override public Response execute() throws IOException {
...
try {
//将RealCall加到队列 Deque<RealCall> runningSyncCalls
client.dispatcher().executed(this);
//通过一系列的拦截器请求处理和响应处理得到最终的返回结果
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
}
...
}
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
//在配置 OkHttpClient 时设置的 interceptors,放在第一位
interceptors.addAll(client.interceptors());
// 负责失败重试以及重定向
interceptors.add(retryAndFollowUpInterceptor);
// 桥拦截器,添加一些Header和移除一些header,例如body的contentLength是-1,则移除Content-Length这个header
interceptors.add(new BridgeInterceptor(client.cookieJar()));
// 负责读取缓存直接返回、更新缓存
interceptors.add(new CacheInterceptor(client.internalCache()));
// 负责和服务器建立连接
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
// 配置 OkHttpClient 时设置的 networkInterceptors
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);
}
可以看到这个调用链的流程是这样的,
- 自定义的 interceptor
- retryAndFollowUpInterceptor
- BridgeInterceptor(client.cookieJar())
- CacheInterceptor(client.internalCache())
- ConnectInterceptor(client)
- client.networkInterceptors() --如果有设置的话
- CallServerInterceptor(forWebSocket)
看下chain.proceed 的内部实现,
//class:RealInterceptorChain
...
//自定义拦截链intercept方法处理完后要调用chain.proceed(request)让拦截链继续下去
@Override public Response proceed(Request request) throws IOException {
return proceed(request, streamAllocation, httpCodec, connection);
}
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
//取出下一个拦截器,调用它的intercept方法
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
...
return response;
}
RealInterceptorChain
是一个负责管理拦截链的类,每个拦截器调用chain.proceed(request)
,就会走到下一个拦截器的 intercept
方法
拦截链最底部的拦截器是 CallServerInterceptor
(后面有分析),用okio请求网络返回了Response,然后Response往这个拦截链回传,上一个拦截器通过 response = chain.proceed(request);
就获取到response了
小结:
Okhttp3通过拦截链的设计,每个拦截器各司其职,我们可以自定义拦截链,像打印日志,统一添加header等等,都是通过添加拦截器实现。
二 、分析下各个拦截器
1.RetryAndFollowUpInterceptor: 失败重试和重定向拦截器
public Response intercept(Chain chain) throws IOException {
while (true) {
...
response = realChain.proceed(request, streamAllocation, null, null);
...
//followUpRequest 判断响应中是否有失败或者重定向(Location)标志,失败的话返回response.request,重定向的话构造新的Request返回
Request followUp = followUpRequest(response, streamAllocation.route());
if (followUp == null) {
//如果没有失败或重定向,返回response
return response;
}
...
//用新重定向的request继续while走拦截链
request = followUp;
priorResponse = response;
}
}
}
2. BridgeInterceptor 桥拦截器,比较简单,添加或者移除一些header
public Response intercept(Chain chain) throws IOException {
...
if (body != null) {
MediaType contentType = body.contentType();
if (contentType != null) {
// contentType 不空就加Content-Type这个header
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
if (contentLength != -1) {
//contentLength 不是-1就加Content-Length这个header
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
//移除Content-Length 这个header
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
}
...
//后面还有响应的header
}
3. CacheInterceptor 缓存拦截器,根据缓存策略,判断是否返回缓存数据,响应的数据是否要缓存起来
public Response intercept(Chain chain) throws IOException {
//缓存策略
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
// If we're forbidden from using the network and the cache is insufficient, fail.
//如果被禁止试用网络,同时缓存为空,则构造一个失败的Response返回
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.
// 如果不需要走网络,返回缓存数据
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
//通过继续拦截链走网络
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
}
//之后网络返回之后的策略
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
//缓存不为空而且网络返回304的情况,会使用到缓存
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()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
}
}
}
4. ConnectInterceptor 连接池
intercept只有几行代码,分析一下
public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
// 获取HttpCodec,会从连接池获取连接
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
//
public HttpCodec newStream(
...
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);
}
}
//获取连接的方法
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);
// If this is a brand new connection, we can skip the extensive health checks.
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
// Do a (potentially slow) check to confirm that the pooled connection is still good. If it
// isn't, take it out of the pool and start again.
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();
continue;
}
return candidate;
}
}
总结一下,ConnectInterceptor主要是从连接池去取连接,http请求要先3次握手才能建立连接,复用连接可以免去握手的时间。
5. 最后一个 CallServerInterceptor 发送和接收数据
//class:CallServerInterceptor
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方法 httpCodec.openResponseBody(response)
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build
//拦截链已经到底部,直接返回 response,将response回传给上一个拦截器
return response;
}
最终获取response方法 httpCodec.openResponseBody(response),
httpCodec的实现有两个,Http1Codec 和 Http2Codec,看下Http1Codec,Http2Codec原理一样
//class:Http1Codec
public ResponseBody openResponseBody(Response response) throws IOException {
...
//内部就是通过Okio.buffer去请求网络
return new RealResponseBody(contentType, -1L, Okio.buffer(newUnknownLengthSource()));
}
好了,以上就是OKHttp3的源码分析(各个拦截器点到为止,详细可以按照文中的分析流程,自己去看源码),如果文中有什么地方没有表达清楚可以跟我留言交流。
三、面试怎么答?
在面试中问到OKHttp3源码,我们可以这样回答:
OKHttp3通过拦截链的设计,让请求分成5个拦截器去处理,拦截器各司其职,扩展性非常高。拦截链是从自定义的拦截器开始,然后再到默认的5个拦截器。一般情况下我们想打印网络请求日志,所以可以自定义Log拦截器,如果要给所有请求添加Header,同样可以自定义Header拦截器。
面试官接着会问5个默认拦截器分别是什么,这时候我们就可以很轻易说出5个拦截器
1.失败重试、重定向拦截器。
2.桥拦截器:主要是添加和删除一些header
3.缓存拦截器:根据缓存策略,如果缓存可用,直接返回缓存数据。
4.连接池拦截器:连接池会缓存http链接,连接池的好处是复用连接,少了3次握手,所以请求会更快
5.真正访问网络的拦截器:内部使用okio去发请求
到这里,面试官可能问某一个拦截器的细节,例如延伸到okio的内部原理,就是NIO,跟IO之间的对比,这个自己再查下资料。
更多文章,敬请期待