Okhttp is an open source project for handling web requests. It is the most popular lightweight framework for Android, and was contributed by Mobile payments company Square (which also contributed Picasso) to replace HttpUrlConnection and Apache HttpClient. As a common framework, it is necessary to understand its source code implementation, not just stay in the API call level, whether from the use or interview.

Before going into the source code, let’s briefly review the use of okHTTP.

First, basic use

1.1 the import

Introduce dependencies through Gradle

Implementation 'com. Squareup. Okhttp3: okhttp: 3.14.0' implementation 'com. Squareup. Okio: okio: 2.8.0'Copy the code

Okhttp is currently available in version 4.9.1, is located in maven’s central repository, and is written in Kotlin. This article analyzes the version used in a company project, and is written in Java.

1.2 the use of

// Synchronize request, The child thread lifecycleScope. Launch {withContext(dispatchers.io) {val syncCall = OkHttpClient().newCall(Request.Builder().url("https://www.baidu.com/").get().build()).execute() toast("sync call result Is ${synccall.body ()?.string()}")} OkHttpClient().newCall(Request.Builder().url("https://www.baidu.com/").get().build()) .enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { } override fun onResponse(call: Call, response: Response) { toast("sync call result is ${response.body()?.string()}") } })Copy the code

With the above code, we can use OKHTTP as quickly as possible (toast in the code is an extension of the project method and can be used as a native TOAST).

Two, the key class in the source code

2.1 OkHttpClient

For sending HTTP requests and reading their responses, newCall () is called to create call objects. For a project, OkHttpClient should provide instances in singleton mode, reducing latency and saving memory by reusing connection pools and thread pools to maximize OKHTTP performance.

Okhttpclient uses the Builder pattern to create instances while assigning values to their properties.

  • Getting instances through OkHttpClient() takes the default Settings
  • Custom configuration (connection timeout, read timeout, write timeout, interceptor, cache information, etc.) when fetching instances from new OkHttpClient.Builder()

For example, when wrapping the titleBar on a page, the left, center, and right areas of the titleBar can specify text, images, or text image mix, as well as the upper and lower left spacing, text size, style, click events, and so on. With the Builder pattern, you can aggregate a large number of properties together and set them through chain calls, separating the invocation from the implementation for aesthetic and easy maintenance.

2.2 the Request

A request represents an Http request, which internally encapsulates information such as THE URL, request mode, request header, and request body. It is also created through the builder pattern.

2.3 the Call

A call represents a request that is ready to be executed. It can be cancelled and cannot be executed more than once. Its implementation class is RealCall, which provides synchronous call method execute() and asynchronous call method enQueue (Callback responseCallback).

2.4 AsyncCall

AsyncCall is an internal RealCall class that implements the NamedRunnable interface. It is a Runnable implementation class whose executeOn() method does the processing of asynchronous requests.

2.5 the Dispatcher

Class for scheduling requests, There are internal maximum requests (64 by default), maximum requests per host (5 by default), thread pool, three dual-end queues (readyAsyncCalls ready for execution, runningAsyncCalls in progress, runningSyncCalls in progress), and so on Properties.

For thread pools, the implementation is as follows:

Dispatcher.java public synchronized ExecutorService executorService() { if (executorService == null) { executorService =  new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false)); } return executorService; }Copy the code
  • Number of core threads corePoolSize = 0
  • Maximum number of threads maximumPoolSize = integer. MAX_VALUE, that is, the number of threads is almost unlimited
  • KeepAliveTime = 60s. The thread ends automatically after 60s.
  • SynchronousQueue workQueue is a SynchronousQueue. This queue is like a baton that must be passed from queue to queue at the same time. Because thread creation is unlimited, there is no queue waiting, so SynchronousQueue is used.

Double ended queue Deque

Can be used as a queue for a FIFO(First In First Out) or as a stack for LIFO(Last In First Out). Push and POP methods are only available as a stack. Unlike lists, double-ended queues do not support fetching elements by index.

The common apis are summarized as follows:

  • Boolean add(E E) : Adds elements to the end of the queue. If there is a limit on the number of queue elements added, throwing an IllegalStateException
  • Boolean offer(E E) : Adds elements to the end of the queue. If there is a limit on the number of queue elements, return false
  • E remove() : Retrieves and removes the queue header element, throwing a NoSuchElementException if the queue is empty
  • E poll() : retrieves and removes the header element from the queue. If the queue is empty, null is returned without throwing an exception
  • E Element () : Retrieving does not remove the queue header element, and throws a NoSuchElementException if the queue is empty
  • E peek() : Retrieves without removing the queue header element, returns null if the queue is empty, and does not throw an exception
  • Void push(E E) : Pushes an element onto the stack represented by a double-ended queue (queue header, stack-specific method). If there is a limit to the number of queues, adding an IllegalStateException beyond that number is added
  • E pop() : Pops an element from the stack represented by this double-ended queue (removes and returns the queue head, stack-specific method) and throws a NoSuchElementException if the queue is empty

