一. okHttp简介
okhttp已经成为Android开发中必不可少的网络请求工具,无论在平时开发还是面试的过程中,都会有所涉及,弄清okhttp的使用流程,已经是每一个android开发者成为高级工程师的必经之路。其实其原理并不复杂,本文将分两篇来着重介绍okhttp中分发器和拦截器原理。
二. 大纲
- okHttp的优点以及请求过程
- Dispatcher的分发流程
三. okHttp的优点以及请求过程
- 首先我们来看看使用okhttp都有哪些优点:
- 支持HTTP/2并允许对同一主机的所有请求共享一个套接字
- 通过连接池,复用socket,减少请求延迟
- 默认通过gzip压缩数据
- 响应缓存,避免重复请求网络
- 请求失败自动重试主机其它ip,自动重定向
- okHttp的请求流程
使用okhttp的大致流程为:
- 创建一个OkHttpClient
- 创建一个Request对象
- 创建一个call对象,接受request
- 进行请求执行任务
- 内部通过Dispatcher分发任务
- 五大默认拦截器完成整个请求过程
- 返回结果
get请求流程:
//1.创建OkHttpClient对象
OkHttpClient okHttpClient = new OkHttpClient();
//2.创建Request对象,设置请求方式。
Request request = new Request.Builder()
.url("https://www.jianshu.com/u/f260c485f077")
.get()
.build();
//3.创建一个call对象
Call call = okHttpClient.newCall(request);
//4.同步请求
new Thread(new Runnable() {
@Override
public void run() {
Response response = call.execute();
}}).start();
//5.异步请求
call.enqueue(new Callback() {
//请求失败执行的方法
@Override
public void onFailure(Call call, IOException e) {
String err = e.getMessage().toString();
}
//请求成功执行的方法
@Override
public void onResponse(Call call, Response response) throws IOException {
inal String rtn = response.body().string();
}
});
post请求流程:
//1.创建OkHttpClient对象,设置参数
OkHttpClient okHttpClient = new OkHttpClient();
FormBody.Builder mBuild = new FormBody.Builder();
mBuild.add("key1", "vaule1")
.add("key2", "vaule2");
RequestBody requestBodyPost = mBuild.build();
//2.创建Request对象,设置请求方式。
Request request = new Request.Builder()
.url("http://www.baidu.com")
.post(requestBodyPost)
.build();
//3.创建一个call对象
Call call = okHttpClient.newCall(request);
//4.请求回调方法
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
//请求成功执行的方法
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
四. Dispatcher的分发流程
okHttp中Dispatcher的分发流程大致分为以下几步:
- 调用execute或者enqueue加入任务
- Dispatcher根据条件判断将当前任务加入队列 runningAsyncCalls(执行队列)还是readyAsyncCalls(等待队列)
- runningAsyncCalls队列调用线程池执行任务,
- runningAsyncCalls执行任务完成,根据条件获取readyAsyncCalls中任务执行
根据okhttp中Dispatcher的分发流程,我们有几个问题需要弄清楚:
Q: Dispatcher将请求分发到队列过程中,如何决定放入ready还是running?
Q: 从ready移动running的条件是什么?(如何移动,从哪里移动)
Q: Dispatcher分发器线程池的工作行为是怎样的。
带着以上三个问题,我们可以看看源码是如何实现的。
第一个问题 Dispatcher将请求分发到队列过程中,如何决定放入ready还是running
当我们要执行一个异步请求的时候,会调用okhttp的enqueue方法,其内部就会调用分发器的enqueue方法,我们直接来看分发器的enqueue方法做了什么:
okhttp的enqueue方法:
@Override
public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
// client就是OkHttpClient 通过client获取Dispatcher 调用Dispatcher的enqueue
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
Dispatcher的enqueue方法:
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
synchronized void enqueue(AsyncCall call) {
//runningAsyncCalls.size() < maxRequests(最大正在请求的数量)
//runningCallsForHost(call) < maxRequestsPerHost(同一域名最大正在请求的个数限制)
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
看到这里相信大家都能明白第一个问题,由注解可以看到,Dispatcher将任务加入队列的两个条件,当前最大请求数不超过64,同一域名最大正在请求的个数限制不超过5,同时满足这两个的情况下,想请求加入执行队列runningAsyncCalls中,并立即执行。不满足条件加入等待队列readyAsyncCalls中。
第二个问题 从ready移动running的条件是什么?(如何移动,从哪里移动)
然后我们再回过头来看看AsyncCall是个什么,我们的请求加入这个AsyncCall以后都做了什么,打开AsyncCal源码
首先可以看到RealCall implements Call ,当我们调用call.enqueue时,也就是再调用RealCall.enqueue,那么我们只要看RealCall.enqueue中做了什么即可。
@Override protected void execute() {
boolean signalledCallback = false;
try {
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 {
//请求执行完毕,调用finish();
client.dispatcher().finished(this);
}
}
如何把ready移动running,那么肯定是running中任务已经执行完成,所以我们继续看finnally中 client.dispatcher().finished(this);做了什么。
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
//将当前执行完成的请求移除,并判断是否移除成功
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
//从等待队列中获取请求加入执行队列
if (promoteCalls) promoteCalls();
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
private void promoteCalls() {
//判断正在执行的队列个数,不满足返回
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
//判断等待队列是否有请求,不满足返回
if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
//循环获取等待队列中的请求
for (Iterator<RealCall.AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
RealCall.AsyncCall call = i.next();
//判断当前call的host执行个数是否小于maxRequestsPerHost
if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
//将等待队列中的call加入到runningAsyncCalls
runningAsyncCalls.add(call);
//开始执行任务
executorService().execute(call);
}
//判断正在执行的队列个数,满足就returen,不在继续增加
if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}
以上代码逻辑,注释的很清晰。会通过相同的条件,将等待队列中的请求加入到执行队列
最后一个问题,分发器线程池的工作行为是怎样的。
看到这里,相信大家已经对okhtt的分发流程了解个大概,此时我们已经将请求加入到了队列中,而在分发执行请求的过程中,okhttp是怎么来执行这些任务的呢,还是继续来看源码,通过以上源码的分析我们可以看到Dispatcher是通过executorService().execute(call)方法执行任务,所以我们直接看executorService()方法的源码
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
熟悉线程池的同学,看到这里估计已经明白了,okhttp内部帮我们创建了一个线程池,通过线程池来执行我们的请求,这里我们主要介绍一下,线程池中的几个参数
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
corePoolSize = 0 okhttp默认不帮我们缓存空闲线程个数,只有当我们真正需要请求的时候才会帮我们创建线程
maximumPoolSize = Integer.MAX_VALUE 最大值保证了我们任务的最大并发数
keepAliveTime = 60 任务闲时最多存活时间
TimeUnit.SECONDS
new SynchronousQueue() 阻塞队列,用于工作线程都被占用情况下,储存新的任务请求
Util.threadFactory(“OkHttp Dispatcher”, false) 线程工厂
这里我们只需要这几个问题便可
- corePoolSize 为何设置为0
- maximumPoolSize 为何设置为最大值
- 阻塞队列为何只能使用SynchronousQueue,不能用ArrayBlockingQueue或者LinkedBlockingQueue。
当一个请求过来,线程池内部首先判断corePoolSize 大小为0,所以加入SynchronousQueue,而SynchronousQueue是一个无容量队列,再来判断maximumPoolSize 值,是一个Int最大值,所以任务被立马创建执行。之后来的每个请求都是这样被创建,保障了最大并发。
再来看看ArrayBlockingQueue,我们给定ArrayBlockingQueue中容量大小为1,假设此时有两个请求A,B。A请求过来,被加入ArrayBlockingQueue中,因为线程池内部判断,当前正在执行线程个数为0是,会新建线程,所以A不会加入ArrayBlockingQueue中,而是直接被创建。这时B请求再过来时,我们假设A是个耗时操作,而这时线程池中无空闲线程,B被加入ArrayBlockingQueue,且处于无限等待状态。
LinkedBlockingQueue的逻辑和ArrayBlockingQueue差不多,只不过ArrayBlockingQueue容量可以无限大。具体逻辑这里不在分析。
分发器的流程以及原理就到这里,okhttp每个版本代码可能会略有不同,本文基于okhttp3.6.0源码进行分析。下一篇我们将介绍okhttp中的拦截器是如何实现的呢。