Recently, I plan to do network related optimization work, so I need to be familiar with network framework again. OkHttp is the leader of network framework in the field of Android, so I take this opportunity to analyze some internal implementation of OkHttp in depth. At the same time, these questions are also frequent in the interview, I believe it will be helpful to you.

Let’s start with a soul torture quadruple:

  • What is the difference between addInterceptor and addNetworkInterceptor?
  • How is network caching implemented?
  • How to reuse network connections?
  • How does OkHttp do network monitoring?

Whether it is familiar or unfamiliar, the fact is that the network framework has already implemented these basic functions for us, so it is easy to ignore. To fully analyze the above, we need to review the fundamentals of OkHttp:

Basic implementation principles of OkHttp

The internal implementation of OkHttp is accomplished through a chain of responsibility pattern, which encapsulates the various stages of the network request into individual chains, enabling the decoupling of the various layers.

Text source based on the latest version of OkHttp 4.2.2, from the 4.0.0 version, OkHttp using the full Kotlin language development, did not get on the car partners to grasp, or the source code are quick to read do not understand, learn Kotlin can refer to the old text Kotlin learning series article Overview.

Let’s start with a call that initiates a request and familiarize ourselves with the flow that OkHttp performs.

Val client = okHttpClient.builder ().build(); Val request = request.builder ().url()"https://wanandroid.com/wxarticle/list/408/1/json"Thread {// Initiate a network request val Response = client.newCall(request).execute()if(! response.isSuccessful) throw IOException("Unexpected code $response")
    Log.d("okhttp_test"."response:  ${response.body? .string()}")
}.start()
Copy the code

So the core code logic is to create a Call object through OkHttpClient’s newCall method and Call its execute method; Call represents an interface for a network request, and the implementation class has only one RealCall. Execute initiates a network request synchronously, and the corresponding enqueue method initiates an asynchronous request, so a callback is passed in as well.

Let’s look at the execute method of RealCall:

# RealCalloverride fun execute(): Response { ... / / timer timeout, send request started callback transmitter. TimeoutEnter () transmitter. CallStart () try {client. The dispatcher. Executed (this) / / step 1returnGetResponseWithInterceptorChain () / / step 2} finally {client. The dispatcher. Finished (this) / / step 3}}Copy the code

It only takes three steps to put an elephant in a refrigerator.

The first step

Execute method. What is Dispatcher? It’s a scheduler by the name, scheduling what? That is, all network requests, which are RealCall objects. Network requests support synchronous execution and asynchronous execution. Asynchronous execution requires thread pool, concurrency threshold and other things. If the threshold is exceeded, the excess part needs to be stored, which can be summarized as follows:

  • Records synchronous tasks, asynchronous tasks, and asynchronous tasks to be executed.
  • Thread pools manage asynchronous tasks.
  • Initiate/cancel network request API: execute, enQueue, cancel.

OkHttp sets the default maximum number of concurrent requests maxRequests = 64 and the maximum number of concurrent requests supported by a single host maxRequestsPerHost = 5.

Three dual-ended queues store these requests simultaneously:

# DispatcherPrivate Val runningAsyncCalls = ArrayDeque<AsyncCall>() private val runningAsyncCalls = Private val runningSyncCalls = ArrayDeque<RealCall>()Copy the code

Why use a double-ended queue? This is simple because network requests are executed in the same order as queuing, first come, first served, new requests are placed at the end of the queue, and execution requests are taken from the other end.

We know that LinkedList also implements the Deque interface, which is a dual-end queue with linked lists. So why not use LinkedList?

This is actually related to the readyAsyncCalls conversion to runningAsyncCalls, which are traversed once when a request is completed or a new request is queued by calling the enQueue method, Those eligible wait requests are moved to the runningAsyncCalls queue and handed over to the thread pool for execution. Although both can accomplish this task, the data structure of linked lists causes elements to be distributed in discrete locations in memory, which makes CPU caching less convenient, and array structures are more efficient than linked lists for garbage collection.

