preface

Since the previous project was built with MVP architecture, which is a combination of RxJava + Glide + OKHttp + Retrofit and other open source frameworks, it was just on the use level without in-depth research. Recently, we plan to attack all of them. Students who have not paid attention to them can pay attention to a wave first. After reading this series of articles, you’ll be much more comfortable handling questions (whether in an interview or on the job) if you know how they work.

Android picture loading framework Glide 4.9.0 (a) From the perspective of source code analysis Glide execution process

Android Image loading framework Glide 4.9.0 (2) Analysis of Glide cache strategy from the perspective of source code

From the perspective of source code analysis of Rxjava2 basic execution process, thread switching principle

OKHttp3 (a) synchronous and asynchronous execution process is analyzed from the perspective of source code

Analyze the charm of OKHttp3 (ii) interceptor from the source point of view

OKHttp3 (iii) cache strategy is analyzed from a source code perspective

Analyze Retrofit network request from source code point of view, including RxJava + Retrofit + OKhttp network request execution process

Interceptor interceptor

In the last analysis from the perspective of source OKHttp3 (a) synchronous and asynchronous execution process, finally we know in getResponseWithInterceptorChain () function to finish the last request and response, so how are internal to complete the request, And calls back the server’s response data to the calling layer.

  Response getResponseWithInterceptorChain(a) throws IOException {
    // Build a container stack of interceptor calls
    List<Interceptor> interceptors = new ArrayList<>();
    // Global interceptor added in addInterceptor when configuring OKHttpClient
    interceptors.addAll(client.interceptors());
    // Error, redirect interceptor
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    // Bridge interceptor, bridge application layer and network layer, add the necessary headers
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    // Cache processing, last-Modified, ETag, DiskLruCache, etc
    interceptors.add(new CacheInterceptor(client.internalCache()));
    // Connect interceptor
    interceptors.add(new ConnectInterceptor(client));
    // Whether it is a webSocket
    if(! forWebSocket) {/ / by okHttpClient Builder# addNetworkInterceptor ()
    // The incoming interceptor only applies to non-web requests
      interceptors.addAll(client.networkInterceptors());
    }
    // The interceptor that actually accesses the server
    interceptors.add(new CallServerInterceptor(forWebSocket));
		
    // The caller that actually executes the interceptor
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null.0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
      // Start executing
      Response response = chain.proceed(originalRequest);
      // Whether to cancel
      if (transmitter.isCanceled()) {
        / / close
        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

There’s not much code in the function, but it’s really the essence. We know that from the above code and comments

  1. Start by creating a container to hold the interceptor
  2. Add global interceptor and apply interceptor
  3. createRealInterceptorChainObject interceptor and pass in some configuration of the interceptor container, emitter, request data, and so on
  4. The last callRealInterceptorChainchain.proceed(originalRequest);Function is what really makes these interceptors work.

In the last article, I briefly introduced interceptors and mentioned the chain of responsibility mode. However, this interceptor uses the RealInterceptorChain object to open the chain of responsibility task delivery. It feels like a CEO delivering tasks and passing them layer by layer. Similar to touch feedback event passing in Android source code, the heart of OKHttp is the interceptor. Let’s start with a step-by-step analysis of the subtlety of the OKHttp interceptor.

RealInterceptorChain

Proceed (originalRequest) : The interceptor task is performed on the Chain.proceed (originalRequest) of the RealInterceptorChain

public final class RealInterceptorChain implements Interceptor.Chain {...// Omit member variable attributes

  public RealInterceptorChain(List < Interceptor > interceptors, / / all Interceptor Transmitter Transmitter, / / transmitters @ Nullable Exchange, Exchange, / / encapsulation of OKIO request data operationint index, Request request, Call call,
    int connectTimeout, int readTimeout, 
    int writeTimeout
  ){...// Omit the assignment code
  }
  
  / / external getResponseWithInterceptorChain function calls
  public Response proceed( Request request, Transmitter transmitter, @Nullable Exchange exchange )throws IOException {
    
    // Index cannot exceed the interceptor container size
    if (index >= interceptors.size()) throw new AssertionError();

 		// Throw an exception if a request connection already exists
    if (this.exchange ! =null&&!this.exchange.connection().supportsUrl(request.url())) {
      ...// Throw exception code omitted
    }

    // Make sure the open call is unique, otherwise throw an exception. I think this will only make the code more robust.
    if (this.exchange ! =null && calls > 1) {...// Throw exception code omitted
    }

    //1. Create an object for the next interceptor to execute
    RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,
        index + 1, request, call, connectTimeout, readTimeout, writeTimeout);
    //2. Fetch the current interceptor
    Interceptor interceptor = interceptors.get(index);
    Call the intercept(Chain) method of the next interceptor, pass in the newly created RealInterceptorChain, and return Response
    Response response = interceptor.intercept(next);

		// Limit some judgments to make sure the program is robust
    if(exchange ! =null && index + 1< interceptors.size() && next.calls ! =1) {...// Throw exception code omitted
    }

		// If the response returned is empty, an exception is thrown
    if (response == null) {...// Throw exception code omitted
    }

    // If the response is empty, an exception is also thrown
    if (response.body() == null) {...// Throw exception code omitted
    }
    
    // Actually returns the server's response
    returnresponse; }}Copy the code

Take a look at comments 1, 2, and 3 above, which are the core code for distributing interceptor execution. Create a RealInterceptorChain inside the RealInterceptorChain and pass in the index + 1 parameter. Get (index + 1) interceptor Note 2 is to fetch the current interceptor, and note 3 is to execute the interceptor.

The RealInterceptorChain class is a recursive function interceptor.Intercept (next); There is an entry point for recursion. There is an exit point, but it is not in this class. It is in the CallServerInterceptor request and response interceptor. Equivalent to exports. So the RealInterceptorChain class is personally responsible for starting/stopping interceptors, kind of like interceptor calls delegate to RealInterceptorChain.

There must be a list. The get (index = 0) RetryAndFollowUpInterceptor interceptor first performed, the following start analysis error and redirect the interceptor.

RetryAndFollowUpInterceptor

As mentioned in the introduction of interceptor, it is an interceptor for error reconnection and redirection. Let’s take a look at its core code

public final class RetryAndFollowUpInterceptor implements Interceptor {
  
 
  @Override public Response intercept(Chain chain) throws IOException {
    // Get the current request
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    // Get the Transmitter object
    Transmitter transmitter = realChain.transmitter();
		
    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      // Prepare the connection
      transmitter.prepareToConnect(request);
			// Determine whether to cancel
      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }

      Response response;
      boolean success = false;
      try {
        // Pass the current request to the next interceptor
        response = realChain.proceed(request, transmitter, null);
        success = true;
      } catch (RouteException e) {
        // Check whether it can continue to use
        if(! recover(e.getLastConnectException(), transmitter,false, request)) 				{
          throw e.getFirstConnectException();
        }
        continue;
      } catch (IOException e) {
        booleanrequestSendStarted = ! (einstanceof ConnectionShutdownException);
        // Check whether it can continue to use
        if(! recover(e, transmitter, requestSendStarted, request))throw e;
        continue;
      } finally {
        // If the connection is not released successfully
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }

      // There is no exception
      if(priorResponse ! =null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null) .build()) .build(); }...// omit the code
      // Process the request header based on the response
      Request followUp = followUpRequest(response, route);
			// If it is empty, no redirection is required and the response is returned directly
      if (followUp == null) {
        if(exchange ! =null && exchange.isDuplex()) {
          transmitter.timeoutEarlyExit();
        }
        return response;
      }
			// Not empty, need redirection
      RequestBody followUpBody = followUp.body();
      if(followUpBody ! =null && followUpBody.isOneShot()) {
        return response;
      }

      closeQuietly(response.body());
      if (transmitter.hasExchange()) {
        exchange.detachWithViolence();
      }
			
      // The number of redirects cannot be greater than 20
      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
			// Try again based on the redirected requestrequest = followUp; priorResponse = response; }}}Copy the code

