preface

Okhhtp is a great web framework from Square. It’s safe to say that most Android developers on the market today use this excellent framework for web requests. Below, I will be combined with the source code to analyze the internal implementation principle of this network framework, so as to understand the purpose of gas.

use

OkHttp is very simple to use, I write a general method to provide you see.

Okhttpclient okHttpClient = new okHttpClient.builder ().build(); Request Request = new request.builder ().url()"www.baidu.com") .build(); Call = okHttpClient.newCall(request); // Synchronize request try {call.execute(); } catch (IOException e) { e.printStackTrace(); } // Call. Enqueue (new)Callback() { @Override public void onFailure(Call call, IOException e) {Override public void onResponse(Call Call, Throws IOException {//TODO Successful operation}});Copy the code

As you can see, OkHttp is not very complicated to use, and you can execute a request by following the procedure above. Because this article focuses on analyzing the source code, it doesn’t go into too much detail about the use of OkHttp.

Let’s start analyzing the source code.

The Dispatcher dispenser

The Dispatcher dispatcher is one of the most important parts of OkHttp. It is used to manage synchronous and asynchronous requests.

Let’s click on The OkhttpClient Builder() to see the source code

public Builder() {
   1   dispatcher = new Dispatcher();
   2   protocols = DEFAULT_PROTOCOLS;
   3   connectionSpecs = DEFAULT_CONNECTION_SPECS;
   4   eventListenerFactory = EventListener.factory(EventListener.NONE);
   5   proxySelector = ProxySelector.getDefault();
   6   cookieJar = CookieJar.NO_COOKIES;
   7   socketFactory = SocketFactory.getDefault();
   8   hostnameVerifier = OkHostnameVerifier.INSTANCE;
   9   certificatePinner = CertificatePinner.DEFAULT;
   10   proxyAuthenticator = Authenticator.NONE;
   11   authenticator = Authenticator.NONE;
   12   connectionPool = new ConnectionPool();
   13   dns = Dns.SYSTEM;
   14   followSslRedirects = true;
   15   followRedirects = true;
   16   retryOnConnectionFailure = true;
   17   connectTimeout = 10_000;
   18   readTimeout = 10_000;
   19   writeTimeout = 10_000;
   20   pingInterval = 0;
    }

Copy the code

You can see that there is a dispatcher = new Dispatcher() in the place marked 1. The original dispatcher was created in the Builder of OkHttpClient.

Then click Dispatcher to see the internal code of the Dispatcher class

public final class Dispatcher { private int maxRequests = 64; private int maxRequestsPerHost = 5; private Runnable idleCallback; /** Thread pool */ private ExecutorService; Private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>(); Private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>(); / / Private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>(); Private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>(); }Copy the code

Some code has been cut down and we just look at some variables declared inside it. There are four things declared inside the Dispatcher, three task queues:

ReadyAsyncCalls, an asynchronous queue ready to execute the task,

RunningAsyncCalls asynchronous queue that is executing tasks,

RunningSyncCalls;

And a thread pool executorService;

How does the Dispatcher perform synchronous and asynchronous requests?

A synchronous request

We click on the execute() method of dispatcher.execute() to see the source code


@Override 
public Response execute() 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");
      returnresult; } finally { client.dispatcher().finished(this); }}Copy the code

1. The code first modifies the code block with a synchronize lock, indicating that the code can only be serial and cannot support parallelism.

If the task is Executed again, “Already Executed” is thrown. You cannot repeat the task.

Dispatcher.execute (this) is then called to perform the synchronization task

4. Then call a connector chain getResponseWithInterceptorChain (), will iterate through all the interceptors, result the result will be returned. (More on interceptors later, not here)

5. Finally call dispatcher.finished(this) again to end all requests.

So this process we focus on the analysis of step 3 and step 5

So let’s click on the execute(this) method in step 3 and see what’s going on there

  
  synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
  }

Copy the code

As you can see, this method is still modified with Synchronize so that multiple threads are only serial. Then add the RealCall task to the runningSyncCalls runtime synchronization task queue.