Returning to the subject, the core logic above is in the promoteAndExecute method:

#Dispatcherprivate fun promoteAndExecute(): Boolean { val executableCalls = mutableListOf<AsyncCall>() val isRunning: Boolean synchronized(this) {val I = readyAsynccalls. iterator() // readyAsyncCallswhile(i.hasNext()) {val asyncCall = i.next() // Threshold checkif (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
        if (asyncCall.callsPerHost().get() >= this.maxRequestsPerHost) continue//per Host count + 1 asynccall.callsperhost ().incrementandGet () ExecutableCalls. Add (asyncCall) // Migrate to the runningAsyncCalls list runningAsyncCalls.add(asyncCall)} isRunning = runningCallsCount() > 0 }for (i in0 until executableCalls. Size) {val asyncCall = executableCalls[I] // Commit tasks to the thread pool asynccall.executeon (executorService)}return isRunning
}
Copy the code

This method is called in both the enQueue and Finish methods to resubmit the task to the thread pool when a new request is queued and the current request completes.

After all that talk about thread pools, what thread pools do OkHttp use internally?

#Dispatcher 
@get:JvmName("executorService") val executorService: ExecutorService
get() {
  if (executorServiceOrNull == null) {
    executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
        SynchronousQueue(), threadFactory("OkHttp Dispatcher".false))}return executorServiceOrNull!!
}
Copy the code

Isn’t this a newCachedThreadPool? That’s right, the same as newCachedThreadPool except for the last threadFactory parameter, except that the thread name is set for troubleshooting.

The SynchronousQueue for blocking queues, which does not store data. When an element is added, it must wait for a consuming thread to fetch it, otherwise it blocks, executes on the idle thread if there is one, or starts a new thread if there is none. It is usually used in scenarios that require fast response tasks and is suitable in the context of low latency required by network requests. For details, see the working Principle of Java Thread Pool in the previous article.

Let’s go back to the main line, step 2 is a little bit more complicated and let’s skip step 3.

The third step

Call the Finished method of the Dispatcher

