preface
I have been in an ordinary company for many years. Working on some very ordinary projects. Always know IT industry, is not on the way to learn, is on the way to be eliminated. I have a heart to enter dachang, and I want to look for brothers with dreams to further study together, so that we can be less lonely. This has been my journey for 14-19 years. I am also the author of ShadowLayout (Star2.2K), an open source control
After 19, I think it is very important for me to develop in t-style. During this period, I learned Web development, back-end entry, and briefly learned Unity. But recently I have received many internal promotion from big factories, and some of my friends who have been working in big factories for 19 years have triggered me a lot. Why? ? Why am I so close to giving up. Starting in September, I went back to the Android knowledge and understanding them in the most colloquial language, and put together a comprehensive interview material. I’m not gonna lose this time. If you and I have something in common, do it together. This is my Android exchange group: 209010674. If you need to.
A brief introduction to OkHttp
The general ability to become a warrior in the TV series begins with a simple secret book of martial arts. So let’s look at a simple piece of code a simple OkHttp GET request.
// Create OkHttpClient object
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10,TimeUnit.SECONDS)
// Add a custom web log interceptor
.addInterceptor(new HttpLogInterceptor())
.build();
// Create a network Request object and set parameters
Request request = new Request.Builder()
.url("url")
.header("headKey"."headValue")
.get()
.build();
// Make a network request
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 {}});Copy the code
I think all of these are very clear, so how do you make these things stick in your mind, and you don’t have to memorize them by rote? Take a look at the following picture (borrowed from another blogger here). I think he compares it to the express industry, and I will explain it in my way) :
Here with the return of goods metaphor and express described very image. From here, add OkHttp’s simple request, and it’s easy to see what classes there are:
- OkHttpClient: Go to the delivery point and build the OkHttpClient object
- Request: Network Request parameter configuration, just like sending express, fill in the tracking number, then know what to use express, send to where
- -Chuck: Oh, Call me the delivery guy
- Callback: You give him your phone number and he will call you back if the shipment is successful or lost
- Enqueue: Network requests are made using an asynchronous method. It’s equivalent to the delivery guy delivering the goods
Second, the OkHttpClient
See OkHttpClient.Builder as the Builder mode, also known as generator mode. Look at the source code, I know we don’t like to look at the code, then to a picture (too many parameters, just cut the first few. I know you don’t want to see it anyway)
There are too many parameters, which is why we use builder mode. For example, if we define a method with 10 parameters, then when we call this method, we have to call it strictly in accordance with the type and order of the parameters in the method, which is really troublesome. In builder mode, to pass an argument, simply click, and use the default if not.
Here are a few parameters:
- -Penny: You’re a Dispatcher
- List interceptors: interceptors
- List networkInterceptors: networkInterceptors
- Int readTimeout: readTimeout time
Forget what they do. We’ll talk about them later. You just need to have an idea.
Third, the Request
Request.Builder is a configuration parameter used for network requests. It is a configuration parameter used for network requests.
- .url(” URL “) : the requested network URL
- . Header (“key”,”value”) : the header parameter, which is usually stored in the login token
- .get() : Confirm the GET request
As an aside: When you want to encapsulate an OkHttp web request yourself, the more you know about the details, the better you’ll be able to encapsulate it
- We talk a lot about breakpoint continuations and downloads, but we actually use the header
CurrentLength is the length of the file you have downloaded
.header("RANGE"."bytes=" + currentLength + "-");
// Then use true in the download stream to reach the breakpoint download. Not using true is a new download.
fos = new FileOutputStream(file, true);
Copy the code
- Web caching
/ / 1.
// Online cache when network is available
// Temp online cache time,
// Scenario: for example, if there is a large number of concurrent projects, and the home page banner is not long, then get the data of the banner interface
// Will be cached. For example, if temp = 3600, the cached data will be read for the next hour
.header("Cache-Control"."public, max-age=" + temp)
/ / 2,
// Offline cache without network
// Scenario: Many news apps will still display the data opened last time when there is no network open
.header("Cache-Control"."public, only-if-cached, max-stale=" + temp)
Copy the code
Mention this here, after all, source code analysis, off topic.
Fourth, the Call
Call is an interface, we click on newCall, in fact, is generated call interface implementation class RealCall
public Call newCall(Request request) {
return new RealCall(this, request, false);
}
Copy the code
Take a look at the Call interface to see what it does, as usual. After seeing the picture, we know that it is actually a method of processing network requests and some states of network requests.
Realcall.enqueue; realcall.enqueue; realcall.enqueue
5.1, the call. The enqueue
This is an asynchronous request, and I’m sure you all know what this code means
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {}@Override
public void onResponse(Call call, Response response) throws IOException {}});Copy the code
Click on enqueue, and at first glance, it’s not complicated
public void enqueue(Callback responseCallback) {
synchronized(this) {
// Executed: As you can see from the code, this prevents multiple requests from being made in one call
if (this.executed) {
throw new IllegalStateException("Already Executed");
}
this.executed = true;
}
this.captureCallStackTrace();
this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}
Copy the code
this.captureCallStackTrace();
This method captures the StackTrace of a request
Go ahead and click in
private void captureCallStackTrace(a) {
Object callStackTrace = Platform.get().getStackTraceForCloseable("response.body().close()");
this.retryAndFollowUpInterceptor.setCallStackTrace(callStackTrace);
}
Copy the code
From the code you can see, he gets the current stack trace of interface object into the retryAndFollowUpInterceptor, interceptors. Then we’ll see retryAndFollowUpInterceptor blocker.
5.2, RetryAndFollowUpInterceptor
We all know okHttp has interceptors, but here we use the chain of responsibility pattern in design mode. Chain of responsibility mode: is a mode for processing a request, giving multiple processors the opportunity to process the request. Here the interceptor only deals with the business logic that is relevant to it.
Digression: as long as inherit Interceptor to Interceptor class, override intercept (Chain Chain) intercept method, finally in okHttpClient. AddInterceptor (). For example, in development, after a successful login, there will be a logon token. In this case, the interceptor can be used to add the token for each subsequent interface header parameter. For example, in development testing, we can print the details of each network request. For example, to implement a web log print interceptor, the pseudo-code is as follows:
@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("┌ ─ ─ ─ ─ ─ ─ ─ the Request Start ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─");
log("Request mode -->>" + request.method() + ":" + url);
log("Request mode -->>"+"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("Request header -->>" + headers.name(i) + ":"+ headers.value(i)); }}}//param
RequestBody requestBody = request.body();
String paramString = readRequestParamString(requestBody);
if(! TextUtils.isEmpty(paramString)) { log("Request Parameters -->>" + 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("Request return -->>" + responseString);
log("└ ─ ─ ─ ─ ─ ─ ─ the Request End ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─" + "\n" + "-");
return response;
}
Copy the code
You don’t need to look too closely at the interceptors above, just know that in the interceptor method, you can get the Request and ResponseBody, so what else can’t you do? Oh, again. Beside the point. Back to our subject.
RetryAndFollowUpInterceptor is responsible for a retry, and request the redirection of the interceptor, use StreamAllocation here, mainly for the connections of RealConnection, if need to release the redirection, close the socket and other resources, Enable “new” requests for retry and redirection. We continue to look at his rewrite of the Intercept method:
public Response intercept(Chain chain) throws IOException {
// Get the request body
Request request = chain.request();
StreamAllocation is a core class. This is a brief introduction, and we'll talk about it later
// Parameter 1: connection pool for OkHttpClient
// Parameter 2: request address
// Argument 3: stack trace object
// What StreamAllocation does:
//1. Deal with the reuse function of different connections, the reusability of protocol streams and the processing of ConnectionPool
//2. Handle input and output to the request HttpCodec stream
StreamAllocation associates all cycles of a Call request to determine whether the Connection is occupied and whether other calls can use the Connection
this.streamAllocation = new StreamAllocation(this.client.connectionPool(), this.createAddress(request.url()), this.callStackTrace);
// Redirection times
int followUpCount = 0;
Response priorResponse = null;
// The condition is always true unless called
/ / call. Cancle (), the method call in the - > retryAndFollowUpInterceptor. Cancel () - > canceled = true () under which condition is false, not to. Streamallocation.cancel () is also called here; The code keeps tracking, eventually calling --> codectocancer.cancel (); The cancellation method of RealConnection is called
while(!this.canceled) {
Response response = null;
boolean releaseConnection = true;
try {
// Intercept the network Response body
response = ((RealInterceptorChain)chain).proceed(request, this.streamAllocation, (HttpCodec)null, (RealConnection)null);
// Release the Connection flag, in the finally after a try catch, where it is not released
releaseConnection = false;
} catch (RouteException var13) {
//recover is used to determine whether a redirection is needed. For example, if you write the wrong interface, it is not needed. It can be understood that there is a reason for the error
if (!this.recover(var13.getLastConnectException(), false, request)) {
throw var13.getLastConnectException();
}
releaseConnection = false;
continue;
} catch (IOException var14) {
booleanrequestSendStarted = ! (var14instanceof ConnectionShutdownException);
if (!this.recover(var14, requestSendStarted, request)) {
throw var14;
}
releaseConnection = false;
continue;
} finally {
if (releaseConnection) {
this.streamAllocation.streamFailed((IOException)null);
this.streamAllocation.release(); }}// The first loop priorResponse must be empty. So response is the response that we get in try{} that we put in streamAllocation
if(priorResponse ! =null) {
// If it arrives here the second time or later, a new response is generated from the previous priorResponse. So this is response that you want to redirect. And actually, at the end of this method, priorResponse = response; So it is the first time that the response of streamAllocation is put in, that is, the redirection will continue as long as the condition is met
response = response.newBuilder().priorResponse(priorResponse.newBuilder().body((ResponseBody)null).build()).build();
}
// There is a response below followUpRequest which is actually to determine whether there is a reconnection or redirection condition, for example, to obtain the code and other conditions for judgment
Request followUp = this.followUpRequest(response);
// Return the Request that needs to be retried or redirected. If empty, the Request was successful, then return the current response
if (followUp == null) {
if (!this.forWebSocket) {
this.streamAllocation.release();
}
return response;
}
// Close the response resource. Util.closeQuietly(this.source()); The source is actually a BufferedSource, which we can think of as streaming and byte resources
Util.closeQuietly(response.body());
// Each retry or redirect is ++
++followUpCount;
// When the number of times exceeds 20, the system fails
if (followUpCount > 20) {
this.streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
// All of the following conditions are not met for retry or redirection, so we throw the error out
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");
}
Copy the code
FollowUpRequest method pseudocode:
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{...returnrequestBuilder.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; }}}Copy the code
5.3, StreamAllocation
StreamAllocation is in retry and redirect blocker RetryAndFollowUpInterceptor initialization. In fact, this class is mainly used when the ConnectInterceptor initiates a real network request.
Learning about this class first here makes it easier to understand the full text. While getting to know this class, let’s take a look at a digression (this is a digression that I found through a lot of research, guys). This is an important point, (pointing to the blackboard)
- Http2.0 uses a multiplexing technique where multiple streams can share a socket connection. Each TCP connection is completed through a socket, which corresponds to a host and a port. If multiple streams (i.e., multiple requests) are connected to the same host and port, They can then share the same socket, which has the advantage of reducing TCP’s three-way handshake time.
RealConnection is responsible for the connection in OkHttp. As mentioned above, we can cancel the request with call.cancle(). Cancle () is called realConnection.cancle() in StreamAllocation.Copy the code
- Http communication performs network request Call. A new Stream (HttpCodec in OkHttp) needs to be set up on the connection to perform the Call. We consider StreamAllocation as a bridge to find a connection for a request and set up a Stream to complete the communication
So let’s look at the pseudocode for 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);
// Set up the OkHttp connection flow, click on the new is an Http2Codec
HttpCodec resultCodec = resultConnection.newCodec(client, this);
}
// Find a healthy RealConnection,
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) throws IOException {
while(true) {...//findHealthyConnection actually calls findConnection, just to determine whether it is healthy
// check whether the RealConnection candidate isHealthy. IsHealthy check whether the current socket is closed voluntarily, etc
if (candidate.isHealthy(doExtensiveHealthChecks)) {
return candidate;
}
// If RealConnection is unhealthy, close its current socket resource
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;
// If this. Connection is not null; If safe, return the current RealConnection
//this.connection is a global variable, which must be empty the first time.
if(allocatedConnection ! =null && !allocatedConnection.noNewStreams) {
return allocatedConnection;
}
// Get the Connection for the first time
// Get an available connection from the connection pool. The Route Route passes null, indicating that the connection pool has been resolved.
Internal.instance.get(this.connectionPool, this.address, this, (Route)null);
// internal.instance.get () calls OkHttpClient's pool.get() and ConnectionPool get() as follows:
// @Nullable RealConnection get(Address address, okhttp3.internal.connection.StreamAllocation streamAllocation, Route route) {
/ /...
// for (RealConnection connection : connections) {
// if (connection.isEligible(address, route)) {
// We pass connection to streamAllocation, which we can use as 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
// The Router is passed in
//Router: to determine exactly how to connect. A Router contains a proxy, IP address, and port. Multiple Router combinations with different IP addresses for the same port and the same proxy type
// Incorporate the concepts of http2.0 multiplexing. Here we keep looping this.routeselector.next () looking for a reusable Connection
Internal.instance.get(this.connectionPool, this.address, this, selectedRoute);
if (this.connection ! =null) {
return this.connection;
}
this.route = selectedRoute;
this.refusedStreamCount = 0;
//3. Obtain Connection for the third time
// Create a new Connection if no Connection can be multiplexed in the current Connection pool
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) {
// Add the new Connecttion to the connection pool
Internal.instance.put(this.connectionPool, result);
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(this.connectionPool, this.address, this);
result = this.connection;
}
}
Util.closeQuietly(socket);
returnresult; }...public void noNewStreams(a) {
Socket socket;
synchronized(this.connectionPool) {
socket = this.deallocate(true.false.false);
}
If the connection is not healthy, find the connection socket and close the resourceUtil.closeQuietly(socket); }... }Copy the code
5.4, the dispatcher
Had said this. CaptureCallStackTrace (); This.client.dispatcher ().enqueue(new realCall.asynccall (responseCallback)); Remember the simple code? As follows:
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));
}
Copy the code
First this.client.dispatcher() gets the object of the Dispatcher. To schedule and manage these requests. Here’s the Dispatcher pseudocode:
public final class Dispatcher {
// Maximum number of requests, meaning a delivery company has 64 cars. If it needs more cars, it has to wait for other cars to be free
private int maxRequests = 64;
// Maximum number of requests allowed by a host. There are 5 vehicles for the same city to deliver express goods. If there are more express goods to be delivered, we can only wait for 5 vehicles to be free
private int maxRequestsPerHost = 5;
// Thread pool, lazy load is created when called
private @Nullable ExecutorService executorService;
// Prepare the queue, i.e., requests that exceed maxRequests and maxRequestsPerHost will be added here
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
// The asynchronous queue, i.e., not exceeding the maximum number of requests, will be placed here
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
// The synchronization queue, not exceeding the maximum number of requests, will be added here
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
public Dispatcher(ExecutorService executorService) {
this.executorService = executorService;
}
public Dispatcher(a) {}public synchronized ExecutorService executorService(a) {
if (executorService == null) {
// Create a thread pool. 60 seconds is keepAliveTime. That is, the thread will not disappear immediately after finishing the work. When there is work to do, it will reuse the thread that is still alive, and the performance is optimized
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher".false));
}
returnexecutorService; }... }Copy the code
5.5, enclosing the client. The dispatcher (). The enqueue ()
synchronized void enqueue(AsyncCall call) {
// After comparing the maximum number of requests, add readyAsyncCalls to wait for the request if it exceeds the maximum number of requests, otherwise add runningAsyncCalls to wait for the request.
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else{ readyAsyncCalls.add(call); }}Copy the code
Now let’s see where readyAsyncCalls are executed. Click open source:
private void promoteCalls(a) {
// If the number of requests is greater than or equal to the maximum number, there is no idle time. The readyAsyncCalls must continue to wait
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
// If readyAsyncCalls themselves are empty, no need to execute and return directly
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);
// Finally requested here.
executorService().execute(call);
}
if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.}}Copy the code
So looking at this, you might be wondering when the promoteCalls() method is executed? Let’s think about the conditions under which this request would be triggered. Well, that would have to be met. Hahaha
- Condition 1: The number of requests does not exceed the maximum number. It is easy to think of changing the maximum number of requests to maxRequests or maxRequestsPerHost
- Condition 2: Waiting for an idle request route, i.e. a request from runningAsyncCalls has been completed
Open source was actually executed in three methods, setMaxRequests(), setMaxRequestsPerHost(), and FINISHED (AsyncCall Call).
5.6, the executorService (). The execute (call)
Call AsyncCall is an inner class of RealCall, and its initialization properties are all properties of RealCall. It is a named Runnable(meaning setThreadName is given to the thread) :
final class AsyncCall extends NamedRunnable {
// Request a Callback for the result
private finalCallback responseCallback; .// Request an address
String host(a) {
return originalRequest.url().host();
}
/ / request body
Request request(a) {
return originalRequest;
}
RealCall get(a) {
return RealCall.this; }}Copy the code
ExecutorService () generates a ThreadPoolExecutor thread pool, adds it to the workQueue through the execute method, and executes it. I’m going to skip the thread pool concept here. Look directly at the execute() method
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);
}
Copy the code
Because we know AsyncCall is a runnable, it actually executes its run method. We know this from the parent NamedRunnable. This implements the abstract execute() method defined in NamedRunnable and implemented by AsyncCall as follows:
final class AsyncCall extends NamedRunnable {...@Override protected void execute(a) {
boolean signalledCallback = false;
try {
/ / by getResponseWithInterceptorChain () to obtain the final request returns the 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 {
// Whether the interface succeeds or fails, the Call request will be removed
client.dispatcher().finished(this); }}}Copy the code
We see here is through getResponseWithInterceptorChain () to get the Response. So let’s see what this method does.
Response getResponseWithInterceptorChain(a) throws IOException {
// You can see a lot of interceptors here
List<Interceptor> interceptors = new ArrayList<>();
// Add a user-defined interceptor
interceptors.addAll(client.interceptors());
// Add a retry failure or redirect interceptor
interceptors.add(retryAndFollowUpInterceptor);
// Bridge data conversion interceptor
interceptors.add(new BridgeInterceptor(client.cookieJar()));
// Add a cache interceptor
interceptors.add(new CacheInterceptor(client.internalCache()));
// Add connection interceptor
interceptors.add(new ConnectInterceptor(client));
if(! forWebSocket) { interceptors.addAll(client.networkInterceptors()); } interceptors.add(new CallServerInterceptor(forWebSocket));
// Finally, with the request data, the network request returns our Response via the RealInterceptorChain and the role of these interceptors
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null.null.null.0, originalRequest);
return chain.proceed(originalRequest);
}
Copy the code
At this point, the asynchronous request is finished. This is about asynchronous requests. The call.enqueue() method is used. What does a synchronous request look like? I’m sure you understand what it says and if I put in two pieces of code, you’ll be enlightened
// The synchronization request is simple enough. RealCall Response execute = call.execute();Copy the code
RealCall the execute (); The synchronous implementation is basically the same as the asynchronous AsyncCall implementation
@Override public Response execute(a) 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); }}Copy the code
As an aside: Seeing what we know about the scheduler, how do we cancel network requests if we wrap an OkHttp ourselves? Here is the method I encapsulated earlier to cancel a network request:
//tag cancels network request
public void cancleOkhttpTag(String tag) {
// Encapsulate, of course, okHttpClient uses singletons to get the scheduler.
Dispatcher dispatcher = okHttpClient.dispatcher();
synchronized (dispatcher) {
//dispatcher.queuedCalls() gets readyAsyncCalls
for (Call call : dispatcher.queuedCalls()) {
// Get the Request. Of course, when requesting the network, okHttp takes a dot tag. So the tag that's passed in is going to be compared to that tag
if(tag.equals(call.request().tag())) { call.cancel(); }}// Get runningCalls
for (Call call : dispatcher.runningCalls()) {
if(tag.equals(call.request().tag())) { call.cancel(); }}}}Copy the code
6. Interceptor
The front for source code analysis more clearly has put RetryAndFollowUpInterceptor 5.2 summary said. Here we see the getResponseWithInterceptorChain () method, there are so many interceptors. Let’s look at them one by one:
6.1, BridgeInterceptor
In fact, we use OkHttp for network requests, is the author in the internal high encapsulation, simplify our operations. A real network request is implemented. Let’s start with a brief description of the bridging interceptor concept and its role:
- Convert application requests to network requests (that is, add the necessary response headers for network requests. Press F12 on the Web and select Network to listen to network. You can see that network requests have many headers)
- Converting network responses to application responses (i.e., to user friendly responses, supporting GZIP compression is also intended to improve performance)
- Since cookies are also in the header information, the system is an empty implementation. If you customize the cookieJar, it counts as a function to distinguish user information
BridgeInterceptor contains the following code:
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
// What do we get when we look at the cookies in the system? First, the default value in OkHttpClient is cookieJar = cookiejar.no_cookies;
// The CookieJar interface loadForRequest is implemented by default
CookieJar NO_COOKIES = new CookieJar() {
@Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {}@Override public List<Cookie> loadForRequest(HttpUrl url) {
// This is an empty array
returnCollections.emptyList(); }};Copy the code
Take a look at the source code for our 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();
// Get the request body of the user's application request
RequestBody body = userRequest.body();
if(body ! =null) {
MediaType contentType = body.contentType();
if(contentType ! =null) {
// Add header information content-type, request Type
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
// Determine which request resolution method to use contentLength, content-Length or 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 the header does not add the requested host site, add it
if (userRequest.header("Host") = =null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
// Hold the request long connection. This means that TCP connections go through three handshakes, four waves of the hand. Long connections can be understood as multiple network requests to reduce the number of handshakes and waves. It not only reduces time but also improves performance
if (userRequest.header("Connection") = =null) {
requestBuilder.header("Connection"."Keep-Alive");
}
// Receive gzip compression. As we pass large files, compression is not transferred faster, less resources
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: identifies user information
requestBuilder.header("Cookie", cookieHeader(cookies));
}
// The requested user information is on what operating system or user platform, such as what browser
if (userRequest.header("User-Agent") = =null) {
requestBuilder.header("User-Agent", Version.userAgent());
}
// After the above operations, the user request is converted into our network request
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)));
}
// After the above operations, a user friendly request response is generated
returnresponseBuilder.build(); }... }Copy the code
6.2, ConnectInterceptor
This interceptor is very concise and mainly calls StreamAllocation. This class was covered in “5.3 StreamAllocation” and StreamAllocation does most of the work
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;
// Get the request body of the network request
Request request = realChain.request();
/ / get our core network requests StreamAllocation, also in RetryAndFollowUpInterceptor initialization
StreamAllocation streamAllocation = realChain.streamAllocation();
booleandoExtensiveHealthChecks = ! request.method().equals("GET");
// Call the newStream() method of streamAllocation. If you look at StreamAllocation in 5.3, the method is findHealthConnect.
// Add the new Connection to the ConnectionPool
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
// Pass it to the next interceptor
returnrealChain.proceed(request, streamAllocation, httpCodec, connection); }}Copy the code
Off topic (emphasis) : May be mentally force here everyone, including myself, why has been in RetryAndFollowUpInterceptor network request, ConnectInterceptor how still again, Why RetryAndFollowUpInterceptor network request is not much in the way to reuse? Not so fast, let’s just look at a diagram of a network request blocker:
From this picture, let’s summarize: first, the direction of the network request starts from the left
- The first is to enter RetryAndFollowUpInterceptor interceptors, but we see the source code, after know this interceptor is wait for the Response to know whether retry or redirect, at the time of the request will not go here to intercept
- Second, enter the BridgeInterceptor interceptor and turn our user request into a real network request, adding header information
- We’ll see later on in the CacheInterceptor interceptor, but let’s clarify the logic here
- Finally, enter the ConnectInterceptor interceptor and call StreamAllocation’s newstream to find a healthy RealConnection. Make a real network request
That’s where the network request ends, so let’s look at the network Response on the right, which is the direction that Response returns
- First, the ConnectInterceptor returns a Response to the network request to the next level of interceptor, which is definitely CacheInterceptor.
- If the BridgeInterceptor is configured with a cache, it will process the Response returned by the network and return a friendly user Response to the user.
- End up in Response to the RetryAndFollowUpInterceptor interceptors, if the request is successful, you will directly return the Response, infinite loop to stop. If no, the system determines whether to retry or redirect. After the conditions are met, the network request is made again.
So that’s sort of the process. So much for the digression in this section, if you feel like you haven’t heard enough
6.3, CacheInterceptor
In the “III Request” section, we explained that we can achieve online cache and offline cache by configuring the header parameter HEAD. Next, let’s have a look at our cache interceptor, the pseudocode is as follows:
public final class CacheInterceptor implements Interceptor {
// DisLruCache encapsulates a Cache.Cache is in the okhttp3 package of OkHttp source code, not in the Cache package
final InternalCache cache;
public CacheInterceptor(InternalCache cache) {
this.cache = cache;
}
@Override public Response intercept(Chain chain) throws IOException {
// Select * from request.url; // Select * from request.urlResponse cacheCandidate = cache ! =null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
// Cache policy, which determines whether to use network, cache, or both
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if(cache ! =null) {
cache.trackResponse(strategy);
}
// If the cache Response is not null, but the result of the cache policy is that the cache does not apply, then the cache is disabled
if(cacheCandidate ! =null && cacheResponse == null) {
closeQuietly(cacheCandidate.body());
}
// If the network and cache are both null, then we return code504 directly, timeout
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 the network is null and the cache is valid, use the cache
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we crash somewhere else in I/O do not leak the cache, close it in time
if (networkResponse == null&& cacheCandidate ! =null) { closeQuietly(cacheCandidate.body()); }}if(cacheResponse ! =null) {
// Return HTTP_NOT_MODIFIED304 meaning the server determines whether the interface data has been updated, if not, then use caching
// Otherwise close the cache
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()); }}// Use network response
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if(cache ! =null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Cache data locally
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.}}}returnresponse; }... }Copy the code
Here is a summary of the following:
- Read the candidate cache.
- Create a cache policy based on the candidate cache.
- If no network request is made and no data is cached according to the cache policy, error code 504 is returned.
- According to the cache policy, cached data is returned directly if no network request is made and cached data is available.
- If the cache is invalid, the network request continues.
- If the server verifies that the cached data can be used (return 304), the cached data is directly returned and the cache is updated.
- Read network results, construct response, and cache data.
In fact, there are a lot of knowledge points, do not understand the place we can go to study; DiskLruCache is fully resolved and can be seen in this article. With all that said about caching interceptors, why don’t we use caching when we use OkHttp, because here and the CookieJar are also empty implementations, look at the following code:
// First in RealCall, add
interceptors.add(new CacheInterceptor(client.internalCache()));
// Then go to the OkHttpClient:
InternalCache internalCache(a) {
// This cache is passed in new OkHttpClient().cache()
// If the cache is empty, internalCache will be used eventually. No transmission is null by default
returncache ! =null ? cache.internalCache : internalCache;
}
If client.internalCache() is null, then the underlying cache is nullResponse cacheCandidate = cache ! =null
? cache.get(chain.request())
: null;
Copy the code
Here’s another digress: Remember the online and offline caches we talked about earlier? The following code
// We also need to provide a cache file to initialize our OkHttpClient. Cache ()
// This is the code when I package EasyOk
// Set the cache file path and file size
.cache(new Cache(new File(Environment.getExternalStorageDirectory() + "/okhttp_cache/"), 50 * 1024 * 1024))
/ / 1.
// Online cache when network is available
// Temp online cache time,
// Scenario: for example, if there is a large number of concurrent projects, and the home page banner is not long, then get the data of the banner interface
// Will be cached. For example, if temp = 3600, the cached data will be read for the next hour
.header("Cache-Control"."public, max-age=" + temp)
/ / 2,
// Offline cache without network
// Scenario: Many news apps will still display the data opened last time when there is no network open
.header("Cache-Control"."public, only-if-cached, max-stale=" + temp)
Copy the code
Notice that we’re using a header file to set up the cache, and we’re actually using a class, CacheControl, for both Request and Response. This class has a method that fetches the Settings in the cache-Control header
// The code is too long
public static CacheControl parse(Headers headers) {}
Copy the code
With that in mind, in addition to setting the header information, we can also use the interceptor to get the Request and set it, just look at the code
Request request = new Request.Builder()
.cacheControl(new CacheControl.Builder()
.onlyIfCached()
.build())
.url("http://publicobject.com/helloworld.txt")
.build();
Copy the code
Remember CacheStrategy in CacheIntereptor, which is a caching policy. OkHttp has two caching strategies:
Mandatory cache: The server is required to participate, and the server returns the time identifier. If the time is not invalid, the cache is used. The mandatory cache has two identifiers
- Expires: The Expires value is the expiration time returned by the server. If the request time is shorter than the expiration time returned by the server, the cached data is directly used. The expiration time is generated by the server. There may be some error between the client and the server.
- There is a time-verification issue, and all HTTP1.1 uses cache-control instead of Expires.
Comparison cache: Requires the server to participate, the server returns whether the resource has been modified, unmodified returns code304 to use cache, otherwise not. Comparison caches also have two identifiers
- Last-modified: indicates the time when the resource was Last Modified.
- ETag: A resource file identifier. When the client sends the first request, the server returns the current resource identifier. If requested again, different identity resources are modified
Here we have a general idea of the cache strategy
ConnectionPool ConnectionPool
We all know that network requests go through three handshakes and four waves. To speed up network request access and improve performance, Http2.0 multiplexing is needed at this time. We can add keepalive to a request head so that the request’s RealConnection can be reused and added to the ConnectionPool.
//ConnectionPool uses a Deque to hold a RealConnection
private final Deque<RealConnection> connections = new ArrayDeque<>();
//Okhttp supports 5 concurrent KeepAlive connections. The default link life is 5 minutes. The connection pool is implemented by ConectionPool to recycle and manage connections.
public ConnectionPool(a) {
this(5.5, TimeUnit.MINUTES);
}
Copy the code
Let’s see how he joins the Deque
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if(! cleanupRunning) { cleanupRunning =true;
// Do not run this code if the idle thread has not been cleared, otherwise actively call a idle thread clearing
executor.execute(cleanupRunnable);
}
// Add to the Deque
connections.add(connection);
}
Copy the code
Well, that’s probably the end of it. There will be no more digressions. If you look at it from the beginning, you will have the same feeling as ME. You will no longer be afraid that the interviewer will ask you the source code of OkHttp. Thank you for browsing, join me in the fight!