Execute () is used to add tasks to a task queue. This is in the dispatcher.Finish (this) method in step 5.

Click Finish (this) to go inside and see the code

  void finished(RealCall call) {
    finished(runningSyncCalls, call, false);
  }

  private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
   1  int runningCallsCount;
   2  Runnable idleCallback;
   3  synchronized (this) {
   4   if(! calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
   5   if(promoteCalls) promoteCalls(); 6 runningCallsCount = runningCallsCount(); 7 idleCallback = this.idleCallback; 8} 9if (runningCallsCount == 0 && idleCallback != null) {
   10   idleCallback.run();
    }
  }

Copy the code

Finished (Deque calls, T calls, Boolean promoteCalls)

If (promoteCalls) promoteCalls();

If promoteCalls is true, the promoteCalls() method is executed. So focus on what happens in the promoteCalls() method. Let’s click on it again

private void promoteCalls() {1if (runningAsyncCalls.size() >= maxRequests) return; 
   2  if (readyAsyncCalls.isEmpty()) return; 

   3  for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
   4   AsyncCall call = i.next();

   5   if(runningCallsForHost(call) < maxRequestsPerHost) { 6 i.remove(); 7 runningAsyncCalls.add(call); 8 executorService().execute(call); 9} 10if (runningAsyncCalls.size() >= maxRequests) return; }}Copy the code

As you can see, the first two steps determine whether the maximum number of asynchronous queues at runtime exceeds 64. If the maximum number of asynchronous queues exceeds 64, the return ends. If the asynchronous ready queue isEmpty(isEmpty()), then return also ends.

Step 3 begins the for loop, fetching the task from the wait queue and adding it to the runningAsyncCalls.add(call); ExecutorService ().execute(call), which completes a request.

So to summarize the synchronization request:

A network request call is added to the runtime synchronization queue runningSynCall via the Dispatcher dispatcher, and then a request is completed by assigning tasks from the task queue to the thread pool via a promoteCall() method.

An asynchronous request

Let’s look at asynchronous requests

// Call. Enqueue (newCallback() { @Override public void onFailure(Call call, IOException e) {Override public void onResponse(Call Call, Throws IOException {//TODO Successful operation}});Copy the code

Let’s click on the enqueue() method to look at the code

void enqueue(Callback responseCallback);
Copy the code

You see it’s one line of code, it’s pretty neat

 @Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

Copy the code

Let’s skip to client.dispatcher().enqueue(new AsyncCall(responseCallback)); In the enqueue() method of the

  synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else{ readyAsyncCalls.add(call); }}Copy the code

And you can see that we’re making a judgment here, If the number of tasks in the running asynchronous queue is smaller than maxRequests and runningCallsForHost(call) < maxRequestsPerHost, add tasks to runningAsyncCalls and send them to the thread pool for execution. Otherwise, the task is added to the asynchronous ready queue as a wait.

So what are maxRequests and maxRequestsPerHost? We saw when we analyzed Dispatcher that there were several variables initialized in this class and these two variables were among them

public final class Dispatcher {
  private int maxRequests = 64;
  private int maxRequestsPerHost = 5;
  
  }

Copy the code

If maxRequests is 64, the maximum number of asynchronous requests is 64, and the maximum number of hosts is 5. If the number of running tasks exceeds these two numbers, the task cannot be sent to the thread pool for execution. Instead, the task can be placed directly in the readyAsyncCalls queue to wait.

 @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else{ responseCallback.onFailure(RealCall.this, e); } } finally { client.dispatcher().finished(this); }}}Copy the code

You can see that the result is still returned by a chain of connectors;

Then a few times to judge, judge whether need to redirect the reconnection, if retryAndFollowUpInterceptor isCanceled () represents the connection to cancel failed, The callback responseCallback. OnFailure (RealCall. This new IOException (” Canceled “)); If success callback responseCallback. OnResponse (RealCall. This response).

Finally, we return to the Finish (this) method, which takes the contents of the runqueue out to the thread pool for execution.

Summarize the asynchronous request process

An asynchronous request