According to the above code analysis, the following points are mainly done

  1. Get the current request object, and get the Transmitter object
  2. Ready to connect, in fact, the real connection is inConnectInterceptorThe interceptor
  3. Invoke the next interceptor, i.eBridgeInterceptorPass the request to it in preprocessing.
  4. Check whether exceptions occur during the connection and determine whether to continue the connection
  5. If not, release the resource
  6. Determine whether a reconnection operation is required based on the response code
  7. If the number of reconnections is greater than 20, an exception is thrown; otherwise, the redirected request is retried.

In the current RetryAndFollowUpInterceptor realChain. Proceed (request, transmitter, null); The call goes to the BridgeInterceptor, the interceptor that the application interacts with the network.

BridgeInterceptor

When the last interceptor calls proceed, it will go to the current Intercept function

public final class BridgeInterceptor implements Interceptor {
  private finalCookieJar cookieJar; .// omit the constructor

  @Override public Response intercept(Chain chain) throws IOException {
    // Get the current Request Request
    Request userRequest = chain.request();
    // Get the Request configuration parameter Builder
    Request.Builder requestBuilder = userRequest.newBuilder();
		// Get the request body
    RequestBody body = userRequest.body();
    // Determine whether the request body is empty
    if(body ! =null) {// Not empty
      // Get the request body type
      MediaType contentType = body.contentType();
      if(contentType ! =null) {
        // Add header to the request body type
        requestBuilder.header("Content-Type", contentType.toString());
      }
			
      // Process the request body length
      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"); }}// Add header HOST HOST
    if (userRequest.header("Host") = =null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    // Add the connection state
    if (userRequest.header("Connection") = =null) {
      requestBuilder.header("Connection"."Keep-Alive");
    }

		// Whether to enable data compression -- Gzip is added by default
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") = =null && userRequest.header("Range") = =null) {
      transparentGzip = true;
      // Add gzip compression
      requestBuilder.header("Accept-Encoding"."gzip");
    }

    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if(! cookies.isEmpty()) {// Add cookies to header
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }
		/ / add the user-agent
    if (userRequest.header("User-Agent") = =null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }
		
    CacheInterceptor executes the next interceptor
    Response networkResponse = chain.proceed(requestBuilder.build());
		// Save the url and cookie
    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
		
    // Get the response and add some attributes
    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);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }
		// Return the response
    returnresponseBuilder.build(); }...// Omit some code
  
}
Copy the code

