前言
本人待在一家普通公司很多年了。做着一些非常普通的项目。一直知道IT行业,不是在学习的路上,就是在被淘汰的路上。有一颗进大厂的心,想寻找有梦想的兄弟,一起进阶学习,让我们的孤独少一些。这是我14-19年的历程,也是开源控件ShadowLayout(star2.2k)作者
19后我觉得T型发展很重要,期间学会了web开发,后端入门,以及简单学习了unity。但最近收到很多大厂的内推,和一些19年的小伙伴都进大厂了给了我很大触发,为什么!?为什么我差一步要放弃呢。从9月开始,我回头认真收拾Android知识,及用最通俗的白话文去理解他们,并整理一份全面的面试质料。这次我一定不会输。如果你和我有共同目标,一起放手去干吧。这是我的Android交流群:209010674。如果你需要的话。
一、OkHttp简单介绍
电视剧里一般能成为大侠,都从一本简单的武功秘籍开始。那我们先来看段简单的代码 一个简单OkHttp的get请求。
//创建OkHttpClient对象
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10,TimeUnit.SECONDS)
//添加一个自定义的网络日志拦截器
.addInterceptor(new HttpLogInterceptor())
.build();
//创建网络请求对象Request对象及配置参数
Request request = new Request.Builder()
.url("url")
.header("headKey","headValue")
.get()
.build();
//进行网络请求
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {}
@Override
public void onResponse(Call call, Response response) throws IOException {}
});
想必这些大家都非常的清楚,那怎么让这些印在脑海里呢,还不用死记硬背呢,接下来看看下面这张图(这里借用了别的博主一张图。觉得他比喻成快递行业很形象,接下来会以我的思路去讲解):
这里用退货的比喻加上快递员形容的很形象。从这里再加上OkHttp的简单请求,我们很容知道有哪些类:
- OkHttpClient:进入快递点,构建OkHttpClient对象
- Request:网络请求参数配置,就像寄快递填快递单号,才知道用什么快递,寄送到哪里去
- Call:快递小哥的角色
- Callback:网络回调,相当于,你告诉了他电话号码,等发货成功或丢失都会回调告诉你
- enqueue:使用异步方法进行网络请求。则相当于,快递小哥进行了货物运送
二、OkHttpClient
看到OkHttpClient.Builder就知道是建筑者模式,也有人称之为是生成器模式。看下源码就知道了,我知道大家都不喜欢看代码,那就来张图片吧(参数太多,就截前面几个。反正我也知道大家不想看)
参数实在太多,这也是为什么用建造者模式的原因。比如我们定义一个方法,方法有10个参数,那么我们调用这个方法时,要严格按照方法里参数的类型和顺序去调用,真的比较麻烦。使用建造者模式,要传参只需要点一下,不点的话就使用默认值。
这里介绍几个参数:
- Dispatcher:调度器
- List interceptors:拦截器
- List networkInterceptors:网络拦截器
- int readTimeout:读取的超时时间
先别管他们是干嘛的,后面会介绍。这里只要有个概念就行了。
三、Request
看到 Request.Builder也是建筑者模式,从上面简单的get请求,就知道他是用来网络请求的配置参数,那么接下来介绍几个主要参数:
- .url(“url”):请求的网络url
- .header(“key”,“value”):头部参数,一般登录者的token都会放在这里
- .get():确定是get请求
题外话:
当你想自己封装一个OkHttp网络请求,你越把细节搞清楚,越能封装的好用,这里稍微提下header
- 我们经常会提到断点续传和下载,其实就是利用了header
// 通过以下设置,这里currentLength就是你已经下载的文件length
.header("RANGE", "bytes=" + currentLength + "-");
// 然后在下载文件流里使用true,就能达到断点下载了。不使用true就是从新下载。
fos = new FileOutputStream(file, true);
- 网络缓存
//1、
// 有网络的时候的在线缓存
// temp 在线缓存时间,
// 场景:比如并发量大的项目,而且是首页banner不长换的,那么获取banner这个接口的数据
// 会被缓存下来。比如temp = 3600,那么在接下1小时都会读取缓存数据
.header("Cache-Control", "public, max-age=" + temp)
//2、
// 无网络时的离线缓存
// 场景:很多新闻类的app,当没有网络打开时,还是会显示上一次打开的数据
.header("Cache-Control", "public, only-if-cached, max-stale=" + temp)
这里提到这吧,毕竟是源码分析,跑题了。
四、Call
call 就是我们之前讲的“快递小哥”,看源码Call是个接口,我们从newCall点进去,其实是生成了Call接口的实现类RealCall
public Call newCall(Request request) {
return new RealCall(this, request, false);
}
看看Call接口,我们就知道它的具体作用,老样子,上图片。看到图片后,我们就知道,它其实就是处理网络请求的方法和网络请求的一些状态。
五、异步发送请求 RealCall.enqueue
5.1、call.enqueue
这里就是异步请求,相信大家都知道这段代码啥意思
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
点进enqueue,咋一看,并不复杂
public void enqueue(Callback responseCallback) {
synchronized(this) {
//executed:中文翻译为已执行的。从代码可以看出这里是防止一次调用进行多次请求
if (this.executed) {
throw new IllegalStateException("Already Executed");
}
this.executed = true;
}
this.captureCallStackTrace();
this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}
this.captureCallStackTrace();
capture:捕捉
StackTrace:堆栈踪迹
从字面上大概能猜想到这个方法是捕捉请求的堆栈踪迹的
继续点进去
private void captureCallStackTrace() {
Object callStackTrace = Platform.get().getStackTraceForCloseable("response.body().close()");
this.retryAndFollowUpInterceptor.setCallStackTrace(callStackTrace);
}
从代码可以看到,他获取当前接口的堆栈踪迹对象放进了retryAndFollowUpInterceptor,拦截器里。那么接下来我们看看retryAndFollowUpInterceptor拦截器。
5.2、RetryAndFollowUpInterceptor
我们都知道okHttp有拦截器,这里则是使用设计模式里的责任链模式。责任链模式:是一种处理请求的模式,让多个处理器都有机会处理该请求。在这里拦截器只处理与自己相关的业务逻辑。
题外话: 要实现拦截器只要继承Interceptor类,并重写intercept(Chain chain)拦截方法就行了,最后在okHttpClient.addInterceptor()下就好了。比如开发中,登录成功后,会有登录者token,此时就可以使用拦截器,为以后每个接口头部参数添加token。再比如,开发测试中,我们可以打印每一个网络请求的详细情况。比如实现一个网络日志打印拦截器,伪代码如下:
@Override
public synchronized okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long startTime = SystemClock.elapsedRealtime();
okhttp3.Response response = chain.proceed(chain.request());
long endTime = SystemClock.elapsedRealtime();
long duration = endTime - startTime;
//url
String url = request.url().toString();
log("┌───────Request Start────────────────────────────");
log("请求方式 -->>" + request.method() + ": " + url);
log("请求方式 -->>"+"Time:" + duration + " ms");
//headers
Headers headers = request.headers();
if (null != headers) {
for (int i = 0, count = headers.size(); i < count; i++) {
if (!headerIgnoreMap.containsKey(headers.name(i))) {
log("请求头部 -->>" + headers.name(i) + ": " + headers.value(i));
}
}
}
//param
RequestBody requestBody = request.body();
String paramString = readRequestParamString(requestBody);
if (!TextUtils.isEmpty(paramString)) {
log("请求参数 -->>" + paramString);
}
//response
ResponseBody responseBody = response.body();
String responseString = "";
if (null != responseBody) {
if (isPlainText(responseBody.contentType())) {
responseString = readContent(response);
} else {
responseString = "other-type=" + responseBody.contentType();
}
}
log("请求返回 -->>" + responseString);
log("└───────Request End─────────────────────────────" + "\n" + "-");
return response;
}
以上拦截器无需看太仔细,你只要知道在拦截方法里,你可以获得Request和ResponseBody,那还有什么不能做的呢。哎呀,又啰嗦了。跑题了。回到我们的主题上。
RetryAndFollowUpInterceptor就是负责重试,和请求重定向的的拦截器,在这里用到StreamAllocation,主要是获取网络连接的RealConnection,如果需要重定向就release,关闭套接字等资源,开启重试和重定向的“新”的请求。我们继续往里看其重写的intercept方法:
public Response intercept(Chain chain) throws IOException {
//获取请求体
Request request = chain.request();
//这个StreamAllocation是个核心类。这里简单介绍,后面会讲
//参数1:OkHttpClient 的连接池
//参数2:请求地址
//参数3:堆栈追踪的对象
//StreamAllocation的作用:
//1、处理不同Connection的复用功能,协议流的可复用性和ConnectionPool连接池的处理
//2、给请求HttpCodec流处理输入输出
//3、StreamAllocation关联了一个Call请求的所有周期,判断Call请求的Connection是否被占用,其他Call能不能使用该Connection
this.streamAllocation = new StreamAllocation(this.client.connectionPool(), this.createAddress(request.url()), this.callStackTrace);
//重定向次数
int followUpCount = 0;
Response priorResponse = null;
//这里的条件一直为true,除非调用
//call.cancle(),此方法里调用了-->retryAndFollowUpInterceptor.cancel()-->canceled = true(进而条件为false,不会往下进行)。这里还调用了streamAllocation.cancel();代码继续追踪下去,最终是调用了-->codecToCancel.cancel();其实就是调用了RealConnection的取消方法
while(!this.canceled) {
Response response = null;
boolean releaseConnection = true;
try {
//把网络返回响应体Response拦截下来
response = ((RealInterceptorChain)chain).proceed(request, this.streamAllocation, (HttpCodec)null, (RealConnection)null);
//释放Connection的标识,在try catch后面的finally里,在这里是不释放
releaseConnection = false;
} catch (RouteException var13) {
//recover的其实就是判断是否需要重定向,比如你把接口都写错了,就不需要了。可以理解为里面是判断这个错造成的原因
if (!this.recover(var13.getLastConnectException(), false, request)) {
throw var13.getLastConnectException();
}
releaseConnection = false;
continue;
} catch (IOException var14) {
boolean requestSendStarted = !(var14 instanceof ConnectionShutdownException);
if (!this.recover(var14, requestSendStarted, request)) {
throw var14;
}
releaseConnection = false;
continue;
} finally {
if (releaseConnection) {
this.streamAllocation.streamFailed((IOException)null);
this.streamAllocation.release();
}
}
//第一次循环priorResponse 必定为空。那么response则是try{}里获取的放入了streamAllocation的response
if (priorResponse != null) {
//如果是第2次及以后到达这里的,则是以先前的priorResponse生成新的response。这里可以理解为就是要重定向的response。其实在这个方法的最后,是赋值了的priorResponse = response;所以还是第一次放入streamAllocation的response,也就是说这里只要条件达到,那么重定向会一直循环下去
response = response.newBuilder().priorResponse(priorResponse.newBuilder().body((ResponseBody)null).build()).build();
}
//followUpRequest下方有其实就是判断是否有重连或者重定向条件的response,比如这里获取code码等条件进行判断
Request followUp = this.followUpRequest(response);
//对返回需要重试或重定向的Request 进行判断,如果为空,说明这次请求是成功的,那么返回当前的response
if (followUp == null) {
if (!this.forWebSocket) {
this.streamAllocation.release();
}
return response;
}
//关闭当前response资源,内部其实调用的是Util.closeQuietly(this.source());source其实就是BufferedSource,看到这些我们就可以联想到流和字节资源
Util.closeQuietly(response.body());
//每一次进行重试或重定向都++
++followUpCount;
//当次数超过20次时,失败
if (followUpCount > 20) {
this.streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
//以下都是达不到重试或重定向的条件,那么我们把错误抛出去
if (followUp.body() instanceof UnrepeatableRequestBody) {
this.streamAllocation.release();
throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
}
if (!this.sameConnection(response, followUp.url())) {
this.streamAllocation.release();
this.streamAllocation = new StreamAllocation(this.client.connectionPool(), this.createAddress(followUp.url()), this.callStackTrace);
} else if (this.streamAllocation.codec() != null) {
throw new IllegalStateException("Closing the body of " + response + " didn't close its backing stream. Bad interceptor?");
}
request = followUp;
priorResponse = response;
}
this.streamAllocation.release();
throw new IOException("Canceled");
}
followUpRequest方法伪代码:
private Request followUpRequest(Response userResponse) throws IOException {
if (userResponse == null) {
throw new IllegalStateException();
} else {
...
switch(responseCode) {
case 307:
case 308:
...
return null;
case 300:
case 301:
case 302:
case 303:
if (!this.client.followRedirects()) {
return null;
} else {
String location = userResponse.header("Location");
if (location == null) {
return null;
} else {
...
return requestBuilder.url(url).build();
}
}
}
case 401:
return this.client.authenticator().authenticate(route, userResponse);
case 407:
...
return this.client.proxyAuthenticator().authenticate(route, userResponse);
case 408:
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
return null;
}
return userResponse.request();
default:
return null;
}
}
}
5.3、StreamAllocation
StreamAllocation是在重试和重定向拦截器RetryAndFollowUpInterceptor里初始化的。其实这个类主要是在拦截器ConnectInterceptor发起真正网络请求时用到,大致我们可以看成网络请求优化,
先在这里了解这个类更容易理解全文。在了解这个类的时候,我们来看看题外话(兄弟们,这是我查阅大量资料找来的题外话)。这个可是划重点的知识点,(指黑板)
- http2.0 使用 多路复用 的技术,多个 stream 可以共用一个 socket 连接。每个 tcp连接都是通过一个 socket 来完成的,socket 对应一个 host 和 port,如果有多个stream(即多个 Request) 都是连接在一个 host 和 port上,那么它们就可以共同使用同一个 socket ,这样做的好处就是 可以减少TCP的一个三次握手的时间。
在OkHttp里负责连接的就是RealConnection,上文也说道了,我们取消请求可以用call.cancle()。
其实最终就是调用了StreamAllocation里的cancle(),也就是调用了realConnection.cancle()
- Http通信执行网络请求Call, 需要在连接上建立新的流Stream(OkHttp里面的流是HttpCodec)执行,我们将StreamAllocation看做是桥梁,作用是为一次请求寻找连接并建立流,进而完成通信
那么接下来我们来看看StreamAllocation的伪代码。
public final class StreamAllocation {
public final Address address;
private Route route;
private final ConnectionPool connectionPool;
private final Object callStackTrace;
private final RouteSelector routeSelector;
private int refusedStreamCount;
private RealConnection connection;
private boolean released;
private boolean canceled;
private HttpCodec codec;
public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
...
RealConnection resultConnection = this.findHealthyConnection(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
//建立OkHttp连接流,点进去其实new的是一个Http2Codec
HttpCodec resultCodec = resultConnection.newCodec(client, this);
}
//寻找一个健康的RealConnection,
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) throws IOException {
while(true) {
...
//findHealthyConnection其实调用了findConnection,只不过判断了是否健康
//判断RealConnection candidate是否健康.isHealthy点进去也就是判断当前socket是否自愿关闭,等健康因素
if (candidate.isHealthy(doExtensiveHealthChecks)) {
return candidate;
}
//如果RealConnection是不健康的,那么把它当前的socket关闭资源
this.noNewStreams();
}
}
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) throws IOException {
Route selectedRoute;
synchronized(this.connectionPool) {
if (this.released) {
throw new IllegalStateException("released");
}
if (this.codec != null) {
throw new IllegalStateException("codec != null");
}
if (this.canceled) {
throw new IOException("Canceled");
}
RealConnection allocatedConnection = this.connection;
//如果当前赋值的this.connection不为null;且安全的话,那么返回当前RealConnection
//this.connection是全局变量,第一次这里肯定为空。
if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
return allocatedConnection;
}
//1、第一次获取Connection
//从连接池中获取一个可用连接。这里的路由Route传的是null,表示从当前已解析过的连接池中寻找。
Internal.instance.get(this.connectionPool, this.address, this, (Route)null);
//Internal.instance.get()最终调用的是OkHttpClient的pool.get(),即连接池ConnectionPool里的get(),代码如下:
// @Nullable RealConnection get(Address address, okhttp3.internal.connection.StreamAllocation streamAllocation, Route route) {
// ...
// for (RealConnection connection : connections) {
// if (connection.isEligible(address, route)) {
// 这里把connection传给了streamAllocation,我们可以当成就是赋值给我们的this.connection
// streamAllocation.acquire(connection);
// return connection;
// }
// }
// return null;
// }
if (this.connection != null) {
return this.connection;
}
selectedRoute = this.route;
}
if (selectedRoute == null) {
selectedRoute = this.routeSelector.next();
}
RealConnection result;
synchronized(this.connectionPool) {
if (this.canceled) {
throw new IOException("Canceled");
}
//2、第二次获取Connection
//这里传入了Router,先看下Router的概念
//Router路由:来确定具体要怎么连接。一个Router包含proxy代理,ip地址和端口port。而同样的端口和同样的代理类型下有不同ip的多个Router组合
//才结合http2.0的多路复用的概念。这里一直循环this.routeSelector.next()寻找一个可复用的Connection
Internal.instance.get(this.connectionPool, this.address, this, selectedRoute);
if (this.connection != null) {
return this.connection;
}
this.route = selectedRoute;
this.refusedStreamCount = 0;
//3、第三次获取Connection
//上面2种方式都获取不到,说明当前连接池没有可复用的,多路路由里也找不到复用的,那么我们创建新的Connection
result = new RealConnection(this.connectionPool, selectedRoute);
this.acquire(result);
}
result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
this.routeDatabase().connected(result.route());
Socket socket = null;
synchronized(this.connectionPool) {
//把新建的Connecttion放入到连接池中
Internal.instance.put(this.connectionPool, result);
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(this.connectionPool, this.address, this);
result = this.connection;
}
}
Util.closeQuietly(socket);
return result;
}
...
public void noNewStreams() {
Socket socket;
synchronized(this.connectionPool) {
socket = this.deallocate(true, false, false);
}
//这里就是之前如果不是健康连接,找到连接套接字,并关闭资源
Util.closeQuietly(socket);
}
...
}
5.4、dispatcher
说完了 this.captureCallStackTrace();再来看看this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));还记得那些简单代码吗?如下:
public void enqueue(Callback responseCallback) {
synchronized(this) {
if (this.executed) {
throw new IllegalStateException("Already Executed");
}
this.executed = true;
}
this.captureCallStackTrace();
this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}
首先this.client.dispatcher()获取的是Dispatcher调度器的对象。用来调度,管理这些请求的。来看看Dispatcher,伪代码如下:
public final class Dispatcher {
//最大请求数,意思一家快递公司有64辆车,如果还需要车,就只能等待其他车空闲了
private int maxRequests = 64;
//一个主机最大允许请求数。意思前往同一城市运送快递的车辆为5量,如果还有快递要运往,只能等5量中有空闲
private int maxRequestsPerHost = 5;
//线程池,懒加载即被调用时才创建
private @Nullable ExecutorService executorService;
//准备队列,也就是超过了maxRequests 和 maxRequestsPerHost 的请求将被添加到这里
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
//异步队列,也就是没超过最大请求数,将放在这里
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
//同步队列,没超过最大请求数,将添加在这里
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
public Dispatcher(ExecutorService executorService) {
this.executorService = executorService;
}
public Dispatcher() {
}
public synchronized ExecutorService executorService() {
if (executorService == null) {
//创建一个线程池,60秒是keepAliveTime。即线程干完活不会立即消失,有活干的时候会复用还存活的线程,性能优化
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
...
}
5.5、this.client.dispatcher().enqueue()
synchronized void enqueue(AsyncCall call) {
//可以看到在比较了最大请求数之后,如果超过最大请求数,加入到readyAsyncCalls里等待请求,否则加入到runningAsyncCalls进行请求。
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
接下来看看readyAsyncCalls在什么地方被执行呢?点开源码:
private void promoteCalls() {
//如果请求数大于等于最大请求数,说明没有空闲。那readyAsyncCalls必须继续等待
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
//如果readyAsyncCalls自身为空,那么不需要执行,直接返回
if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();
if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
runningAsyncCalls.add(call);
//最终在这里被请求。
executorService().execute(call);
}
if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}
那么看到这里你会想promoteCalls()方法在什么时候去执行呢?让我们试想一下在什么条件下会触发他请求呢。那肯定得满足条件的时候啊,哈哈哈
- 条件1:请求数不超过最大请求数。那我们很容易想到修改最大请求数maxRequests或maxRequestsPerHost
- 条件2:等到了空闲的请求路线,即runningAsyncCalls里有请求被执行完毕了
点开源码也确实是在3个方法里被执行了,分别是setMaxRequests()、setMaxRequestsPerHost()和finished(AsyncCall call)
5.6、executorService().execute(call)
从5.5的源码我们知道,这里的call是AsyncCall,是RealCall里的内部类,初始化属性也都是RealCall里的属性,代码如下,他其实是个被命名的Runnable(意思会给这个线程setThreadName):
final class AsyncCall extends NamedRunnable {
//请求结果的Callback回调
private final Callback responseCallback;
...
//请求地址
String host() {
return originalRequest.url().host();
}
//请求体
Request request() {
return originalRequest;
}
RealCall get() {
return RealCall.this;
}
}
从5.4可以看到executorService()生成的是ThreadPoolExecutor线程池,并通过execute方法加入到workQueue工作队列中,并执行。这里是线程池的概念就略过了。直接看execute()方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
因为我们知道AsyncCall 就是runnable,其实执行的就是他里面的run方法。通过父类NamedRunnable我们知道。其实现的是NamedRunnable里定义的抽象方法execute(),并由AsyncCall 去实现,如下:
final class AsyncCall extends NamedRunnable {
...
@Override protected void execute() {
boolean signalledCallback = false;
try {
//通过getResponseWithInterceptorChain()获得了最终请求返回的Response
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
responseCallback.onFailure(RealCall.this, e);
}
} finally {
//不管接口成功或失败,都会将这个请求Call移除
client.dispatcher().finished(this);
}
}
}
我们看到这里都是通过getResponseWithInterceptorChain()得到Response的。那么我们看看这个方法都做了什么。
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));
//最后带上请求数据,通过RealInterceptorChain以及这些拦截器的作用,最终网络请求返回了我们的Response
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
return chain.proceed(originalRequest);
}
致此,异步请求就将完了。以上讲解的是异步请求。用的方法是call.enqueue()。同步请求是怎么样的呢?我相信你看懂了上面写的我只要放2段代码,你就焕然大悟了
//同步请求够简单吧。再看看RealCall
Response execute = call.execute();
RealCall的execute();同步的实现方法基本和异步的AsyncCall里的实现方法一模一样
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} finally {
client.dispatcher().finished(this);
}
}
题外话又如约而至了,题外话: 看到了调度器的知识,如果我们自己封装一个OkHttp该怎么取消网络请求呢?
这里放上一段我之前封装的取消网络请求方法:
//tag取消网络请求
public void cancleOkhttpTag(String tag) {
//封装,当然okHttpClient要使用单例咯,获取调度器。
Dispatcher dispatcher = okHttpClient.dispatcher();
synchronized (dispatcher) {
//dispatcher.queuedCalls()获得的就是readyAsyncCalls
for (Call call : dispatcher.queuedCalls()) {
//获取到Request,当然在请求网络的时候,okHttp带了个点tag的参数。这个时候传入的tag也就和那个tag对比
if (tag.equals(call.request().tag())) {
call.cancel();
}
}
//看runningCalls也知道获取到什么了吧
for (Call call : dispatcher.runningCalls()) {
if (tag.equals(call.request().tag())) {
call.cancel();
}
}
}
}
六、拦截器
前面为了源码分析更清楚已经把RetryAndFollowUpInterceptor放在5.2小结讲过了。这里我们看到了getResponseWithInterceptorChain()方法里,有那么多拦截器。接下来我们一个一个看吧:
6.1、BridgeInterceptor
其实我们运用OkHttp进行网络请求,是作者在内部高度封装,简化我们的操作。实现了真正的网络请求。先来简单说下桥接拦截器的概念和作用:
- 把应用请求转换成网络请求(就是添加网络请求必要的响应头,在web上按F12,选中network监听网络可以看到网络请求带很多头部信息)
- 把网络响应转换成应用响应(就是转换成对用户友好的响应,支持gzip压缩也有提高性能的意思)
- 因为Cookie也在头部信息里,系统是个空实现,如果自定义cookieJar,这里也算成一个作用吧分辨用户信息
//为什么说他是空实现呢,首先BridgeInterceptor里有段代码是:
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
//我们看看系统的cookies得到的是什么呢?首先OkHttpClient里的默认值是cookieJar = CookieJar.NO_COOKIES;
//再来看CookieJar接口loadForRequest默认实现是
CookieJar NO_COOKIES = new CookieJar() {
@Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
}
@Override public List<Cookie> loadForRequest(HttpUrl url) {
//没看错这里就是一个空数组
return Collections.emptyList();
}
};
现在来看看我们的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) {
//添加头部信息Content-Type,请求类型
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
//对contentLength进行判断用哪种请求解析方式,Content-Length或Transfer-Encoding
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));
}
//保持请求长连接。这里的意思是tcp连接要经过三次握手,四次挥手。长连接可以理解为多次网络请求可以减少握手和挥手的次数。不但能减少耗时而且还能提高性能
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}
//接收gzip压缩。就像我们传大型文件,压缩完是不是传递快些,占用的资源少
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()) {
//Cookie:分辨用户信息
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);
responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
}
//经过以上操作最终生成对用户友好的请求响应
return responseBuilder.build();
}
...
}
6.2、ConnectInterceptor
这个拦截器非常简洁,主要是调用StreamAllocation,这个类我们已经在“5.3、StreamAllocation”讲过了,而大部分工作都让StreamAllocation给做了
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,也是在RetryAndFollowUpInterceptor中初始化的
StreamAllocation streamAllocation = realChain.streamAllocation();
boolean doExtensiveHealthChecks = !request.method().equals("GET");
//调用streamAllocation的newStream()方法。大家可以去看下5.3的StreamAllocation,这个方法就是找findHealthConnect,
//就是多路复用,3次寻找,有复用的Connection就复用,没有的话就新建,并且把新建的加入连接池ConnectionPool中
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
//交给下个拦截器使用
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
题外话(重点): 可能这里大家会有点懵逼,包括我自己,为什么RetryAndFollowUpInterceptor中已经进行了网络请求,ConnectInterceptor怎么还来一遍,为什么RetryAndFollowUpInterceptor里的网络请求不多路复用呢?别急,我们直接看一张网络请求拦截器的图:
从这张图看,我们来总结下:
首先是从左边开始进行网络请求的方向
- 首先是进入到RetryAndFollowUpInterceptor拦截器,但是我们看源码,知道这个拦截器是等Response回来之后才知道是重试还是重定向,在请求的时候是不会走这里的拦截的
- 其次进入BridgeInterceptor拦截器,把我们的用户请求,添加头部信息,变成真正的网络请求
- CacheInterceptor拦截器后面会讲,这里先理清这个逻辑
- 最后进入到ConnectInterceptor拦截器里,调用StreamAllocation的newstream,寻找健康的RealConnection。进行真正的网络请求
到这里网络请求就结束了,来看看右边的网络响应Response返回的方向
- 首先是在ConnectInterceptor返回网络请求的Response给下一级拦截器,这里说的下一级,肯定就是CacheInterceptor了。
- 如果配置了缓存走完后就到了BridgeInterceptor里,将网络返回的Response处理后返回友好的用户Response给用户。
- 最后到了Response到了RetryAndFollowUpInterceptor拦截器里,如果请求是成功的,那么就会直接返回Response,无限循环停止。否知就会判断是否要重试或者重定向,满足条件后,再次进行网络请求。
大概就是这样的流程了。本小节的题外话到此为止了,是不是有种没听够的感觉
6.3、CacheInterceptor
之前在"三、Request"部分讲了我们通过配置头部参数head可以达到在线缓存和离线缓存2种方式,接下来来看看我们的缓存拦截器,伪代码如下:
public final class CacheInterceptor implements Interceptor {
//使用DisLruCache封装的一个Cache.Cache在OkHttp源码的okhttp3包下,不在cache包下
final InternalCache cache;
public CacheInterceptor(InternalCache cache) {
this.cache = cache;
}
@Override public Response intercept(Chain chain) throws IOException {
//用当前请求去获取缓存的Response,点进去可以发现其实用的是request.url作为key获取的
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);
}
//如果缓存Response不为null,但是缓存策略的结果是缓存不适用,那么关闭缓存
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body());
}
//如果网络和缓存都为null,那么我们直接返回code504,超时
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();
}
//如果网络为null,缓存有效的话,那使用缓存
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
//如果我们在I/O其他地方崩溃不要泄露缓存,及时关闭它
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
if (cacheResponse != null) {
//返回HTTP_NOT_MODIFIED304的意思是服务器判断此接口数据是否更新过,没更新过的话,那么使用缓存
//否则关闭缓存
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;
} 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) {
// The cache cannot be written.
}
}
}
return response;
}
...
}
这里大致总结以下:
- 读取候选缓存。
- 根据候选缓存创建缓存策略。
- 根据缓存策略,如果不进行网络请求,而且没有缓存数据时,报错返回错误码 504。
- 根据缓存策略,如果不进行网络请求,缓存数据可用,则直接返回缓存数据。
- 缓存无效,则继续执行网络请求。
- 通过服务端校验后,缓存数据可以使用(返回 304),则直接返回缓存数据,并且更新缓存。
- 读取网络结果,构造 response,对数据进行缓存。
这里其实有很多知识点,不懂的地方我们可以去好好学习下;DiskLruCache完全解析,可以看这篇文章。说了缓存拦截器这么多,为什么我们在使用OkHttp的时候都没有使用过缓存呢,因为这里和CookieJar也是个空实现,看下面代码:
//首先在RealCall里,添加了
interceptors.add(new CacheInterceptor(client.internalCache()));
//然后来到OkHttpClient里:
InternalCache internalCache() {
//这个cache是在new OkHttpClient().cache() 传入的,显然,用的时候大家都没定义过
//cache为空的话,最终就会用internalCache,其实这个变量就是Builder里传入的。没传默认为空
return cache != null ? cache.internalCache : internalCache;
}
//都为空的情况下,来看看缓存拦截器的一段代码,client.internalCache()为null,那么下方的cache就是null
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
又到了题外话的环节了:
还记得我们之前提过的在线缓存和离线缓存吗?代码如下
//在设置之前我们还要提供一个缓存文件就是初始化我们的OkHttpClient的时候使用.cache()
//这是我封装EasyOk时的代码
//设置缓存文件路径,和文件大小
.cache(new Cache(new File(Environment.getExternalStorageDirectory() + "/okhttp_cache/"), 50 * 1024 * 1024))
//1、
// 有网络的时候的在线缓存
// temp 在线缓存时间,
// 场景:比如并发量大的项目,而且是首页banner不长换的,那么获取banner这个接口的数据
// 会被缓存下来。比如temp = 3600,那么在接下1小时都会读取缓存数据
.header("Cache-Control", "public, max-age=" + temp)
//2、
// 无网络时的离线缓存
// 场景:很多新闻类的app,当没有网络打开时,还是会显示上一次打开的数据
.header("Cache-Control", "public, only-if-cached, max-stale=" + temp)
注意我们使用头部文件去设置了缓存,其实不管是Request还是Response都使用了一个类,CacheControl。这个类里有一个方法,是获取头部信息Cache-Control里的设置的
//代码还是比较长,还是伪代码吧
public static CacheControl parse(Headers headers) {}
知道这些以后,除了在头部信息设置之外,我们还可以利用拦截器,获取到Request进行设置,看段代码就明白了了
Request request = new Request.Builder()
.cacheControl(new CacheControl.Builder()
.onlyIfCached()
.build())
.url("http://publicobject.com/helloworld.txt")
.build();
还记得CacheIntereptor里的CacheStrategy吗,即是缓存策略。OkHttp有2种缓存策略:
强制缓存: 需要服务器参与,服务器返回时间标识,如果时间不失效,则利用缓存。强制缓存有2个标识
- Expires:Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。到期时间是服务端生成的,客户端和服务端的时间可能有误差。
- Cache-Control:Expires有个时间校验的问题,所有HTTP1.1采用Cache-Control替代Expires。
对比缓存: 需要服务器参与,服务器返回资源是否被修改,未修改返回code304使用缓存,否则不使用。对比缓存也有2个标识
- Last-Modified:表示资源上次修改的时间。
- ETag:是资源文件的一种标识码,当客户端发送第一次请求时,服务端会返回当前资源的标识码。如果再次请求,不同标识资源被修改
在这里我们大致对缓存策略有个了解吧
七、ConnectionPool连接池
我们都知道网络请求要经过3次握手和4次挥手,为了加快网络请求访问速度和提高性能,这个时候就要用到Http2.0的多路复用了。我们可以在一个请求head里添加keepalive,从而这次请求的RealConnection可以被复用并加入到ConnectionPool连接池中。
//ConnectionPool是用Deque保存RealConnection的
private final Deque<RealConnection> connections = new ArrayDeque<>();
//Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间),连接池有ConectionPool实现,对连接进行回收和管理。
public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);
}
我们来看下他是怎么加入到Deque里的
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
//如果清除闲置的线程还没走完则不运行这段代码,否则主动调用一次清除闲置线程
executor.execute(cleanupRunnable);
}
//加入到Deque里
connections.add(connection);
}
好了到这里大概就完结了。不会再有题外话了。相信,你从头看下来会跟我有同样的感觉,不再害怕面试官问你OkHttp源码了。感谢你的浏览,和我一起并肩作战吧!