Dispatcher.enqueue () will have two checks to determine whether the total number of tasks in the runningAsyncCalls queue exceeds 64 and whether the number of hosts in the network request exceeds 5. If not, add tasks to the running task queue. The task is then handed off to the thread pool for execution. If it does, it goes directly to readyAsyncCalls in the ready to wait task queue.

2. After perform a interceptor chain getResponseIntecepterChain () to return to return the results

3. To make a judgment, whether you need this task to reconnect, whether be cancelled retryAndFollowUpInterceptor isCanceled (), if cancelled certificate failed to perform the task, in the thread of the callback onFailure () method, If successful, the onResponse() method is called back.

4. Finally call dispatcher.Finish (this) with a promoteCalls() method that adjusts the tasks in the queue. By adding the task from the readyAsyncCalls queue to the asynchronous run task queue again and handing it off to the thread pool for execution.

This is the flow of an asynchronous request.

OkHttp interceptor

If OkHttp’s best and brightest feature is anything, I’d say it’s the interceptor feature without hesitation. Interceptors are arguably the most important feature of OkHttp.

What is an interceptor?

My personal understanding is that when you launch a real sense of the network request, the need to operate or through some checkpoints, only through this pass, can really launch a real sense of the network request.

Let’s look at how a complete interceptor request flow is implemented.


 @Override public Response execute() throws IOException {
    synchronized (this) {
   1   if (executed) throw new IllegalStateException("Already Executed");
   2   executed = true;
    }
    captureCallStackTrace();
    try {
   3   client.dispatcher().executed(this);
   4   Response result = getResponseWithInterceptorChain();
   5   if (result == null) throw new IOException("Canceled");
   6   returnresult; } finally { 7 client.dispatcher().finished(this); }}Copy the code

We’ll put our code in the RealCall class and find the execute() method, which of course has two execute() methods, one for asynchronous requests and one for synchronous requests. Let’s take synchronous requests as an example (asynchronous is the same thing in interceptors).

See marked up in the third row, the dispenser dispatcher to perform first executed (this) synchronization method, and then in the fourth guild have called a getResponseWithInterceptorChain () method, performed after this method is available as a result, Then return the result result. While getResponseWithInterceptorChain () is to perform the interceptor chain main core method. Let’s focus on what this method does.

GetResponseWithInterceptorChain () :

Response getResponseWithInterceptorChain() throws IOException { // Build a full stack of interceptors. List<Interceptor>  interceptors = new ArrayList<>(); interceptors.addAll(client.interceptors()); interceptors.add(retryAndFollowUpInterceptor); interceptors.add(new BridgeInterceptor(client.cookieJar())); interceptors.add(new CacheInterceptor(client.internalCache())); interceptors.add(new ConnectInterceptor(client));if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }


Copy the code

Click on this method and we’ll look at it step by step.

First, we create a collection interceptors. The generic type of this List collection is Interceptor, the Interceptor type. That is, this collection is dedicated to storing interceptors.

This is followed by a lot of Add (), which adds interceptors in sequence to the collection.

And we see,

Added a custom common interceptor, client.interceptors();

Retry or redirect the interceptor retryAndFollowUpInterceptor;

BridgeInterceptor, which sets some request headers, compacts data, and sets some cookies;

Setting a cache or using a CacheInterceptor CacheInterceptor;

The ConnectInterceptor multiplexed join operation has a connection pool in it

Another custom interceptor, networkInterceptors, that can do some custom operations before making a real network request

The CallServerInterceptor request interceptor that actually makes a network request

After adding all interceptors to the collection, put the collection into a class called RealInterceptorChain, finally execute a process() method on the RealInterceptorChain object, and return the result.

This is a complete interceptor process.

When you add interceptors to a RealInterceptorChain class, you need to add interceptors to a RealInterceptorChain class. When you add interceptors to a RealInterceptorChain class, you need to add interceptors to a RealInterceptorChain class. I don’t know why the interceptor can be executed by putting it into a collection, how he can execute it layer by layer I don’t know.

To be honest, when I first looked at this code, the only thing I could understand was how to define the collection and put the interceptor into the collection.

Let’s answer each of these questions one at a time.

First, why put a collection of interceptors into the constructor of a class called RealInterceptorChain?

Let’s click inside this class to see the source code.

public final class RealInterceptorChain implements Interceptor.Chain { private final List<Interceptor> interceptors; private final StreamAllocation streamAllocation; private final HttpCodec httpCodec; private final RealConnection connection; private final int index; private final Request request; private int calls; public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection, int index, Request request) { this.interceptors = interceptors; this.connection = connection; this.streamAllocation = streamAllocation; this.httpCodec = httpCodec; this.index = index; this.request = request; }}Copy the code