From the above code, we know that the BridgeInterceptor mainly does some pre-processing of the request header before calling the next interceptor.

CacheInterceptor

The BridgeInterceptor call ends up in the current Intercept. The BridgeInterceptor call is used to fetch and update the cache. Now let’s look at the implementation

public final class CacheInterceptor implements Interceptor {
  final @NullableInternalCache cache; .// Constructor omitted

  @Override public Response intercept(Chain chain) throws IOException {
    // If the cache is not empty, get the cache response according to the requestResponse cacheCandidate = cache ! =null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
		// Get the cache policy
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    // Get the request according to the cache policy
    Request networkRequest = strategy.networkRequest;
    // Get the cached responseResponse cacheResponse = strategy.cacheResponse; .// Omit some code
    // If the request and cache response are empty, the cache is forced and error code 504 is returned
    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 networkRequest is empty, the cache is also forcibly fetched
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    
    Response networkResponse = null;
    try {
      // Invoke the next interceptor
      networkResponse = chain.proceed(networkRequest);
    } finally{... }// If the cache is not empty
    if(cacheResponse ! =null) {
      // And the response code == previously defined 304
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        // Generate a response
        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 response
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else{ closeQuietly(cacheResponse.body()); }}// No cache usage, read 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
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
			
      // Check whether the cache is valid
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          // Delete invalid cache
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          
        }
      }
    }
    return response;
  }
Copy the code

The OKHttp cache mechanism will be covered in a separate article since it only covers interceptor calls and some basic processing logic. Just know that if the external OKHttpClient is configured with a cache (see the following code block, otherwise the cache will be empty). Caches are put, GET, and update. Since there is no caching policy, we call the next interceptor, ConnectInterceptor

File file = new File(Environment.getExternalStorageDirectory() + "/T01");
Cache cache = new Cache(file, 1024 * 1024 * 10);
OkHttpClient okHttpClient = new OkHttpClient.Builder().
                        addInterceptor(new LoggingInterceptor())
                        .cache(cache).
                        build();
Copy the code

ConnectInterceptor

(PS: Interceptor interceptor mainly refers to:Juejin. Cn/post / 684490…

After the cache interceptor completes execution, the next call chain is to connect the interceptor. Look at the code implementation:

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
    Request request = realChain.request();
    / / get the Transmitter
    Transmitter transmitter = realChain.transmitter();
    
    booleandoExtensiveHealthChecks = ! request.method().equals("GET");
    // Create a new Exchange
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
		// Call the proceed method, which calls the intercept method of the next interceptor, CallServerInterceptor
    returnrealChain.proceed(request, transmitter, exchange); }}Copy the code

The ConnectInterceptor has a simple internal code. First, it gets the Request and the Transmitter object. Exchange is responsible for writing data into the IO stream that creates the connection. Finally, call the CallServerInterceptor interceptor. NewExchange (Chain, doExtensiveHealthChecks) internal code implementation

  Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    synchronized (connectionPool) {
      // If no Exchanges throw an exception
      if (noMoreExchanges) {
        throw new IllegalStateException("released");
      }
      if(exchange ! =null) {...// omit exception code
    }
		
    // find an exchange dec using ExchangeFinder's find method
    ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
    // Create an Exchange and pass in the Exchange Ecodec instance codec, so the Exchange ecodec instance is held inside the Exchange
    Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);

    synchronized (connectionPool) {
      this.exchange = result;
      this.exchangeRequestDone = false;
      this.exchangeResponseDone = false;
      returnresult; }}Copy the code