Internal fun finished(Call: AsyncCall) { call.callsPerHost().decrementAndGet() finished(runningAsyncCalls, Internal fun Finished (call: RealCall {finished(runningSyncCalls, call)} Private fun <T> finished(calls: Deque<T>, call: T) {val idleCallback: Runnable? Synchronized (this) {// Delete completed tasks from the queueif(! calls.remove(call)) throw AssertionError("Call wasn't in-flight!") idleCallback = this.idleCallback} // This method was analyzed in the first step to move requests from the wait queue to the asynchronous queue and to the thread pool for execution. Val isRunning = promoteAndExecute() // If no request needs to be executed, the callback is idleif(! isRunning && idleCallback ! = null) { idleCallback.run() } }Copy the code

The second step

Now we are looking back on it, the second step in the most complex call getResponseWithInterceptorChain method, which is the whole OkHttp realize the core of the chain of responsibility pattern.

#RealCallfun getResponseWithInterceptorChain(): Response {// create an Interceptor array val interceptors = mutableListOf<Interceptor>() // Add an application Interceptor += client.interceptors / / add a retry and redirect the interceptor interceptors + = RetryAndFollowUpInterceptor (client) / / add bridge interceptor interceptors + = BridgeInterceptor(client.cookiejar) // Add the cache interceptor += CacheInterceptor(client.cache) // Add the connection interceptor += ConnectInterceptorif (!forWebSocket) {/ / add network interceptor interceptors + = client.net workInterceptors} / / add request interceptor interceptors + = CallServerInterceptor (forVal Chain = RealInterceptorChain(interceptors, transmitter, null, 0, originalRequest, this, client.connectTimeoutMillis, client.readTimeoutMillis, client.writeTimeoutMillis) ... Val response = chain.proceed(originalRequest)...returnresponse } catch (e: IOException) { ... }}Copy the code

Regardless of what each interceptor does, the main process ends up with chain.proceed. Let’s take a look at the procceed method:

  # RealInterceptorChain
  override fun proceed(request: Request): Response {
    returnproceed(request, transmitter, exchange) } @Throws(IOException::class) fun proceed(request: Request, transmitter: Transmitter, exchange: Exchange?) : Response {if(index >= interceptors.size) throw AssertionError() // count the number of times that the current interceptor calls the proceed method calls++ // exchage is a wrapper around the request stream, When the ConnectInterceptor is executed, it is null. The connection and stream are already established, but the current URL is no longer supported. check(this.exchange == null || this.exchange.connection()!! .supportsUrl(request.url)) {"network interceptor ${interceptors[index - 1]} must retain the same host and port"The ConnectInterceptor and subsequent interceptors can only call proceed once at most!! check(this.exchange == null || calls <= 1) {"network interceptor ${interceptors[index - 1]} must call proceed() exactly once"} val next = RealInterceptorChain(interceptors, transmitter, exchange, index + 1, request); call, connectTimeout,readTimeout, writeTimeout) // Fetch the interceptor with subscript index and call its Intercept method to pass in the new chain. val interceptor = interceptors[index] val response = interceptor.intercept(next) // Make sure that the ConnectInterceptor and subsequent interceptors call PROCEED at least once!! check(exchange == null || index + 1 >= interceptors.size || next.calls == 1) {"network interceptor $interceptor must call proceed() exactly once"
    }

    return response
  }
Copy the code

The comments in the code are fairly clear, which sums up by creating the next level of responsibility chain, then fetching the current interceptor, calling its Intercept method and passing in the created responsibility chain. ++ To ensure that the chain of responsibility continues, the chain.proceed() method must be called once inside all intercept methods except the last one (CallServerInterceptor).

For example, in the ConnectInterceptor source code:

# ConnectInterceptor uses singletons here
object ConnectInterceptor : Interceptor {

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    val request = realChain.request()
    val transmitter = realChain.transmitter()

    val doExtensiveHealthChecks = request.method ! ="GET"Val exchange = transmitter. NewExchange (chain,doExtensiveHealthChecks) // Execute the next level of the responsibility chainreturn realChain.proceed(request, transmitter, exchange)
  }
}
Copy the code

The ConnectInterceptor interceptor and its subsequent interceptors can only be invoked once, because the work of shaking hands, connecting, and sending requests takes place in these interceptors, indicating that a network request has been formally issued. An interceptor before that can proceed multiple times, such as error retry.

After the recursion of responsibility chain level by level, the Intercept method of CallServerInterceptor will be executed eventually, which will encapsulate the result of network Response into a Response object and return. Along the chain of responsibility level after level back, finally returned to getResponseWithInterceptorChain method returns.

Interceptor classification

Now we need to summarize the role of interceptors at each node in the chain of responsibility:

The interceptor role
Apply interceptor You get the original request, and you can add custom headers, generic parameters, parameter encryption, gateway access, and so on.
RetryAndFollowUpInterceptor Handles error retries and redirects
BridgeInterceptor The main job of the application layer and network layer bridge interceptor is to add cookies to the request, add fixed headers, such as Host, Content-Length, content-Type, user-agent, etc., and then save the cookie of the response result. If the response has been compressed using gzip, you also need to decompress it.
CacheInterceptor A cache interceptor that does not initiate a network request if it hits the cache.
ConnectInterceptor Connection interceptors, which internally maintain a connection pool, are responsible for connection reuse, connection creation (three-way handshake, etc.), connection release, and socket flow creation on connections.
networkInterceptors User-defined interceptors are typically used to monitor data transfers at the network layer.
CallServerInterceptor The request interceptor, after the preparatory work is complete, actually initiates the network request.

At this point, the core execution flow of OkHttp is over. Does that feel like an Epiphany? Now we can finally answer the opening question:

AddInterceptor is different from addNetworkInterceptor

The two are commonly known as application interceptor and network interceptor. From the perspective of the whole responsibility link, application interceptor is the interceptor executed first, that is, the original request after the user sets the request attribute. The network interceptor is located between the ConnectInterceptor and CallServerInterceptor, and the network link is ready to send the request data.

  1. First of all, the application of the interceptor before RetryAndFollowUpInterceptor and CacheInterceptor, so once there was an error retry or redirect network, network interceptor may be executed multiple times, because of the second request, but the application of the interceptor will only trigger a forever. In addition, if a CacheInterceptor hits the cache, there is no need to go out of the network, so you can short-circuit the network interceptor.

  2. Second, as mentioned above, every interceptor except the CallServerInterceptor should call the realchain.proceed method at least once. It is actually possible to call the PROCEED method multiple times (local exception retry) or not to call the PROCEED method (interrupt) at the apply interceptor layer, but the network interceptor layer is already wired and the PROCEED method can be called only once.

  3. Finally, from the use scenario, the application interceptor will be called only once, usually used to count the network request initiated by the client; A call of a network interceptor means that a network communication must be initiated, so it can usually be used to count the data transmitted over a network link.

The network caching mechanism CacheInterceptor

Caching here refers to data caching strategies based on the Http network protocol, with emphasis on client caching, so we’ll review how the Http protocol identifies cache availability based on request and response headers.

When it comes to caching, it’s important to talk about the validity and validity of caching.

Principles of HTTP Caching

In the HTTP 1.0 era, responses used an Expires header to indicate the expiration date of the cache, with a value of an absolute time, such as Expires:Thu,31 Dec 2020 23:59:59 GMT. When a client makes a network request again, it can compare the current time to the expires time of the last response to determine whether to use caching or make a new request.

The biggest problem with using an Expires header is that it depends on the client’s local time, which can make it impossible to accurately determine whether the cache is expired if the user changes the local time.

Therefore, since HTTP 1.1, cache-control headers have been used to represent Cache state, which takes precedence over Expires, and are commonly used for one or more of the following values.

  • Private, the default value, identifies private business logic data, such as recommendation data delivered based on user behavior. Nodes such as proxy servers in the network link in this mode should not cache this data because it is meaningless.
  • Public, as opposed to private, is used to identify common business data, such as getting a news list, where everyone sees the same data and therefore can be cached by both clients and proxy servers.
  • No-cache can be used for caching, but before the client can use the cache, the server must verify the validity of the cache resource, which is described in the comparison cache section below.
  • Max-age Indicates the cache duration. The unit is second. It refers to a time period, such as a year, that is usually used for static resources that do not change frequently.
  • No-store No cache is allowed on any node.

Mandatory cache

Based on the above Cache header specification, mandatory caching is when a network request response header identifies Expires or cache-Control contains max-age information, and the client calculates that the Cache is not expired. In this case, the local Cache content can be used directly without actually making a network request.

Negotiate the cache

The biggest problem with forced caching is that once the server resource is updated, the client cannot get the latest resource until the cache time expires (unless the no-store header is manually added to the request). In addition, in most cases, the server resource cannot directly determine the cache expiration time, so it is more flexible than caching.

Negotiation caching is implemented using last-modify/if-modify-since headers, by adding last-modify headers to the server response headers to identify the Last modification time of the resource, in seconds. When the client requests again, the if-modify-since header is added and assigned the value of the last-modify header obtained in the previous request.

After receiving the request, the server checks whether the cache resource is still valid. If yes, the server returns the status code 304 and the body is empty. Otherwise, the server delivers the latest resource data. If the client finds that the status code is 304, it retrieves the local cache data as a response.

One problem with using this scheme is that resource files have certain limitations with last-modified times:

  1. Last-modify the unit is second. If some files are modified within one second, the modification time cannot be accurately identified.
  2. The time of resource modification is not the only basis for whether a resource is modified or not. For example, the resource file is a Daily Build and new files are generated every day, but the actual content may not change.

Therefore, HTTP also provides another set of headers to handle caching, ETag/ if-none-match. The process is the same as last-modify, except that the header of the server response is last-modify and the header issued by the client is if-none-match. ETag is the unique identifier of a resource. The ETag must change when the server resource changes. The specific generation mode is controlled by the server, and the factors affecting the scenario include the final modification time of the file, file size, file number, and so on.

Caching implementation of OKHttp

With all this said, OKHttp actually implements the above process in code:

  1. The first time you get the response, you decide whether to cache it based on the header information.
  2. The next time you make a request, determine whether there is a local cache, whether you need to use contrast caching, encapsulate the request header information, and so on.
  3. Make a network request if the cache is invalid or if a comparison cache is required, otherwise use local cache.

OKHttp uses Okio internally to read and write cached files.

Cache files are divided into CleanFiles and DirtyFiles. CleanFiles are used for reading, and DirtyFiles are used for writing. They are both arrays with a length of 2, representing two files, namely the cached request header and request body. The cached operation logs are also recorded in journalFile.

Enabling caching requires setting a Cache object when OkHttpClient is created and specifying the Cache directory and Cache size. The Cache system internally uses LRU as the Cache elimination algorithm.

## Cache.kt
class Cache internal constructor(
  directory: File,
  maxSize: Long,
  fileSystem: FileSystem
): Closeable, Flushable
Copy the code

Earlier versions of OkHttp had an InternalCache interface that supported custom implementation caching. However, in the 4.x version, InternalCache was removed and the Cache class became final, thus disabling the extension function.

The source code is available in the CacheInterceptor class.

Caching is global via OkHttpClient. If we want to enable or disable caching for a particular request, we can do so through the CacheControl API:

// Disable caching Request Request = new request.builder ().cachecontrol (new cachecontrol.builder ().noCache().build()).url("http://publicobject.com/helloworld.txt")
    .build();
Copy the code

Cache cases not supported by OKHttp

One final point to note is that OKHttp only supports caching of GET requests by default.

# okhttp3.Cache.java@Nullable CacheRequest put(Response response) { String requestMethod = response.request().method(); . // The cache only supports GET requestsif(! requestMethod.equals("GET")) {
      // Don't cache non-GET responses. We're technically allowed to cache
      // HEAD requests and some POST requests, but the complexity of doing
      // so is high and the benefit is low.
      returnnull; } // The vary header is not cachedif (HttpHeaders.hasVaryAll(response)) {
      returnnull; }... }Copy the code