Since this class has a lot of code, I will post part of the code here and let’s take a look at it step by step.

As you can see, this class starts with several variables defined, such as List, StreamAllocation, HttpCodec, RealConnection, and so on. And the reason why you define this variable is because you’re going to need it later, and we’ll talk about where you’re going to use it. So since we’re going to use these variables, we definitely have to give it an initial value, otherwise what do you do? So where do these initial values come from? That’s what we pass in through the constructor. Just as we usually define a JavaBean, we pass values in the constructor for variable assignment, so this is also the case. We will need to operate on the List interceptor in a method of the RealInterceptorChain class later, so we pass it in the constructor first.

This explains why we pass the List collection with interceptors into the RealInterceptorChain. The class has methods that need to operate on interceptors, so we need to pass them in first.

The second question, with so many interceptors defined, is how do you call them layer by layer?

To answer this question, we need to look at the next method, proceed(), in which the operation is performed.


  public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

     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");
    }

     chain.proceed().
    if(this.httpCodec ! = null && calls > 1) { throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }

    RealInterceptorChain next = new RealInterceptorChain(
        interceptors, streamAllocation, httpCodec, connection, index + 1, request);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

     chain.proceed().
    if(httpCodec ! = null && index + 1 < interceptors.size() && next.calls ! = 1) { throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    return response;
  }


Copy the code

This code is preceded by three if judgments, and throws some corresponding exception information, this is to do some fault tolerance, not much to look at. Let’s look at the main part.


    1 RealInterceptorChain next = new RealInterceptorChain(
        interceptors, streamAllocation, httpCodec, connection, index + 1, request);
    2 Interceptor interceptor = interceptors.get(index);
    3 Response response = interceptor.intercept(next);

Copy the code

These are the three most important lines of code in the proceed() method.

The first line of code shows that we are instantiating the RealInterceptorChain class again and passing in almost the same parameters, except for index (index + 1)

The second line is the interceptor that retrieves the index position from the interceptor’s List collection using the get() method

The third line is the interceptor taken out in the second step to execute the Intercept () method, which executes the interceptor.

RealInterceptorChain = RealInterceptorChain = RealInterceptorChain = RealInterceptorChain = RealInterceptorChain = index + 1

The reason for this confusion is that we are looking at these three lines of code in isolation, and it is hard to read them without referring to the constructor of the RealInterceptorChain class.

RealInterceptorChain (RealInterceptorChain) : RealInterceptorChain (RealInterceptorChain) : RealInterceptorChain (RealInterceptorChain) : RealInterceptorChain (RealInterceptorChain) : RealInterceptorChain The present proceed() method.

The purpose of instantiating the RealInterceptorChain class is to update the constructor variable. Which variable is updated? That’s this index right there.

The reason we keep passing in (index + 1) is to keep incrementing this index, index = index + 1.

So is it infinitely self-increasing? Clearly not, it is incremented conditionally, which is the first if judgment in the proceed() method

 if (index >= interceptors.size()) throw new AssertionError();
Copy the code

When index increments beyond the maximum number of interceptors in the collection, size(), it cannot increse and throws an exception.

In prceed(), we need to update the new RealInterceptorChain() class again and again. The reason is to update the index variable and increment it by +1

Let’s look at the second line of code

Interceptor interceptor = interceptors.get(index);
Copy the code

Interceptors get the index value from the collection interceptors, i.e. the interceptor that gets the index position in the collection.