ExchangeFinder object via the Transmitter in RetryAndFollowUpInterceptor early prepareToConnect method to create, it find methods is where the connection to create real, What is ExchangeFinder? ExchangeFinder is responsible for the creation of a connection, putting it into a pool, and taking it out of the pool if it already exists, so ExchangeFinder manages two important roles: RealConnection, RealConnectionPool, RealConnectionPool and RealConnection.

RealConnection

The real realization of the Connection, the realization of the Connection interface, internal use of Socket to establish a Connection, as follows:

public interface Connection {
    // Return the Route used for this connection
    Route route(a);

    // Return the Socket used for this connection
    Socket socket(a);

    // If HTTPS is used, TLS handshake information is returned for establishing the connection, otherwise null is returned
    @Nullable Handshake handshake(a);

    Protocol is an enumeration, such as HTTP1.1 and HTTP2
    Protocol protocol(a);
}

public final class RealConnection extends Http2Connection.Listener implements Connection {

    public final RealConnectionPool connectionPool;
    / / routing
    private final Route route;
    // Use this rawSocket internally to establish connections at the TCP layer
    private Socket rawSocket;
    // If HTTPS is not used, then socket == rawSocket, otherwise the socket == SSLSocket
    private Socket socket;
    / / TLS handshake
    private Handshake handshake;
    // Application layer protocol
    private Protocol protocol;
    / / HTTP2 connections
    private Http2Connection http2Connection;
    // The okio library's BufferedSource and BufferedSink are javaIO's input and output streams
    private BufferedSource source;
    private BufferedSink sink;


    public RealConnection(RealConnectionPool connectionPool, Route route) {
        this.connectionPool = connectionPool;
        this.route = route;
    }


    public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener eventListener) {
        / /...
    }

    / /...
}

Copy the code

RealConnection has a connect method that can be called externally to establish a connection. The connect method looks like this:

//RealConnection.java
public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener eventListener) {
    if(protocol ! =null) throw new IllegalStateException("already connected");

    RouteException routeException = null;
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

    // Route selection
    if (route.address().sslSocketFactory() == null) {
      if(! connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication not enabled for client"));
      }
      String host = route.address().url().host();
      if(! Platform.get().isCleartextTrafficPermitted(host)) {throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication to " + host + " not permitted by network security policy")); }}else {
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        throw new RouteException(new UnknownServiceException(
            "H2_PRIOR_KNOWLEDGE cannot be used with HTTPS")); }}// Start the connection
    while (true) {
      try {
        if (route.requiresTunnel()) {// If it is channel mode, establish channel connection
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          if (rawSocket == null) {
            // We were unable to connect the tunnel but properly closed down our resources.
            break; }}else {//1. Otherwise, the Socket connection is performed
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        // Establish an HTTPS connection
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        break;
      }
      / /... Omit exception handling

    if(http2Connection ! =null) {
      synchronized(connectionPool) { allocationLimit = http2Connection.maxConcurrentStreams(); }}}Copy the code

The connectSocket method is called to establish a Socket connection:

//RealConnection.java
private void connectSocket(int connectTimeout, int readTimeout, Call call,
                           EventListener eventListener) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();

    // Create sockets based on the proxy type
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    eventListener.connectStart(call, route.socketAddress(), proxy);
    rawSocket.setSoTimeout(readTimeout);
    try {
        //1. Establish a Socket connection
        Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    }
    / /... Omit exception handling

    try {
        // Get Socket input/output streams
        source = Okio.buffer(Okio.source(rawSocket));
        sink = Okio.buffer(Okio.sink(rawSocket));
    } 
     / /... Omit exception handling
}

Copy the code

Note 1: Platform is a compatible class implemented in okhttp based on different versions of Android platforms. We won’t go into details here. Platform’s connectSocket method eventually calls rawSocket’s Connect () method to establish its Socket connection. Okhttp can read data from source or write data to sink. Source and sink are BufferedSource and BufferedSink types. They come from okio, a library that encapsulates both java.io and java.nio. The okHTTP base relies on this library to read and write data. To learn more about Okio, see this article dismantling the Wheel: Dismantling Okio.

RealConnectionPool

Connection pool, used to manage connection objects RealConnection, as follows:

public final class RealConnectionPool {

    / / thread pool
    private static final Executor executor = new ThreadPoolExecutor(
        0 /* corePoolSize */,
        Integer.MAX_VALUE /* maximumPoolSize */.60L /* keepAliveTime */, 
        TimeUnit.SECONDS,
        new SynchronousQueue<>(), 
        Util.threadFactory("OkHttp ConnectionPool".true));
 
    boolean cleanupRunning;
    // Clean up connection tasks, executed in executor
    private final Runnable cleanupRunnable = () -> {
        while (true) {
            // Call the cleanup method to perform the cleanup logic
            long waitNanos = cleanup(System.nanoTime());
            if (waitNanos == -1) return;
            if (waitNanos > 0) {
                long waitMillis = waitNanos / 1000000L;
                waitNanos -= (waitMillis * 1000000L);
                synchronized (RealConnectionPool.this) {
                    try {
                        // Call the wait method to wait
                        RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
                    } catch (InterruptedException ignored) {
                    }
                }
            }
        }
    };

    // Double end queue, save connection
    private final Deque<RealConnection> connections = new ArrayDeque<>();

    void put(RealConnection connection) {
        if(! cleanupRunning) { cleanupRunning =true;
            // Use thread pools to perform cleanup tasks
            executor.execute(cleanupRunnable);
        }
        // Insert the new connection into the queue
        connections.add(connection);
    }

    long cleanup(long now) {
        / /...
    }

    / /...
}

Copy the code

RealConnectionPool internally maintains a thread pool to perform the cleanupRunnable connection task, as well as a double-ended queue connections to cache connections that have been created. To create a connection, you need to go through a TCP handshake, or a TLS handshake if you’re using HTTPS. Both handshake processes are time-consuming, so connections is needed to cache the connection for reuse. Okhttp supports 5 concurrent connections. By default, each connection has a keepAlive duration of 5 minutes. The amount of time that you stay alive.

When we first call RealConnectionPool’s PUT method to cache a new connection, if cleanupRunnable hasn’t been executed yet, it will first execute cleanupRunnable using the thread pool and then put the new connection into a two-ended queue. The cleanup method is called in cleanupRunnable, which returns the interval between now and the next cleanup, then calls the WIAT method to wait, then calls the cleanup method again, and so on. Let’s look at the cleanup logic for the cleanup method:

//RealConnectionPool.java
long cleanup(long now) {
    
    int inUseConnectionCount = 0;// The number of connections being used
    int idleConnectionCount = 0;// Number of idle connections
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    synchronized (this) {
        // Iterate over all connections, recording the number of idle connections and the number of connections in use
        for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
            RealConnection connection = i.next();

            / / if the connection is still in use, pruneAndGetAllocationCount through reference counting ways to judge whether a connection idle
            if (pruneAndGetAllocationCount(connection, now) > 0) {
                // Add 1 to the number of connections
                inUseConnectionCount++;
                continue;
            }
            
            // The connection is not in use

            // The number of free connections is increased by 1
            idleConnectionCount++;

            // Record the connection with the longest keepalive duration
            long idleDurationNs = now - connection.idleAtNanos;
            if (idleDurationNs > longestIdleDurationNs) {
                longestIdleDurationNs = idleDurationNs;
                // This connection will probably be removed because the idle time is too longlongestIdleConnection = connection; }}// Out of the loop

        // The default keepalive duration keepAliveDurationNs is 5 minutes. The maximum number of idle connections idleConnectionCount is 5
        if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) {// If the keepalive time of longestIdleConnection is greater than 5 minutes or the number of idle connections is greater than 5
            // Clear the longestIdleConnection connection from the queue
            connections.remove(longestIdleConnection);
        } else if (idleConnectionCount > 0) {// If the number of idle connections is less than 5 and the longestIdleConnection connection has not expired, clean up
            // Return the expiration time of the connection and clean it up next time
            return keepAliveDurationNs - longestIdleDurationNs;
        } else if (inUseConnectionCount > 0) {// If there are no free connections and all connections are still in use
            // Go back to keepAliveDurationNs and clean up after 5 minutes
            return keepAliveDurationNs;
        } else {
            // Reset cleanupRunning without any connection
            cleanupRunning = false;
            return -1; }}// After clearing the longestIdleConnection connection from the queue, close the socket for the connection, return 0, immediately clean up again
    closeQuietly(longestIdleConnection.socket());

    return 0;
}

Copy the code

From the cleanup method, okHTTP cleans up connections using the following logic:

1. Firstly, all connections are traversed, and idleConnectionCount and inUseConnectionCount are recorded. When recording the number of idle connections, longestIdleConnection with the longest idle time is also found. The connection is likely to be cleared;

2. After traversal, decide whether to clean longestIdleConnection according to the maximum idle time and the maximum number of idle connections.

2.1. If the idle time of longestIdleConnection is greater than the maximum idle time or the number of idle connections is greater than the maximum number of idle connections, the connection will be removed from the queue, and then close the socket of the connection, return 0, and immediately clean up again.

2.2. If the number of idle connections is less than 5 and the idle time of longestIdleConnection is less than the maximum idle time, that is, it has not expired for cleaning, then return the expiration time of the connection and clean it next time;

2.3. If there are no idle connections and all connections are still in use, the default keepAlive time is returned and cleaned up after 5 minutes.

2.4, There is no connection, idleConnectionCount and inUseConnectionCount are both 0, reset cleanupRunning and wait for the next PUT connection to execute cleanupRunnable again using the thread pool.

With RealConnectionPool and RealConnection in mind, let’s go back to the find method of ExchangeFinder, where the connection is created.

Connection mechanism

The fing method for ExchangeFinder is as follows:

//ExchangeFinder.java

  public ExchangeCodec find(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    
    

    try {
      //1. Call findHealthyConnection internally to return the RealConnection object
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      // create a new connection
      return resultConnection.newCodec(client, chain);
    } catch (RouteException e) {
      ...// omit exception handling}}Copy the code

As we know from note 1 to create a RealConnection, let’s look at the findHealthyConnection function

  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      // Find a connection
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled);

      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          returncandidate; }}// Determine the availability of the connection
      if(! candidate.isHealthy(doExtensiveHealthChecks)) { candidate.noNewExchanges();continue;
      }

      returncandidate; }}Copy the code