This is the logical code that is ready to be cached when a network request is responded to. Null means no cache. As you can see from the code comments, it is technically possible to cache Method as HEAD and partial POST requests, but the implementation complexity is high and the benefits are small. This essentially depends on how each method is used.

Let’s start by looking at the common types of methods and their uses.

  • GET requests the resource. The parameters are in the URL.
  • HEAD is basically the same as GET, except that it does not return a message body, and is usually used in speed or bandwidth first scenarios, such as checking resource availability, accessibility, and so on.
  • POST submits the form, modifies the data, and the parameters are in the body.
  • PUT is basically the same as POST, except that PUT is idempotent.
  • DELETE Deletes the specified resource.

You can see that for standard RESTful requests, where GET is used to retrieve data, caching is best suited, whereas for other operations on data caching makes little sense or does not require caching at all.

This is why OKHTTP uses the Request URL as the cache key when only GET requests are supported (with a series of digest algorithms, of course).

Finally, as noted in the code above, if the request header contains vary:*, it will not be cached. The vary header is used to improve cache hit ratios for multiple requests, such as two clients, one that supports gZIP compression and the other that does not, with the same request URL but different accept-encoding, which can easily lead to cache errors. We can prevent this by declaring vary: accept-encoding.

The vary:* header, however, indicates that this request is unique and should not be cached unless it is intended, which is generally not done at the expense of caching performance.


It is too long to write about this finding, for better reading experience, the following connection reuse and network monitoring section will be written separately, please pay attention.