introduce

OkHttp is a dependency library for handling HTTP network requests, developed and open sourced by Square and currently available in Java and Kotlin. OkHttp has now taken over almost all network request operations for Android apps, so it is a must for every Android developer. Understanding its internal implementation helps to extend, encapsulate, and optimize functionality.

HTTP and HTTP/2 clients for Android and Java applications.

The 4.0.x version of OkHttp has been completely replaced by Java with Kotlin, and some use of the API will be a little different.

Making portal

Document and API

requirements

Supported versions

4.0.x: Android 5.0+ (API level 21+) and Java 8+.

3.12.x: Android 2.3+ (API level 9+) and Java 7+. The platform may not support TLSv1.2. (2021-12-31 No longer supported)

OkHttp has a library dependency on Okio, a small library for high-performance I/O. It works with Okio 1.x (implemented in Java) or Okio 2.x (upgraded to Kotlin).

The version of OkHttp used in this article is 3.14.2, not that it will not access the higher version, mainly because the 4.0.x version has been completely replaced by Java to Kotlin, Kotlin is not familiar with the misunderstanding, so as to mislead people.

dependencies {
    // This is used in this article
    implementation 'com. Squareup. Okio: okio: 1.15.0'
    implementation 'com. Squareup. Okhttp3: okhttp: 3.14.2'
    
    / / high version
    // define a BOM and its version
    implementation(platform("Com. Squareup. Okhttp3: okhttp - bom: 4.9.0"))

    // define any required OkHttp artifacts without version
    implementation("com.squareup.okhttp3:okhttp")
    implementation("com.squareup.okhttp3:logging-interceptor")}Copy the code

Network request flow analysis

OkHttp has changed a lot over several iterations. Better WebSocket support, more Interceptor responsibilities, and even the core HttpEngine has become HttpCodec. This article will review the entire network request process and the implementation mechanism.

Let’s take a look at the basic uses of OkHttp:

public void getHttp(String url){
        // Create OkHttpClient object
        OkHttpClient client = new OkHttpClient();
        / / create Request
        Request request = new Request.Builder()
                .url(url)
                .build();
        // Create a Call object client.newCall(request)
        // Use the execute() method to get the Response object for the request Response
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {}

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if(response.isSuccessful()){
                    String result = response.body().string();
                    // The UI needs to be switched to the UI thread}}}); }Copy the code

In addition to directly new OkHttpClient, you can also set up OkHttpClient using the internal Factory class Builder. As follows:

    public void buildHttp(String url){
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.connectTimeout(15, TimeUnit.SECONDS)// Set timeout
                .addInterceptor(interceptor)    / / the interceptor
                .proxy(proxy)       // Set the proxy
                .cache(cache);      // Set the cache
        Request request = new Request.Builder()
                .url(url)
                .build();
        builder.build().newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {}

            @Override
            public void onResponse(Call call, Response response) throws IOException {}}); }Copy the code

The starting point for the request operation starts with the okHttpClient.newCall ().enqueue() method:

OkHttpClient.newCall

  @Override public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
  }
Copy the code
RealCall.newRealCall.java
  static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    // Safely publish the Call instance to the EventListener.
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.transmitter = new Transmitter(client, call);
    return call;
  }
Copy the code

This method returns a RealCall object through which the network request operation is added to the request queue.

RealCall.enqueue

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

Client.dispatcher () returns to the Dispatcher and calls the Dispatcher’s enQueue method to execute an asynchronous network request.

The Dispatcher is a scheduler for OkHttpClient and is a portal pattern. It is mainly used to execute and cancel asynchronous request operations. In essence, a thread pool is maintained internally to perform asynchronous operations, and the maximum number of concurrent requests and the number of threads allowed to execute requests by the same host host are guaranteed within the Dispatcher according to certain policies.

Dispatcher.enqueue

  void enqueue(AsyncCall call) {
    synchronized (this) {
      readyAsyncCalls.add(call);
      if(! call.get().forWebSocket) { AsyncCall existingCall = findExistingCallWithHost(call.host());if(existingCall ! =null) call.reuseCallsPerHostFrom(existingCall);
      }
    }
    promoteAndExecute();
  }
Copy the code

AsyncCall is actually executed using a thread pool, and AsyncCall inherits NamedRunnable. NamedRunnable implements the Runnable interface, so the entire operation is executed in a child thread (not a UI thread).