Get (index) = interceptors.get(index) = interceptors.get(index) = interceptors.get(index) = interceptors.get(index) Allow the collection to continually pull out the next interceptor.

For example, if the first line of code updates the index so that it increments by +1 (index = index +1), then the second line will not use the index value, but will use (index +1), and then add a margin to the index increment with the initial if judgment. To iterate over and retrieve interceptors from the List collection.

Finally, there is the third line of code, which is simple enough to take the interceptor out of the collection and execute it directly.

Sum up the proceed() method

The purpose of this method is to continuously traverse the collection of interceptors, retrieve interceptors from the collection, and then execute the next interceptor through the Intercept () method to achieve the purpose of chain call.

At this point, the overall interceptor invocation step is complete.

I’ll write some separate content and source analysis for the main interceptors and custom interceptors later.

RetryAndFollowUpInterceptor interceptor

This is called the Request Redirection interceptor, and its main function is to automatically reconnect after a request fails.


  @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();

    1 streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()), callStackTrace);

    2 int followUpCount = 0;
    3 Response priorResponse = null;
    4 while (true) {5if (canceled) {
   6     streamAllocation.release();
   7     throw new IOException("Canceled");
   8   }

   9   Response response = null;
   10   boolean releaseConnection = true;
   11   try {
   12     response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
   13     releaseConnection = false;
   14   } catch (RouteException e) {
   15     if(! recover(e.getLastConnectException(),false, request)) {
   16       throw e.getLastConnectException();
   17     }
   18     releaseConnection = false;
   19     continue; 20 } catch (IOException e) { 21 boolean requestSendStarted = ! (e instanceof ConnectionShutdownException); 22if(! recover(e, requestSendStarted, request)) throw e; 23 releaseConnection =false;
   24     continue;
   25   } finally {
   26     if(releaseConnection) { 27 streamAllocation.streamFailed(null); 28 streamAllocation.release(); 29} 30} 31if(priorResponse ! = null) { 32 response = response.newBuilder() 33 .priorResponse(priorResponse.newBuilder() 34 .body(null) 35 .build()) 36 .build(); 37 } 38 Request followUp = followUpRequest(response); 39if (followUp == null) {
   40     if (!forWebSocket) { 41 streamAllocation.release(); 42} 43return response;
   44   }

   45   closeQuietly(response.body());

   46   if (++followUpCount > MAX_FOLLOW_UPS) {
   47     streamAllocation.release();
   48     throw new ProtocolException("Too many follow-up requests: "+ followUpCount); 49} 50if (followUp.body() instanceof UnrepeatableRequestBody) {
   51     streamAllocation.release();
  52      throw new HttpRetryException("Cannot retry streamed HTTP body", response.code()); 53} 54if (!sameConnection(response, followUp.url())) {
   55     streamAllocation.release();
   56     streamAllocation = new StreamAllocation(
   57         client.connectionPool(), createAddress(followUp.url()), callStackTrace);
   58   } else if(streamAllocation.codec() ! = null) { 59 throw new IllegalStateException("Closing the body of " + response
   60         + " didn't close its backing stream. Bad interceptor?"); 61 } 62 request = followUp; 63 priorResponse = response; }}Copy the code

The code itself is not analyzed, so there is nothing too complicated about it, but one thing I should note here is that the number of reconnections is not infinite, it actually has a maximum number. On line 46, we can see that


     if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

Copy the code

If followUpCount > MAX_FOLLOW_UPS will stop reconnection and raise an exception. What is MAX_FOLLOW_UPS?


private static final int MAX_FOLLOW_UPS = 20;

Copy the code

As you can see, the maximum number of reconnection is 20 times, that is to say, RetryAndFollowUpInterceptor interceptor reconnection number is not infinite, but up to 20 times, more than the number is no longer to reconnect.

BridgeInterceptor interceptor

This interceptor can be called a bridging interceptor. The code is as follows:


 @Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();

    RequestBody body = userRequest.body();
    if(body ! = null) { MediaType contentType = body.contentType();if(contentType ! = null) { requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      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 (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection"."Keep-Alive");
    }
    
    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()) { requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }

    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)));
    }

    return responseBuilder.build();
  }

