Preface
Recently, it is a pit in the source code. What always wants to hold down the ctrl to see the source code. During this period of time, we studied the source code of okhttp, and found that the source code of okhttp is not easy to gnaw down in a few days, so let's go step by step.
This blog mainly analyses the execution process of the source code from the overall process of okhttp, has a general understanding of the source code of okhttp, and shows the design idea of okhttp from the overall perspective.
Analysis
1.OkHttpClient
Now that it's process analysis, everyone who has used okhttp knows that first you need to initialize an OkHttpClient object. OkHttp supports two constructions
1. Default mode
public OkHttpClient() {
this(new Builder());
}
As you can see, there is no need to configure any parameters, that is to say, the basic parameters are default and the following constructors are called.
OkHttpClient(Builder builder) {...}
2.builder mode, through the Builder configuration parameters, and finally through the builder() method to return an OkHttpClient instance.
public OkHttpClient build() {
return new OkHttpClient(this);
}
OkHttpClient basically finished this analysis, and the details are basically the methods used to initialize and set parameters. So it's also necessary to put a lot of code up to take up the content... Here's another point. What design patterns can you see from OkHttpClient?
1.builder mode
2. Appearance pattern
2.Request
After building OkHttpClient, you need to build a Request object. Looking at the source code of Request, you will find that you can't find many public constructors. The only constructor is like this.
Request(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = builder.headers.build();
this.body = builder.body;
this.tag = builder.tag != null ? builder.tag : this;
}
What does that mean, of course, that we need to build a request in builder mode, so take a look at the source code of builder.
public Builder newBuilder() {
return new Builder(this);
}
//builder===================
public Builder() {
this.method = "GET";
this.headers = new Headers.Builder();
}
Builder(Request request) {
this.url = request.url;
this.method = request.method;
this.body = request.body;
this.tag = request.tag;
this.headers = request.headers.newBuilder();
}
public Request build() {
if (url == null) throw new IllegalStateException("url == null");
return new Request(this);
}
In fact, ignoring other source code, since this blog is only to analyze OkHttp source code from the overall process, so we mainly focus on process source code analysis. From the source code above, we can see that request is also built based on builder mode.
3. Asynchronous requests
Note here that the analysis distinguishes synchronous requests from asynchronous requests, but the actual execution process is basically the same except asynchronous.
After building the Request, we need to build a Call, which is usually called = mOkHttpClient. newCall (request); then we return to the source code of OkHttpClient.
/**
* Prepares the {@code request} to be executed at some point in the future.
*/
@Override public Call newCall(Request request) {
//Factory mode
return RealCall.newRealCall(this, request, false /* for web socket */);
}
As you can see, the newRealCall method in RealCall is actually invoked here, but one thing to note here is the @Override annotation in front of the method, and when we see this annotation, we need to realize that the method is either inheritance or implementation interface.
public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {...}
You can see that OkhttpClient implements the Call.Factory interface.
//Call.java
interface Factory {
Call newCall(Request request);
}
From the source code of the interface, we can see that the interface is not complicated. It is just to define a new Call method for creating Call. The idea of factory mode is applied here. The details of the construction are given to the concrete implementation. The top layer only needs to get the Call object.
Back to the mainstream, let's continue to look at the new RealCall method in RealCall.
final class RealCall implements 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;
}
...
}
You can see that RealCall implements the Call interface. newRealCall is a static method. newRealCall has a RealCall object and created an EvetListener object. As you can see from its name, it is used to monitor the event flow. We can also see from the construction method that factory mode is used.
private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
this.client = client;
this.originalRequest = originalRequest;
this.forWebSocket = forWebSocket;
//Create a retryAndFollowUpInterceptor filter by default
this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
}
The key point is that in RealCall's constructor, besides the basic assignment, a retryAndFollowUpInterceptor filter is created by default. The filter can be said to be a great highlight of OkHttp. I will analyze some filters in detail in the following articles (limited capacity, try to look at them all).
Now that the Call is created, it's usually the last step, adding the request to the schedule, as is the general code.
//Request Join Scheduling
call.enqueue(new Callback()
{
@Override
public void onFailure(Request request, IOException e)
{
}
@Override
public void onResponse(final Response response) throws IOException
{
//String htmlStr = response.body().string();
}
});
You can see that the call's enqueue method is called here. Since the call - > RealCall is here, let's look at the enqueue method of RealCall.
@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));
}
1. First, synchronized object lock is added to prevent multi-threaded simultaneous invocation. Here we first judge whether executed is true to determine whether the current call has been executed or not. If true, an exception is thrown and if not, it is set to true.
2.captureCallStackTrace()
private void captureCallStackTrace() {
Object callStackTrace = Platform.get().getStackTraceForCloseable("response.body().close()");
retryAndFollowUpInterceptor.setCallStackTrace(callStackTrace);
}
As you can see here, it can be roughly understood that retryAndFollowUpInterceptor has added a call StackTrace for tracking stack information, which can be analyzed in detail later, without affecting the overall process understanding.
3.eventListener.callStart(this); you can see that the previously built eventListener works, here callback the callStart method first.
4. client. dispatcher (). enqueue (new AsyncCall (response Callback); here we need to go back to the source code of OkHttpClient.
public Dispatcher dispatcher() {
return dispatcher;
}
You can see that a Dispatcher object is returned only, and then it is traced back to the Dispatcher source code.
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
Here we first make a preliminary understanding of Dispatcher's member variables.
public final class Dispatcher {
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
private @Nullable Runnable idleCallback;
/** Executes calls. Created lazily. */
private @Nullable ExecutorService executorService;
/** 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<>();
...
}
As you can see, three queues, Array Deque, are used to save Call objects, which are divided into three states: asynchronous waiting, synchronous running and asynchronous running.
So the logic here is clearer.
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
When the number of asynchronous queues being executed is less than maxRequest(64) and the number of requests for the same host is less than maxRequestsPerHost(5), the request is added to the asynchronous execution queue runningAsyncCall, and the call is executed with the thread pool, otherwise the asynchronous waiting queue is added. Here you can see the runningCallsForHost method.
/** Returns the number of running calls that share a host with {@code call}. */
private int runningCallsForHost(AsyncCall call) {
int result = 0;
for (AsyncCall c : runningAsyncCalls) {
if (c.host().equals(call.host())) result++;
}
return result;
}
In fact, it is also very understandable, traversing the running AsyncCalls, recording the number of the same Host.
Now let's look at the source code of an AsyncCall, which is essentially where the core is executed.
final class AsyncCall extends NamedRunnable {
. . .
}
Looking at a class, first at its structure, you can see that AsyncCall inherits the NameRunnable class.
public abstract class NamedRunnable implements Runnable {
protected final String name;
public NamedRunnable(String format, Object... args) {
this.name = Util.format(format, args);
}
@Override public final void run() {
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
execute();
} finally {
Thread.currentThread().setName(oldName);
}
}
protected abstract void execute();
}
You can see that NamedRunnable is an abstract class. First, the Runnable interface is used. That's easy to understand. Then, look at the run method. You can see that the name of the thread currently executed is set as the name we passed in from the constructor. Then execute the method and finally set it back. So now we're back to AsyCall to find the execute method.
@Override protected void execute() {
boolean signalledCallback = false;
try {
//Asynchronization and synchronization go the same way, but the master executes only in the sub-threads
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 {
eventListener.callFailed(RealCall.this, e);
responseCallback.onFailure(RealCall.this, e);
}
} finally {
client.dispatcher().finished(this);
}
}
Finally, we found the Response, which means that the execution of the network request is in the getResponseWithInterceptorChain() method. The latter code is basically some interface callbacks, which calls back the execution status of the current Call. This is not analyzed here. Here we focus on the getResponseWithInterceptorChain(), which gives me the feeling that this method is o. The essence of kHttp.
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
//Failure and redirection filter
interceptors.add(retryAndFollowUpInterceptor);
//Encapsulating request and response filters
interceptors.add(new BridgeInterceptor(client.cookieJar()));
//Cache-related filters, which are responsible for reading caches and directly returning and updating caches
interceptors.add(new CacheInterceptor(client.internalCache()));
//Be responsible for establishing connections with servers
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
//Network Interceptors Set Up When Configuring OkHttpClient
interceptors.addAll(client.networkInterceptors());
}
//Responsible for sending request data to server and reading response data from server (actual network request)
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);
}
As you can see, here we first add an Array List of Interceptors, and then add various kinds of Interceptors. So when we create okHttpClient by default, okHttp will implement these filters by default. Each filter performs different tasks. This idea is too brilliant, each filter is responsible for its own tasks, and each filter is not coupled with each other. Combining, high cohesion, low coupling, and a series of design ideas such as expanding and opening up Barabara, here we can compare the ideas in Volley source code. Volley's processing is a series of operations such as caching, network requests and so on. As a result, users can only modify Volley by modifying the source code, and the modification must fully read and understand the whole process of volley. Perhaps some of the changes will affect the overall process, and here, separate filters with different responsibilities. Users only need to understand one of the functions they are concerned about and can make extended modifications. One-to-one comparison shows the advantages of okHttp in this respect. Here we will describe the functions of several filters.
retryAndFollowUpInterceptor -- Failure and redirection filters
BridgeInterceptor -- Encapsulating request and response filters
CacheInterceptor - a cache-related filter that reads and returns caches directly and updates them
Connect Interceptor - Responsible for establishing connections with servers, connection pools, etc.
Network Interceptors - Network Interceptors set up when configuring OkHttpClient
CallServer Interceptor -- responsible for sending request data to the server and reading response data from the server (actual network requests)
After adding the filter, the filter is executed. It's also very important here. It's hard to understand at first.
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
You can see that a RealInterceptorChain is created here and the proceed method is called. Notice here the parameter 0.
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
// If we already have a stream, confirm that the incoming request will use it.
if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must retain the same host and port");
}
// If we already have a stream, confirm that this is the only call to chain.proceed().
if (this.httpCodec != null && calls > 1) {
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
+ " must call proceed() exactly once");
}
// Call the next interceptor in the chain.
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);
// Confirm that the next interceptor made its required call to chain.proceed().
if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
throw new IllegalStateException("network interceptor " + interceptor
+ " must call proceed() exactly once");
}
// Confirm that the intercepted response isn't null.
if (response == null) {
throw new NullPointerException("interceptor " + interceptor + " returned null");
}
if (response.body() == null) {
throw new IllegalStateException(
"interceptor " + interceptor + " returned a response with no body");
}
return response;
}
At first glance, the head may be a little numb, a little deal with it.
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
. . .
// Call the next interceptor in the chain.
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;
}
This is very clear. Here index is the 0 we just had, that is, starting from 0. If index exceeds the number of filters and throws an exception, then a new Real Interceptor Chain will be added, and the parameter will be passed, and index+1, then the interceptor of index will be obtained, and the intercept method will be called to pass in the new next object. This may feel a little bit here. The idea of recursion is used to complete the traversal. In order to verify our idea, just find an interceptor and take a look at the intercept method.
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;
. . . There's no need to look at it for the time being.
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
You can see here we take a Connect Interceptor source code, here we get the chain, after the corresponding processing, continue to call proceed method, then the logic just now, index+1, get the next interceptor, repeat operation, so now it is clear, here we use recursive loop, that is, okHttp's most classic responsibility chain mode.
4. Synchronization requests
Synchronization is easy to understand when you see it asynchronously.
/**
* Synchronous request
*/
@Override public Response execute() throws IOException {
//Check that the call has been run
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
//Callback
eventListener.callStart(this);
try {
//Add requests to synchronous queues
client.dispatcher().executed(this);
//Create the filter responsibility chain and get the response
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
eventListener.callFailed(this, e);
throw e;
} finally {
client.dispatcher().finished(this);
}
}
You can see that basically the process is consistent, except for synchronous execution, the core method is the getResponseWithInterceptorChain() method.
okHttp's process is basically analyzed here, and then the analysis of Inteceptor. Here's a stolen graph for understanding the process. I hope to analyze all Inteceptors.
OkHttp source code