Then see findConnection

//ExchangeFinder.java
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;// Return the result of the available connection
    Route selectedRoute = null;
    RealConnection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
       if (transmitter.isCanceled()) throw new IOException("Canceled");
      hasStreamFailure = false; .

	 //1. Try to use a connection that has already been created. The connection that has already been created may be restricted to create new streams
      releasedConnection = transmitter.connection;
      / / 1.1, if the connection has been created has restricted to create a new flow, the release of the connection (releaseConnectionNoEvents will connect the empty), and returns the connection Socket to shut downtoClose = transmitter.connection ! =null && transmitter.connection.noNewExchanges
          ? transmitter.releaseConnectionNoEvents()
          : null;

        // if the connection is still available, use it as a result.
        if(transmitter.connection ! =null) {
            result = transmitter.connection;
            releasedConnection = null;
        }

        // The connection that has been created cannot be used
        if (result == null) {
            2.1. Try to find the connection available in the connection pool. If the connection is found, the value will be stored in the Transmitter
            if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null.false)) {
                2.2. Find an available connection from the connection pool
                foundPooledConnection = true;
                result = transmitter.connection;
            } else if(nextRouteToTry ! =null) {
                selectedRoute = nextRouteToTry;
                nextRouteToTry = null;
            } else if (retryCurrentRoute()) {
                selectedRoute = transmitter.connection.route();
            }
        }
    }
	closeQuietly(toClose);
    
	/ /...
    
    if(result ! =null) {
        // if a connection is already available, return the result
        return result;
    }
    
    // No connection available

    // See if routing is required, multi-IP operation
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null| |! routeSelection.hasNext())) { newRouteSelection =true;
        routeSelection = routeSelector.next();
    }
    List<Route> routes = null;
    synchronized (connectionPool) {
        if (transmitter.isCanceled()) throw new IOException("Canceled");

        // If there is a next route
        if (newRouteSelection) {
            routes = routeSelection.getAll();
            // This is the second attempt to find available connections from the connection pool
            if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
                4.1. Find an available connection from the connection pool
                foundPooledConnection = true; result = transmitter.connection; }}// No available connection was found in the connection pool
        if(! foundPooledConnection) {if (selectedRoute == null) {
                selectedRoute = routeSelection.next();
            }

           // create a Socket connection
            result = newRealConnection(connectionPool, selectedRoute); connectingConnection = result; }}If a connection is available in the connection pool, return the connection directly
    if (foundPooledConnection) {
        eventListener.connectionAcquired(call, result);
        return result;
    }

    Call RealConnection's connect method to connect the Socket. This is described in RealConnection
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, call, eventListener);
    
    connectionPool.routeDatabase.connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
        connectingConnection = null;
        // If we just created a multiplexed connection with the same address, release this connection and get that one
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
            result.noNewExchanges = true;
            socket = result.socket();
            result = transmitter.connection;
        } else {
            5.2. Add the newly created connection to the connection pool
            connectionPool.put(result);
            Save the newly created connection to the Transmitter connection field
            transmitter.acquireConnectionNoEvents(result);
        }
    }
    
    closeQuietly(socket);
    eventListener.connectionAcquired(call, result);
    
    //5.4
    return result;
}