Copy the code

The main function of this interceptor is to set headers for requests, such as “content-Type”, “Content-Length”, “Host”, “Cookie”, etc.

Discuss OkHttp’s internal caching in conjunction with the CacheInterceptor CacheInterceptor

OkHttp’s caching process is a process worth discussing in detail. So let’s talk about OkHttp’s cache internals in conjunction with the cache interceptor.

How do I use the cache?

To use OkHttp’s cache, you can actually set it up when you initialize OkHttpClient.


            OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .cache(new Cache(new File("Cache location"),1024))
                .build();

Copy the code

As you can see, there is a constructor for cache() with an argument that instantiates a cache class, which is used for caching.

We click inside the class to see the constructor

  public Cache(File directory, long maxSize) {
    this(directory, maxSize, FileSystem.SYSTEM);
  }

Copy the code

The constructor takes two arguments: File, which records the location of the cache, and maxSize, which records the size of the cache. These two parameters are used to store the cache in the appropriate location and set the size of the cache.

The main methods in the Cache class are get() and PUT (). Let’s look at these two methods.

Cache storage method put()

Let’s look at the storage method put()


   1 CacheRequest put(Response response) {
   2  String requestMethod = response.request().method();

   3 if (HttpMethod.invalidatesCache(response.request().method())) {
   4   try {
   5     remove(response.request());
   6   } catch (IOException ignored) {
   7   }
   8   returnnull; 9} 10if(! requestMethod.equals("GET")) {11returnnull; 12} 13if (HttpHeaders.hasVaryAll(response)) {
   14   return null;
   15 }

   16 Entry entry = new Entry(response);
   17 DiskLruCache.Editor editor = null;
   18 try {
   19   editor = cache.edit(key(response.request().url()));
   20   if (editor == null) {
   21     return null;
   22   }
   23   entry.writeTo(editor);
   24   return new CacheRequestImpl(editor);
   25 } catch (IOException e) {
   26   abortQuietly(editor);
   27   returnnull; 28}}Copy the code

OkHttp caches only GET requests. It does not cache other requests, such as POST requests. OkHttp caches only GET requests. Return NULL in case of any other request

We start at line 16, where we initialize an Entry array that is used to write some cache information.

We then retrieve a DiskLruCache class using the URL we requested as the key via the cache.edit() method. In other words, the OkHttp cache is stored based on the DiskLruCache algorithm, using the requested URL as the key to query the results.

Finally, write the cache results to the entry array using the entry.writeto (Editor) method to complete a cache.

Cache fetch method get()

Let’s look at the cache fetch method


  Response get(Request request) {
    1 String key = key(request.url());
    2 DiskLruCache.Snapshot snapshot;
    3 Entry entry;
    4 try {
    5   snapshot = cache.get(key);
    6   if (snapshot == null) {
    7     return null;
    8   }
    9 } catch (IOException e) {
    10   return null;
    11 }

    12 try {
    13  entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    14 } catch (IOException e) {
    15  Util.closeQuietly(snapshot);
    16  return null;
    17 }

    18 Response response = entry.response(snapshot);

    19 if(! entry.matches(request, response)) { 20 Util.closeQuietly(response.body()); 21returnnull; 22 23}return response;
  }

Copy the code

First, the requested URL is used to obtain the key value. Then, the DiskLruCache algorithm has an object called Snapshot. This object is called cache Snapshot, which is used to record the cache at a certain time.

If the value is null, it means that there is no cache at this time. Null is returned. Otherwise, the cache snapshot taken at this point is used to retrieve the cache result from the Entry array.

Finally, after obtaining the cache, it does not end, and there is another layer of judgment


if(! entry.matches(request, response)) { Util.closeQuietly(response.body());return null;
    }

Copy the code

This is to determine whether the cache we get matches the cache we requested. Because we know that the cache we finally get must be the one we cached when we made the request. If it doesn’t match, it means that the cache we got is not the one we used when we made the network request, so there is no need to return it. Matches, on the other hand, represent a cache.

