接下来几篇文章,将会进行OkHttp的源码解析。还记得去年花了半天时间去进行Volley的源码解析,其实去年那篇文章我也说过,Volley已经算是一个过时的网络请求框架。因此,接下来,我会选择主流的网络请求框架OkHttp进行源码解析。今天,主要从OkHttp的两种请求方式开始说起,也就是同步请求和异步请求。
一、前言
作为一名优秀的程序员,一定要有分析源码的经验和能力。我们经常在面试中被问及源码,或许我们会在心里去反对这一现象,觉得框架就是拿来用的,我管他怎么实现的干嘛?我估计很多朋友跟我一样,有过这样的想法。其实,面试官之所以问及我们源码实现,其实是想从侧面考察我们的一种软件架构能力。为什么这么说?因为,但凡是GitHub上优秀的框架,都是在很多优秀的工程师的努力下,一步一步地丰富和发展起来的。当然,也经过了无数工程师的测试。可以说,这些框架从架构设计到代码实现,都是非常优秀,非常值得我们借鉴的。当然,我们更多地是去学习框架的架构设计和代码实现,并不代表我们学习了也一定能写出这么优秀的框架。
一、同步请求
在这一章节,我会通过一次简单的okhttp同步请求方式,去跟踪一下源码的实现。首先,我们看一下okhttp同步请求的简单实现。如下图所示,我在跟踪和学习源码过程中,也在我的示例代码中去做了相应的笔记:
private void syncRequest() {
//Builder():建造者模式,里面封装了okhttpclient初始化所需要的所有参数
OkHttpClient client = new OkHttpClient.Builder().readTimeout(5,TimeUnit.SECONDS).build();
//Builder():建造者模式,指明get请求方式,保存headers的信息
Request request = new Request.Builder()
.url("http://www.baidu.com")
.get()
.build(); //构建携带请求信息的Request对象
Call call = client.newCall(request);//RealCall,传入request,构建出Call对象
try {
Response response = call.execute();//dispatcher.executed():把请求加入到同步请求队列,获取到response后,移除掉同步请求。
if(response != null){
Log.d("TTTT",response.body().string());
}
} catch (IOException e) {
e.printStackTrace();
}
}
对于okhttp的同步请求,我们大致可以概括为如下四个步骤:
(1)创建okhttpclient对象:okhttpclient对象的创建,采用的是建造者模式。我们知道,建造者模式一般应用于非常复杂的对象的创建。okhttpclient对象本身就是非常复杂的,它具有很多的参数。我们跟踪源码看一下Builder(),在这里面封装了很多个参数,在这里我们不一一去解释每个参数的意思。
public Builder() {
dispatcher = new Dispatcher();
protocols = DEFAULT_PROTOCOLS;
connectionSpecs = DEFAULT_CONNECTION_SPECS;
eventListenerFactory = EventListener.factory(EventListener.NONE);
proxySelector = ProxySelector.getDefault();
if (proxySelector == null) {
proxySelector = new NullProxySelector();
}
cookieJar = CookieJar.NO_COOKIES;
socketFactory = SocketFactory.getDefault();
hostnameVerifier = OkHostnameVerifier.INSTANCE;
certificatePinner = CertificatePinner.DEFAULT;
proxyAuthenticator = Authenticator.NONE;
authenticator = Authenticator.NONE;
connectionPool = new ConnectionPool();
dns = Dns.SYSTEM;
followSslRedirects = true;
followRedirects = true;
retryOnConnectionFailure = true;
callTimeout = 0;
connectTimeout = 10_000;
readTimeout = 10_000;
writeTimeout = 10_000;
pingInterval = 0;
}
(2)构建Request对象:Request对象的创建,也是使用建造者模式。我们看一下Builder(),指明了请求的方式是get,并且创建了一个Headers对象。
public Builder() {
this.method = "GET";
this.headers = new Headers.Builder();
}
(3)创建Call对象:通过上面的client和request对象,创建一个call对象。我们跟进newCall()方法:
@Override public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}
注意,在这里,实际上调用的是RealCall.newRelCall方法,我们继续跟进去,创建了一个RealCall对象。这个RealCall对象,就是具体执行请求的call对象。
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
// Safely publish the Call instance to the EventListener.
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
(4)执行call的execute方法,我们看一下源码实现:
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
timeout.enter();
eventListener.callStart(this);
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
e = timeoutExit(e);
eventListener.callFailed(this, e);
throw e;
} finally {
client.dispatcher().finished(this);
}
}
最开头的代码,是为了同步请求只执行一次。后面关键的代码就是dispatcher.executed()方法,通过调用这个方法去执行请求,我们跟进去看一下,很简单的实现,就是把请求加到了同步请求队列。
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
在执行完同步请求获取response后,我们看到,在finally语句块中,执行了dispatcher的finished方法。我们跟进去看一下具体实现:
private <T> void finished(Deque<T> calls, T call) {
Runnable idleCallback;
synchronized (this) {
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
idleCallback = this.idleCallback;
}
boolean isRunning = promoteAndExecute();
if (!isRunning && idleCallback != null) {
idleCallback.run();
}
}
首先,就是执行了remove(call)方法,把call移除,这也是保证了同步请求只执行一次。
二、异步请求
异步请求也可以分为四个步骤,而且前三个步骤跟同步请求是一样的。因此,我们只跟踪一下第四个步骤,也就是异步请求的源码。在这里,我把异步请求的代码贴一下:
private void diffRequest() {
OkHttpClient client = new OkHttpClient.Builder().readTimeout(5,TimeUnit.SECONDS).build();
Request request = new Request.Builder()
.url("http://www.baidu.com")
.get()
.build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
}
通过上面的示例代码,大家可以发现,前三步确实和同步请求一模一样。唯一的区别就是第四步,也就是真正执行请求的方法:call.enqueue。
接下来,我们去RealCall中看一下enqueue方法的具体实现,代码如下:
@Override public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
和同步请求一样,最开始会通过synchronized去保证我们的请求只执行一次。最关键的实现仍然是最后面的dispatcher.enqueue,我们跟踪一下这个方法:
void enqueue(AsyncCall call) {
synchronized (this) {
readyAsyncCalls.add(call);
}
promoteAndExecute();
}
把call加入就绪的异步请求队列。这个和同步请求的实现不一样,同步请求是直接加入了runningSyncCalls,而异步请求是放进了readyAsyncCalls。我们看一下官方对这两个队列的注释:
/** Ready async calls in the order they'll be run. */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
(1)readyAsyncCalls:意思很明确,是他们"将会"被按顺序执行。因此,他们只是被加进了一个就绪态的队列,而不是立刻被执行。
(2)runningAsyncCalls:正在执行的异步请求队列。也就是说该队列里的请求是正在被执行或者被取消掉的没执行完的异步请求。
(3)runningSyncCalls:正在执行的同步请求队列。也就是说该队列里的请求是正在被执行或者被取消掉的没执行完的同步请求。
可能有些朋友早就有这么个疑问:同步请求和异步请求的区别是啥?其实,通过二者使用方法的区别,我们也能归纳出来。同步请求是直接加入正在执行队列,也就是立刻执行。而异步请求是被加入到就绪队列,是要过一段时间才会加到正在执行队列的。因此,异步请求的方法,有失败和成功的异步回调。
在把异步请求添加到就绪队列后,后面还有一个promoteAndExecute方法,这是真正执行异步请求的方法,我们还是跟进去:
private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));
List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.
i.remove();
executableCalls.add(asyncCall);
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}
for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService());
}
return isRunning;
}
这个方法,首选他会去遍历就绪队列中的call,在遍历的过程中,会去做两个判断:(1)如果正在执行队列的请求数超过了最大的请求数容量则break,如果一个Host的请求比允许的每个host的请求容量小,那么将会continue,并且把异步请求添加到两个队列:可执行队列和正在执行异步请求队列。后面,去遍历可执行队列中的请求,并且通过executeOn方法,去执行异步请求,跟一下executeOn方法:
void executeOn(ExecutorService executorService) {
assert (!Thread.holdsLock(client.dispatcher()));
boolean success = false;
try {
executorService.execute(this);
success = true;
} catch (RejectedExecutionException e) {
InterruptedIOException ioException = new InterruptedIOException("executor rejected");
ioException.initCause(e);
eventListener.callFailed(RealCall.this, ioException);
responseCallback.onFailure(RealCall.this, ioException);
} finally {
if (!success) {
client.dispatcher().finished(this); // This call is no longer running!
}
}
}
这是RealCall的方法,我们看到了executorService,这是一个线程池。也就是说,RealCall通过线程池去执行请求。线程池的原理我们在这里不再多说,我们看一下此线程池的具体参数,跟踪源码:
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;
}
corePoolSize为0,maxPoolSize为Integer的最大值,keepAliveTime为60秒。在这里,有一个很有意思的设定。为什么说有意思呢,因为corePoolSize设置为0。熟悉线程池的朋友应该知道,当线程池中执行完所有的任务,并且在keepAliveTime的时间里没有来新的任务时,如果核心线程数不为0,那么线程池将无法关闭。如果设置核心线程数为0,那么到了60秒之后,线程池自动关闭。
最后,总结一下这篇文章的内容。通过同步请求和异步请求的实现,我们跟踪了源码的实现,并且通过源码的实现总结出了同步请求和异步请求的区别。后面的文章,将会对okhttp另外的一些比较关键的地方进行源码分析。例如,我们在本章中多次提到的dispatcher。