Copy the code

This findConnection method is the core of the ConnectInterceptor. We ignore multiple IP operations and multiplexing (HTTP2). Assume that this connection is not in the connection pool or Transmitter for the first time, so skip 1, 2, 3 and go directly to 5. Create a new connection and put it in the connection pool and Transmitter; Then we use the same Call to make the second request. At this time, there is the connection in the connection pool and the Transmitter, so we will go 1, 2 and 3. If the connection in the Transmitter is still available, we will return, otherwise we will get an available connection from the connection pool, so the general process of the whole connection mechanism is as follows:

What is the difference between a connection in Transmitter and a connection in a connection pool? As we know, every time a Call is created, a corresponding Transmitter will be created. A Call can send multiple calls (synchronous and asynchronous), and different calls have different transmitters. The connection pool is created when OkhttpClient is created. Therefore, the connection pool is shared by all calls, that is, all calls in the connection pool can be reused, while the connection in the Transmitter only corresponds to its corresponding Call, which can only be reused by all calls of this Call.

Now that we know about okHttp3’s connection mechanism, we move on to the next interceptor, networkInterceptors.

networkInterceptors

NetworkInterceptors is the sixth interceptor in the OKHttp interceptor. It belongs to the network interceptor.

Finally, OKHttp’s last interceptor, CallServerInterceptor, is executed

CallServerInterceptor