NamedRunnable

/** * Runnable implementation which always sets its thread name. */
public abstract class NamedRunnable implements Runnable {
  protected final String name;

  public NamedRunnable(String format, Object... args) {
    this.name = Util.format(format, args);
  }

  @Override public final void run(a) {
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      execute();
    } finally{ Thread.currentThread().setName(oldName); }}protected abstract void execute(a);
}
Copy the code

An abstract method execute is executed in the run method. This abstract method is implemented by AsyncCall.

AsyncCall.execute

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

See from the above method is really get request results in getResponseWithInterceptorChain method, from the name also can see its internal is an interceptor invocation chain.

RealCall.getResponseWithInterceptorChain

  Response getResponseWithInterceptorChain(a) throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    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, transmitter, null.0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
      Response response = chain.proceed(originalRequest);
      if (transmitter.isCanceled()) {
        closeQuietly(response);
        throw new IOException("Canceled");
      }
      return response;
    } catch (IOException e) {
      calledNoMoreExchanges = true;
      throw transmitter.noMoreExchanges(e);
    } finally {
      if(! calledNoMoreExchanges) { transmitter.noMoreExchanges(null); }}}Copy the code

Interceptor: Interceptors are a powerful mechanism for monitoring, overwriting, and retrying calls.

Each interceptor does the following:

  • BridgeInterceptor: Sets default values for the Head of Request, such as Content-Type, keep-alive, and Cookie.
  • CacheInterceptor: Handles the caching of HTTP requests.
  • ConnectInterceptor: Establishes a TCP connection with the server address.
  • CallServerInterceptor: Sends requests to the server and gets remote data results from the server.
  • RetryAndFollowUpInterceptor: this interceptor to recover from failure, and according to the need to perform a redirect. If the call is cancelled, it may raise IOException.

Before adding the interceptors mentioned above, the client. Interceptors are called to add the interceptors set by the developer to the list.

There is not much we can control to modify the Request Head and TCP links. So we know about CacheInterceptor and CallServerInterceptor.

CacheInterceptor a CacheInterceptor

A CacheInterceptor does several things: a. Obtains an existing cached Response (which may be null) based on Request and creates a CacheStrategy object based on that cached Response.

CacheInterceptor.intercept

@Override public Response intercept(Chain chain) throws IOException {
  // Obtain the current cached Response (possibly null) according to Request, and obtain the cached Response according to RequestResponse cacheCandidate = cache ! =null
      ? cache.get(chain.request())
       : null;
  // Get the current time
  long now = System.currentTimeMillis();
  // Create a CacheStrategy object
  // Use CacheStrategy to determine whether the cache is valid
  CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
  Request networkRequest = strategy.networkRequest;
  Response cacheResponse = strategy.cacheResponse;
  if(cache ! =null) {
    cache.trackResponse(strategy);
  }
  if(cacheCandidate ! =null && cacheResponse == null) {
    closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
  }
  // If we are prohibited from using the network and the cache is insufficient, we will fail. Return empty corresponding (util.empty_response)
  if (networkRequest == null && cacheResponse == null) {
    return new Response.Builder()
        .request(chain.request())
        .protocol(Protocol.HTTP_1_1)
        .code(504)
        .message("Unsatisfiable Request (only-if-cached)")
        .body(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();
  }

  // If the cache is valid, the cache Response is available
  if (networkRequest == null) {
    return cacheResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .build();
  }
  // If there is no cache or the cache fails, a network request is sent to get a Response from the server
  Response networkResponse = null;
  try {
    // Execute the next interceptor, networkRequest
    // Initiate a network request
    networkResponse = chain.proceed(networkRequest);
  } finally {
    // If we crash on I/O or anything else, do not leak the cache body.
    if (networkResponse == null&& cacheCandidate ! =null) { closeQuietly(cacheCandidate.body()); }}...// Get the latest Response from the network
  Response response = networkResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .networkResponse(stripBody(networkResponse))
      .build();
  // If the developer has a custom cache, the latest response is cached
  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);
    }
    // Return response(cache or network)
    return response;
  }
Copy the code

Cache implements the caching function

As you can see from the above flow of caching interceptors, OkHttp only specifies a set of caching policies, but it is up to the developer to define how to cache data locally and how to retrieve data from the local cache. And set by cache method of okHttpClient. Builder.