Third, source code analysis

3.1 Analysis of Synchronous Request Modes

The synchronization request starts with the execute() method, and let’s take a look at RealCall’s execute() implementation

RealCall.java @Override public Response execute() throws IOException { synchronized (this) { if (executed) throw new IllegalStateException("Already Executed"); executed = true; } transmitter.timeoutEnter(); // Start the timeout calculation transmitter. CallStart (); // Request initiated try {client.dispatcher().executed(this); / / execution request return getResponseWithInterceptorChain (); } finally {client.dispatcher().finished(this); // Request completed}}Copy the code

Where, the method to execute the request is implemented as follows:

Dispatcher.java /** Used by {@code Call#execute} to signal it is in-flight. */ synchronized void executed(RealCall call)  { runningSyncCalls.add(call); }Copy the code

Put the current synchronous call into the Dispatcher’s runningSyncCalls.

Take a look at the request completion method implementation:

Dispatcher.java /** Used by {@code Call#execute} to signal completion. */ void finished(RealCall call) { finished(runningSyncCalls, call); // } private <T> void finished(Deque<T> calls, T call) { Runnable idleCallback; Synchronized (this) {// remove request if (! calls.remove(call)) throw new AssertionError("Call wasn't in-flight!" ); idleCallback = this.idleCallback; } boolean isRunning = promoteAndExecute(); // Check if there is a request in progress if (! isRunning && idleCallback ! = null) {// There is no request in progress and the empty callback is not null idlecallback.run (); }}Copy the code

Remove the request from the synchronous request queue runningSyncCalls. In that case, the synchronous request processing occurs in the RealCall $getResponseWithInterceptorChain ().

To sum up:

Execute () is called to make a synchronous request in three steps:

  • Request to join the runningSyncCalls queue
  • RealCall $getResponseWithInterceptorChain () call
  • The request was removed from the runningSyncCalls queue

Speak about getResponseWithInterceptorChain () the analysis of the behind.

3.2 Analysis of Asynchronous Request Modes

The asynchronous request starts with the enqueue(CallBack CallBack) method. See what happens after the Enqueue (CallBack CallBack) of the RealCall

RealCall.java Override public void enqueue(Callback responseCallback) { synchronized (this) { if (executed) throw new IllegalStateException("Already Executed"); executed = true; } transmitter. CallStart (); client.dispatcher().enqueue(new AsyncCall(responseCallback)); }Copy the code

After calling enQueue, an AsyncCall is generated with the response callback we created and enqueue() of the Dispatcher is called.

Dispatcher.java void enqueue(AsyncCall call) { synchronized (this) { readyAsyncCalls.add(call); ReadyAsyncCalls // Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call  to // the same host. if (! call.get().forWebSocket) { AsyncCall existingCall = findExistingCallWithHost(call.host()); If (existingCall! = null) call.reuseCallsPerHostFrom(existingCall); // Set the number of requests under the current host}} promoteAndExecute(); // Remove executable requests from readyAsyncCalls and add them to runningAsyncCalls}Copy the code

ReadyAsyncCalls are placed in the Dispatcher enqueue() and promoteAndExecute() is called. Remove executable requests from readyAsyncCalls and add them to runningAsyncCalls.

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 (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity. i.remove(); asyncCall.callsPerHost().incrementAndGet(); 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; }Copy the code

When traversing readyAsyncCalls, it checks whether the number of requests in progress has reached the threshold and whether the number of requests in progress on the same host has reached the threshold. After passing the previous detection, it removes the requests from readyAsyncCalls. ExecutableCalls and runningAsyncCalls in the executing queue. ExecutableCalls, call AsyncCall$executeOn().

RealCall.java RealCall$AsyncCall void executeOn(ExecutorService executorService) { assert (! Thread.holdsLock(client.dispatcher())); boolean success = false; try { executorService.execute(this); // Thread pool execution Runnable success = true; } catch (RejectedExecutionException e) { InterruptedIOException ioException = new InterruptedIOException("executor rejected"); ioException.initCause(e); transmitter.noMoreExchanges(ioException); responseCallback.onFailure(RealCall.this, ioException); } finally {if (! success) { client.dispatcher().finished(this); // This call is no longer running! }}}Copy the code

In executeOn(), the thread pool triggers AsyncCall’s run(). AsyncCall does not have a run() method. Look at the definition of run() in its parent NameRunnable

  NamedRunnable.java
    
    @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();
Copy the code

Execute () is called in run() and AsyncCall implements this abstract method, so we’ll have to look at the implementation of execute() in AsyncCall.

RealCall.java RealCall$AsyncCall @Override protected void execute() { boolean signalledCallback = false; transmitter.timeoutEnter(); / / the timeout time try {Response Response = getResponseWithInterceptorChain (); 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); // Callback request failed}} finally {client.dispatcher().finished(this); // Request completed}}Copy the code

Execute () of AsyncCall is where asynchronous requests really start. Like a synchronous request, an asynchronous request is end RealCall $getResponseWithInterceptorChain () to obtain the response, and the callback after the request is successful or failed to application layer. Let’s look at the finished() call after the asynchronous request.

Dispatcher.java /** Used by {@code AsyncCall#run} to signal completion. */ void finished(AsyncCall call) { call.callsPerHost().decrementAndGet(); // Finished (runningAsyncCalls, call); }Copy the code

After the asynchronous request is complete, the number of host requests for the current request is reduced by one and Dispatcher$finished() is called. Unlike synchronous requests, the first parameter of Dispatcher$finished() passes runningAsyncCalls and removes the request from the runningAsyncCalls.

To sum up:

Call enqueue(Callback Callback) to make an asynchronous request:

  • Generate an AsyncCall using the callback and place the AsyncCall in readyAsyncCalls.
  • Iterate over readyAsyncCalls, removing eligible requests from readyAsyncCalls and adding runningAsyncCalls, a local variable executableCalls
  • ExecutableCalls, call asynccall.executeon (), trigger asynccall.execute () in the thread pool
  • RealCall. GetResponseWithInterceptorChain () call
  • Callback result, request removed from runningAsyncCalls queue

At this point, a synchronous and asynchronous request general process we’ve combed out, all roads lead to Rome, both the core of the implementation is RealCall getResponseWithInterceptorChain (), the following analysis of the method.

3.3 getResponseWithInterceptorChain () analysis

GetResponseWithInterceptorChain () code is as follows:

RealCall.java Response getResponseWithInterceptorChain() throws IOException { // Build a full stack of interceptors. List<Interceptor> interceptors = new ArrayList<>(); interceptors.addAll(client.interceptors()); / / add custom interceptors interceptors. Add (new RetryAndFollowUpInterceptor (client)); // Add interceptors.add(new BridgeInterceptor(client.cookiejar ())); Add (new CacheInterceptor(client.internalCache())); // Add (new ConnectInterceptor(client)); // Add connection interceptor if (! forWebSocket) { interceptors.addAll(client.networkInterceptors()); } interceptors.add(new CallServerInterceptor(forWebSocket)); // The last interceptor, Chain = new RealInterceptorChain(Interceptors, transmitter, null, 0, originalRequest, this, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis()); boolean calledNoMoreExchanges = false; try { Response response = chain.proceed(originalRequest); if (transmitter.isCanceled()) { closeQuietly(response); throw new IOException("Canceled"); } return response; } catch (IOException e) { calledNoMoreExchanges = true; throw transmitter.noMoreExchanges(e); } finally { if (! calledNoMoreExchanges) { transmitter.noMoreExchanges(null); }}}Copy the code

This approach does two main things

  • Add custom interceptors and frameware-provided interceptors to the list in order to form a chain of interceptors.
  • The chain of interceptors starts at index=0, takes an instance of the RealInterceptorChain type, calls proceed(), generates Response, and returns it layer by layer.

RealInterceptorChain$proceed()

RealInterceptorChain.java public Response proceed(Request request, Transmitter transmitter, @Nullable Exchange exchange) 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.exchange ! = null && ! this.exchange.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.exchange ! = 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, transmitter, exchange, index + 1, request, call, 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 (exchange ! = 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; }Copy the code

The RealInterceptorChain$proceed() does a lot of validation, such as: Whether the same request of the domain name and port change, request is executed multiple times, whether the response is empty, whether the response body is empty, in the method is the most important thing is to get to the current interceptor next interceptor, and proceed to enter the next interceptor (), that is to say when the interceptor did not get response, issued a request to the next interceptors, Until the last interceptor in the chain, the response is returned layer by layer from the last interceptor to the interceptor with index=0.

Interceptors are added in the reverse order in which they are executed, with the CallServerInterceptor added last being the first to execute and producing a Response, and the interceptor added first being the last to execute.

The request delivery of the interceptor chain is similar to the event delivery model in Android system, but there is a difference between the two. The order of event delivery is the same, both are top-down, until the bottom of the tree or the last element of the list. However, for event delivery, the child view consumes the event, and the event is not processed in the parent view. After the last interceptor completes execution, Response is wrapped layer by layer back to the previous interceptor until the chain index=0 interceptor proceed completes execution.

The interceptor dissects the portal

Four,

In one picture