To this, the cache storage and access to the method of analysis is completed. So, where does OkHttp call the two methods that fetch and store the cache? That’s what the cache interceptor’s capabilities are going to focus on.

CacheInterceptor — CacheInterceptor

@Override public Response intercept(Chain chain) throws IOException { 1 Response cacheCandidate = cache ! = null ? cache.get(chain.request()) : null; 2 long now = System.currentTimeMillis(); 3 CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get(); 4 Request networkRequest = strategy.networkRequest; 5 Response cacheResponse = strategy.cacheResponse; 6if (cache != null) {
    7  cache.trackResponse(strategy);
    8 }

    9 if(cacheCandidate ! = null && cacheResponse == null) { 10 closeQuietly(cacheCandidate.body()); 11}if (networkRequest == null && cacheResponse == null) {
    12  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();
    }

    
    13if (networkRequest == null) {
    14  return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    15 }

    16 Response networkResponse = null;
    17 try {
    18  networkResponse = chain.proceed(networkRequest);
    19 } finally {
    20  if(networkResponse == null && cacheCandidate ! = null) { 21 closeQuietly(cacheCandidate.body()); 22} 23} 24if(cacheResponse ! = null) { 25if (networkResponse.code() == HTTP_NOT_MODIFIED) {
    26    Response response = cacheResponse.newBuilder()
    27        .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
    28    networkResponse.body().close();

    29    cache.trackConditionalCacheHit();
    30   cache.update(cacheResponse, response);
    31    returnresponse; 32}else {
    33    closeQuietly(cacheResponse.body());
    34  }
    35 }

    36 Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    37 if(cache ! = null) { 38if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
    39    CacheRequest cacheRequest = cache.put(response);
    40    returncacheWritingResponse(cacheRequest, response); 41} 42if (HttpMethod.invalidatesCache(networkRequest.method())) {
    43    try {
    44      cache.remove(networkRequest);
    45    } catch (IOException ignored) {
    46      // The cache cannot be written.
    47    }
    48  }
    49 }

    50 returnresponse; 51}Copy the code

This is the code that the cache interceptor primarily executes. As you can see in the third line, there is a class called CacheStrategy, which is essentially the heart of the cache interceptor, deciding whether to use the cache or the network connection to fetch data.

So let’s click through and take a look inside this class.


public final class CacheStrategy {
  
  public final Request networkRequest;

  public final Response cacheResponse;

  CacheStrategy(Request networkRequest, Response cacheResponse) {
    this.networkRequest = networkRequest;
    this.cacheResponse = cacheResponse;
  }
  
   public CacheStrategy get() {
      CacheStrategy candidate = getCandidate();

      if(candidate.networkRequest ! = null && request.cacheControl().onlyIfCached()) {return new CacheStrategy(null, null);
      }

      returncandidate; }}Copy the code

This is part of the code. We can see that there are two classes defined here, one is Request and one is Response. We then get the CacheStrategy class by using getCandiate() in the get() method, and then manipulate the class depending on the conditions to see if we want to use caching or network requests.

Let’s take a look at what conditions we use caching and what conditions we use network requests.


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();
    }
    

Copy the code

When the network request is deemed empty (i.e., there is no network) and the cache is empty, a 504 response code is thrown to indicate a gateway timeout

if(cacheResponse ! = null) {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()); }}Copy the code

When the network cache is not empty and the request code is networkResponse.code() == HTTP_NOT_MODIFIED, 304 is unmodified, we pull the data directly from the cache and process it. This is where the cache is used.


 Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if(cache ! = null) {if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        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.
        }
      }
    }

Copy the code

Put (response) is called to store the data directly into the cache. We have analyzed how the data is stored internally, so we won’t go into details here.

At this point, the entire cache interceptor is written.

To summarize the cache interceptor

Inside the cache interceptor is a class that relies on a cache policy to control whether or not the cache is used. If there is neither network nor cache, it will directly throw a 504 gateway timeout warning, and then determine that if the request is not modified, i.e. 304, it will return the cache information directly from the cache. If there is no cache, obtain the cache from the network and store the cache for future use.

To be continued…