OkHttp provides a default caching class, cache.java, which can be used to implement caching directly when building OkHttpClient. You only need to specify the path of the cache and the maximum available space, as shown below:

OkHttpClient.Builder builder = new OkHttpClient.Builder();
    builder.connectTimeout(15, TimeUnit.SECONDS)// Set timeout
            / / / / interceptor
            .addInterceptor(new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    return null; }})// Set the proxy
            .proxy(new Proxy(Proxy.Type.HTTP,null)) 
            // Set the cache
            . / / AppGlobalUtils getApplication () is obtained by reflection Application instance
            //getCacheDir The built-in cache directory serves as the cache path
            //maxSize 10*1024*1024 Sets the maximum cache size to 10MB
            .cache(new Cache(AppGlobalUtils.getApplication().getCacheDir(),
                        10*1024*1024));
Copy the code

The Cache uses DiskLruCach internally to implement specific caching functions, as shown below:

 /**
   * Create a cache of at most {@code maxSize} bytes in {@code directory}.
   */
  public Cache(File directory, long maxSize) {
    this(directory, maxSize, FileSystem.SYSTEM);
  }

  Cache(File directory, long maxSize, FileSystem fileSystem) {
    this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
  }
Copy the code

DiskLruCache eventually saves the data that needs to be cached locally. If the OkHttp cache policy is too complicated, you can set up your own cache mechanism using DiskLruCache.

LRU is the least recently used algorithm (cache elimination algorithm). The core idea of LRU is that when the cache is full, the least recently used cache objects are preferentially eliminated. There are two kinds of cache using LRU algorithm: LrhCache and DisLruCache, which are used to implement memory cache and hard disk cache respectively. Their core ideas are LRU cache algorithm.

CallServerInterceptor,

The CallServerInterceptor is the last interceptor in OkHttp and the core network request part of OkHttp.

CallServerInterceptor.intercept

  @Override public Response intercept(Chain chain) throws IOException {
    / / get RealInterceptorChain
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    / / for Exchange
    Exchange exchange = realChain.exchange();
    Request request = realChain.request();

    long sentRequestMillis = System.currentTimeMillis();

    exchange.writeRequestHeaders(request);

    boolean responseHeadersStarted = false;
    Response.Builder responseBuilder = null;
    if(HttpMethod.permitsRequestBody(request.method()) && request.body() ! =null) {
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        exchange.flushRequest();
        responseHeadersStarted = true;
        exchange.responseHeadersStart();
        responseBuilder = exchange.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        if (request.body().isDuplex()) {
          exchange.flushRequest();
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, true));
          request.body().writeTo(bufferedRequestBody);
        } else {
          // Write the request body if the "Expect: 100-continue" expectation was met.
          BufferedSink bufferedRequestBody = Okio.buffer(
              exchange.createRequestBody(request, false)); request.body().writeTo(bufferedRequestBody); bufferedRequestBody.close(); }}else {
        exchange.noRequestBody();
        if(! exchange.connection().isMultiplexed()) {// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
          // from being reused. Otherwise we're still obligated to transmit the request body to
          // leave the connection in a consistent state.exchange.noNewExchangesOnConnection(); }}}else {
      exchange.noRequestBody();
    }

    if (request.body() == null| |! request.body().isDuplex()) { exchange.finishRequest(); } the above is to send the request data to the server side ----- powerful split line ---------- below is to get the corresponding data from the server side and build the Response objectif(! responseHeadersStarted) { exchange.responseHeadersStart(); }if (responseBuilder == null) {
      responseBuilder = exchange.readResponseHeaders(false);
    }

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

    int code = response.code();
    if (code == 100) {
      // server sent a 100-continue even though we did not request one.
      // try again to read the actual 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) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else{ response = response.newBuilder() .body(exchange.openResponseBody(response)) .build(); }...return response;
  }
Copy the code

summary

1. OkHttp is a portal mode internally, all delivery work is distributed through a portal Dispatcher.

2. In the network request stage, chain calls the Intercept method of each interceptor through the responsibility chain mode. This article focuses on two of the more important interceptors: CacheInterceptor(request caching) and CallServerInterceptor(executing network requests).

Phase to recommend

Android RecyclerView drawing process and Recycler cache

Android RecyclerView simple use

Android Glide cache mechanism and source code

Android article easy to use Glide