It is the last interceptor in the chain, according to the source code. It performs network request and response operations with the server.

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    // Get the Exchange to interact with the network
    Exchange exchange = realChain.exchange();
    // Get the requested data
    Request request = realChain.request();
		// Get the current request time
    long sentRequestMillis = System.currentTimeMillis();
		// Write the request header
    exchange.writeRequestHeaders(request);
		
    boolean responseHeadersStarted = false;
    Response.Builder responseBuilder = null;
    // If the request body can be written
    if(HttpMethod.permitsRequestBody(request.method()) && request.body() ! =null) {
      // If the request header adds 100-continue
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        exchange.flushRequest(); // Close the IO stream resource
        responseHeadersStarted = true;
        exchange.responseHeadersStart();
        responseBuilder = exchange.readResponseHeaders(true); 
      }

      if (responseBuilder == null) { // If it is null
        if (request.body().isDuplex()) {
          exchange.flushRequest();
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, true));
          request.body().writeTo(bufferedRequestBody);
        } else { //一般走 else
          // Write to the request body
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, false)); request.body().writeTo(bufferedRequestBody); bufferedRequestBody.close(); }}else {
        exchange.noRequestBody();
        if(! exchange.connection().isMultiplexed()) {// exchange.noNewExchangesOnConnection(); }}}else { // noRequestBody is executed if there is noRequestBody
      exchange.noRequestBody();
    }

    // If the request body is empty and isDuplex = false is not supported I/O streams
    if (request.body() == null| |! request.body().isDuplex()) { exchange.finishRequest(); }if(! responseHeadersStarted) { exchange.responseHeadersStart(); }// Read the head of the response
    if (responseBuilder == null) {
      responseBuilder = exchange.readResponseHeaders(false);
    }

    // Build response data
    Response response = responseBuilder
        .request(request)
        .handshake(exchange.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    // Get the response code
    int code = response.code();
    if (code == 100) {
      // Build the response
      response = exchange.readResponseHeaders(false)
          .request(request)
          .handshake(exchange.connection().handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();

      code = response.code();
    }
		
    exchange.responseHeadersEnd(response);

    if (forWebSocket && code == 101) {
      // Build an empty response body
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      Construct the response body with the body of the responseresponse = response.newBuilder() .body(exchange.openResponseBody(response)) .build(); }...// Omit some code

    return response;
  }
Copy the code

In the current interceptor, we write the request head /body to the server through OKIO, and then build some response data such as the response header and the response body based on the response data of the server.

Now that we’re done with the interceptor, let’s move on to the actual interceptor.

Interceptor combat

OKHttp interceptor usage guide

Custom Log print interceptor

/** * Prints the log interceptor */
class LoggingInterceptor implements Interceptor {
    private String TAG = "LoggingInterceptor";
    public static String requestBodyToString(RequestBody requestBody) throws IOException {
        if (requestBody == null)return "";
        Buffer buffer = new Buffer();
        requestBody.writeTo(buffer);
        return buffer.readUtf8();
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        // Get the requested data
        Request request = chain.request();


        // Request headers can be added before requesting the server
        request =   request.newBuilder()
                .addHeader("head-1"."1")
                .addHeader("head-2"."2")
                .url("https://juejin.cn/user/3368559355637566")
                .build();


        HttpUrl url = request.url();
        String scheme = url.scheme();// http https
        String host = url.host();/ / 127.0.0.1
        String path = url.encodedPath();// /test/upload/img
        String query = url.encodedQuery();// userName=DevYk&userPassword=12345
        RequestBody requestBody = request.body();
        String bodyToString = requestBodyToString(requestBody);

        Log.d(TAG,"Scheme -" "+scheme);
        Log.d(TAG,"Host--->"+host);
        Log.d(TAG,"path--->"+path);
        Log.d(TAG,"query--->"+query);
        Log.d(TAG,"requestBody---->"+bodyToString+"");
        Log.d(TAG,"head---->"+request.headers().names());

        // Invoke the next interceptor
        Response response = chain.proceed(request);
        
       // Get the response
        ResponseBody responseBody = response.body();
        String body = responseBody.string();
        String type = responseBody.contentType().type();
        String subtype = responseBody.contentType().subtype();

        // Prints the response
        Log.d(TAG,"contentType--->"+type+""+subtype);
        Log.d(TAG,"responseBody--->"+body);

        returnchain.proceed(request); }}Copy the code

Add the configuration

OkHttpClient okHttpClient = new OkHttpClient.Builder().
        addInterceptor(new LoggingInterceptor())
        build();
Copy the code

output:

LoggingInterceptor: scheme-- HTTPS LoggingInterceptor: Host-- >juejin. Im LoggingInterceptor: path-- >/user/578259398ac2470061f3a3fb
LoggingInterceptor: query--->null
  
LoggingInterceptor: requestBody---->
  
LoggingInterceptor: head---->[head-1, head-2] LoggingInterceptor: responseHeader--->text html LoggingInterceptor: responseBody---><! DOCTYPE html><html ....Copy the code

Custom global disallow network request interceptor

public class NetworkInterceptor implements Interceptor {
    @Override
    public okhttp3.Response intercept(Chain chain) throws IOException {
        if (true) {
            Response response = new Response.Builder()
                    .code(404) // Code can be given freely
                    .protocol(Protocol.HTTP_1_1)
                    .message("Due to policy, Internet requests cannot be made at this time.")
                    .body(ResponseBody.create(MediaType.get("text/html; charset=utf-8"), "")) // Return to empty page
                    .request(chain.request())
                    .build();
            return response;
        } else {
            returnchain.proceed(chain.request()); }}}Copy the code

configuration

OkHttpClient okHttpClient = new OkHttpClient.Builder().
        addInterceptor(new LoggingInterceptor()).
  			addInterceptor(new NetworkInterceptor()).
        build();
Copy the code

Output:

LoggingInterceptor: responseCode--->404LoggingInterceptor: responseMessage-- > According to the regulations, network requests cannot be made at the moment. LoggingInterceptor: responseisSuccessful--->false
Copy the code

Summary: Interceptors are divided into application interceptors and network interceptors.

Apply interceptor

  • Don’t worry about intermediate responses, such as redirects and retries.
  • Even if an HTTP response is provided from the cache, it is always called once.
  • Adhere to the original intent of the application. Don’t care about OkHttp injected headers, for exampleIf-None-Match.
  • Allow short circuit instead ofChain.proceed().
  • Allow retries and multiple callsChain.proceed().

Network interceptor

  • The ability to operate on intermediate responses such as redirects and retries.
  • Will not be called for cache responses that short-circuit the network.
  • Observe the data as if it were transmitted over a network.
  • accessConnectionWith a request.

So how to choose depends on their own needs.

Interceptor summary

OKHttp interceptor: OKHttp interceptor: OKHttp interceptor: OKHttp interceptor: OKHttp interceptor

Each interceptor corresponds to a RealInterceptorChain, and each interceptor generates the next RealInterceptorChain until the List iteration is complete. So this is basically recursion, and I found some pictures to help you understand the picture below

reference

OKHttp source code parsing (4)- Mid-level interceptor and call chain

OKHttp source code analysis