Explore OkHttp series caching mechanisms

preface

In the previous article, we introduced BridgeInterceptor. In this article, we will introduce CacheInterceptor, which is related to the caching mechanism of OkHttp.

Before introducing the caching mechanism of OkHttp, let's first understand the caching mechanism of Http.

Cache mechanism of Http

Caching mainly refers to the resource copies saved in the disk of the proxy server or client. Caching can reduce access to the source server and improve efficiency.

Cache rule

For ease of understanding, we believe that the client has a cache database to store cache information, and the existence of proxy server is not considered.

When the client requests data for the first time, there is no corresponding cache data in the cache database. It needs to request the server. After the server returns, store the data in the cache database:

We divide the cache rules of Http into two categories: forced cache and contrast cache. The information related to the cache rules is contained in the Header of the message. These two types of caching rules can exist at the same time. These two types of caching rules are introduced below.

Force cache

Assuming that there is cached data in the cache database, it is only based on forced caching. The process of requesting data is as follows:

It can be seen that if the mandatory cache takes effect, there is no need to interact with the server.

The implementation of forced caching depends on two headers, Expires and cache control.

Expires/Cache-Control

Expires is the cache support provided by HTTP/1.0. Through this Header, the server can tell the client the expiration time of the cache, indicating that the resource will not be changed within the expiration time, so you can no longer request from yourself.

For example: Expires: Mon, 22 Nov 2021 16:21:00 GMT indicates the expiration time of the cache.

It can be found that it is a specific time generated by the server. Client applications such as browsers will compare with the specific time according to the local time, and there may be an error between the client time and the server time, which will lead to the error of cache hit.

Due to the above problems of Expires, the cache control mechanism is introduced into HTTP/1.1 protocol. Through this Header, the cache information can be communicated between the server and the client. Common cache instructions are as follows

valueexplain
privateClient can cache
publicBoth client and proxy servers can cache
max-age=xxxCached data expires in xxx seconds
no-cacheYou need to use the contrast cache to validate the cached data

Where private is the default.

In Http/1.0, if cache control: Max age = and Expires occur at the same time, the max age instruction will be ignored, and Expires will prevail;

In Http/1.1, if cache control: Max age = and Expires appear at the same time, Expires will be ignored, and Max age will prevail.

Contrast cache

Assuming that there is cached data in the cache database, only based on the comparison cache, the process of requesting data is as follows:

It can be seen that the comparison cache needs to interact with the server whether it is effective or not.

When the browser requests data for the first time, the server will return the cache ID together with the data to the client, and the client will back them up to the cache database.
When requesting data again, the client sends the backed up cache ID to the server. The server judges according to the cache ID. if the cache resource is still valid, the server will return 304 status code to notify the client that it is successful and can use the cache data.

There are two types of identity transfer: last modified / if modified since and Etag / if none match. We will introduce them respectively below.

Last-Modified / If-Modified-Since

These two fields need to be used in conjunction with cache control. Last modified is in the response header and if modified since is in the request header. Their meanings are:

  • Last modified: the last modification time of the response resource. The server can fill in this field when responding to the request.
  • If modified since: when the client cache expires (max age arrives), it is found that the resource has a last modified field. You can fill in the if modified since field in the Header and fill in the last modified record time. After receiving the time, the server will compare it with the last modified time of the resource.
    • If the resource has been modified, it will return the status code 200 and respond to the whole resource.
    • Otherwise, it indicates that the resource has not been modified during access, and the status code 304 will be responded to inform the client that the cached resource can be used.

Etag / If-None-Match

It also needs to be used with cache control. Etag is in the response header and if none match is in the request header. And their priority is higher than last modified / if modified since.

Their meanings are:

  • Etag: the unique identification of the requested resource in the server. The rules are determined by the server.
  • If none match: if the client finds that the resource has Etag field when the cache expires (max age arrives), it can add if none match header and pass in the value in Etag. After receiving the request, the server will compare the value of if none match with the unique ID of the requested resource
    • If the comparison is different, it indicates that the resource has been changed, the status code 200 will be returned and the whole resource content will be responded
    • If the comparison is the same, it indicates that there is no new modification to the resource, then the status code 304 will be returned to inform the client that the cached resource can continue to be used.

summary

Forced cache and contrast cache can exist at the same time, and the priority of forced cache is higher than that of contrast cache. That is, when the rule of forced cache is executed, if the cache is effective, the cache will be used directly, and the contrast cache rule will not be executed.

When forced cache and contrast cache exist at the same time:

  1. For forced caching, the server notifies the client of a cache time. During the cache time, the client can directly use the cached resources. If the client needs to obtain data, it needs to implement the comparison cache strategy.

  2. For the comparison cache, the client sends the Etag and last modified in the cache information to the server through a request for verification by the server. If the 304 status code is returned, the client can use the resources in the cache.

The flow chart is as follows

When the client first requests:

When the client requests again:

OkHttp caching mechanism

Let's start with the CacheInterceptor interceptor.

CacheInterceptor

intercept

We start with its intercept method

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val call = chain.call()
    // Take Request as the Key to obtain the candidate cache  
    val cacheCandidate = cache?.get(chain.request())

    val now = System.currentTimeMillis()

    // Create a cache policy based on "current time, request, candidate cache"  
    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
    // If the request does not need to use the network, the networkRequest is null  
    val networkRequest = strategy.networkRequest
    // If there is no cache corresponding to the request, the cacheResponse is null  
    val cacheResponse = strategy.cacheResponse

    cache?.trackResponse(strategy)
    val listener = (call as? RealCall)?.eventListener ?: EventListener.NONE

    if (cacheCandidate != null && cacheResponse == null) {
      // The cache candidate wasn't applicable. Close it.
      cacheCandidate.body?.closeQuietly()
    }

    // If we're forbidden from using the network and the cache is insufficient, fail.
    // If the request does not use the network and there is no corresponding cache, an error is directly reported and the status code 504 is returned  
    if (networkRequest == null && cacheResponse == null) {
      return Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(HTTP_GATEWAY_TIMEOUT)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build().also {
            listener.satisfactionFailure(call, it)
          }
    }

    // If we don't need the network, we're done.
    // If the request does not use the network and there is a corresponding cache, enter the if statement and return the cache
    // (the code is executed here, indicating that networkRequest and cacheResponse cannot be null at the same time)  
    if (networkRequest == null) {
      return cacheResponse!!.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build().also {
            listener.cacheHit(call, it)
          }
    }

    /* After the above two If statements, the code is executed here, indicating that the networkRequest is not null, that is, the request uses the network */  
      
    if (cacheResponse != null) {
      listener.cacheConditionalHit(call, cacheResponse)
    } else if (cache != null) {
      listener.cacheMiss(call)
    }

    // Represents a response to a network request  
    var networkResponse: Response? = null
    try {
      // Make a network request and obtain the Response returned by the next interceptor  
      networkResponse = chain.proceed(networkRequest)
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      // Release resources  
      if (networkResponse == null && cacheCandidate != null) {
        cacheCandidate.body?.closeQuietly()
      }
    }

    // If we have a cache response too, then we're doing a conditional get.
    // The cache corresponding to the request has been obtained before  
    if (cacheResponse != null) {
      // If the response returned by the network request contains the status code 304, it indicates that the previous cached data is valid, and the corresponding cacheResponse is returned
      // Cache result of HTTP_NOT_MODIFIED (corresponding status code 304)
      if (networkResponse?.code == HTTP_NOT_MODIFIED) {
        val response = cacheResponse.newBuilder()
  			// Mix the Header of the cached Response and the Header of the Response obtained from the network        
            .headers(combine(cacheResponse.headers, networkResponse.headers))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis)
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis)
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build()

        networkResponse.body!!.close()

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache!!.trackConditionalCacheHit()
        // Update cache
        cache.update(cacheResponse, response)
        return response.also {
          listener.cacheHit(call, it)
        }
      } else {
        // Cache expires, reclaim resources  
        cacheResponse.body?.closeQuietly()
      }
    }
      
    /* In the above large if statement, if the response code is 304, the cache resource is valid and the cache resource is returned. If the response code is not 304, */  
	/* It means that the cache resource expires and the cache resource is closed */
    
    // Read the results of the network request  
    val response = networkResponse!!.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build()

    if (cache != null) {
      // Cache the Response obtained by the network request  
      if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        val cacheRequest = cache.put(response)
        return cacheWritingResponse(cacheRequest, response).also {
          if (cacheResponse != null) {
            // This will log a conditional cache miss only.
            listener.cacheMiss(call)
          }
        }
      }

      // If the request method does not require a cache, remove the cache  
      if (HttpMethod.invalidatesCache(networkRequest.method)) {
        try {
          cache.remove(networkRequest)
        } catch (_: IOException) {
          // The cache cannot be written.
        }
      }
    }

    // Returns the result of the network request  
    return response
  }

Let's summarize its intercept method:

  1. Read candidate Cache from Cache with Request as key
  2. Build a cache policy according to "current time, Request, candidate cache" to judge whether the current Request needs to use the network and whether there is a cache
  3. According to the cache policy, if the current request does not use the network and there is no cache, an error is directly reported and the status code 504 is returned
  4. According to the caching policy, if the current request does not use the network and there is a cache, the cached data is returned directly
  5. Conduct network operation, submit the request to the following interceptor for processing, and obtain the returned Response at the same time
  6. If the status code of the Response returned through the network is 304, mix the request header of the cache Response and the Response returned by the network, update the cache and return the cache Response
  7. Read the Response returned by the network, judge whether caching is required, and cache the Response if necessary

Cache policy is mainly determined according to networkRequest and cacheResponse in CacheStrategy:

networkRequestcacheResponseCorresponding processing
nullnullAn error is reported directly, and the status code returns 504
nullnon-nullDirectly return cached Response
non-nullnullReturns the Response obtained by the network. If the caching conditions are met, the Response is cached
non-nullnon-nullIf the network Response status code is 304, update the cache after mixing the request header and return the cache; If the status code is 200, the network Response is returned. If the caching conditions are met, the Response is cached

Cache

In the CacheInterceptor, there is a Cache object. The intercept method CRUD the Response based on this object.

Custom Cache

In OkHttp, Cache is not used by default. The explanation is as follows:

CacheInterceptor is built in RealCall::getResponseWithInterceptorChain

interceptors += CacheInterceptor(client.cache)

The cache object of OkHttpClient is used. In OkHttpClient, the cache of Builder is used

@get:JvmName("cache") val cache: Cache? = builder.cache

And in okhttpclient In builder, its properties are as follows

internal var cache: Cache? = null

That is, the cache of the CacheInterceptor is null by default.

We can build a Cache by ourselves through the following methods and pass it in when building OkHttpClient

    val cacheFile = File(cachePath)
    val cacheSize = (10 * 1024 * 1024).toLong()
    val cache = Cache(cacheFile, cacheSize)
    val client: OkHttpClient = OkHttpClient.Builder()
                                           .cache(cache)
                                           .build()

The constructor under Cache is called here

constructor(directory: File, maxSize: Long) : this(directory, maxSize, FileSystem.SYSTEM)

The main constructor was called

class Cache internal constructor(
  directory: File,
  maxSize: Long,
  fileSystem: FileSystem
) : Closeable, Flushable {
  internal val cache = DiskLruCache(
      fileSystem = fileSystem,
      directory = directory,
      appVersion = VERSION,
      valueCount = ENTRY_COUNT,
      maxSize = maxSize,
      taskRunner = TaskRunner.INSTANCE
  )
  ...
}

As you can see, a DiskLruCache object is created here.

put

  internal fun put(response: Response): CacheRequest? {
    // Method to get the request  
    val requestMethod = response.request.method

    // If the method is one of POST, PATCH, PUT, DELETE and MOVE, it is considered that the Response does not need caching
    if (HttpMethod.invalidatesCache(response.request.method)) {
      try {
        // Remove the cache corresponding to the request  
        remove(response.request)
      } catch (_: IOException) {
        // The cache cannot be written.
      }
      // end  
      return null
    }

    // If the method is not GET, the Response is not cached
    // Technically, some POST responses of HEAD can also be cached, but the complexity is too high and the benefit is too low  
    if (requestMethod != "GET") {
      return null
    }

    if (response.hasVaryAll()) {
      return null
    }

    // Create cache based on Response Entry  
    val entry = Entry(response)
    var editor: DiskLruCache.Editor? = null
    try {
      // Trying to get editor  
      editor = cache.edit(key(response.request.url)) ?: return null
      // Write the information of the entry into the editor
      entry.writeTo(editor)
      // Get CacheRequest object according to editor  
      return RealCacheRequest(editor)
    } catch (_: IOException) {
      abortQuietly(editor)
      return null
    }
  }

The execution of the put method can be roughly divided into two parts

  1. Judge whether the Response needs to be cached according to the method of the Request. If the Response does not need to be cached, return null and the execution ends
  2. Create an Entry according to the Response, store the Response information in the Entry, and then write the Entry information to disklrucache Editor.

In addition, create disklrucache Editor, the key method is called, which will generate the corresponding stored key according to the url of the request, as follows

fun key(url: HttpUrl): String = url.toString().encodeUtf8().md5().hex()

It can be seen that it actually encodes the url using UTF-8, encrypts it with md5, and then converts it into hexadecimal.

Cache.Entry

Let's look at the cache that stores the Response information Entry

  private class Entry {
  	...
    constructor(response: Response) {
      // Request url  
      this.url = response.request.url.toString()
      // Vary response header  
      this.varyHeaders = response.varyHeaders()
      // Request method
      this.requestMethod = response.request.method
      // agreement
      this.protocol = response.protocol
      // Http status code
      this.code = response.code
      // HTTP status message  
      this.message = response.message
      // HTTP response header  
      this.responseHeaders = response.headers
      // The TLS handshake that hosts this response connection is null if the response has no TLS  
      this.handshake = response.handshake
      // The timestamp of the originating request. If the response is provided by the cache, this is the timestamp of the original request  
      this.sentRequestMillis = response.sentRequestAtMillis
      // The response timestamp is received. If the response is provided by the cache, this is the timestamp of the original response  
      this.receivedResponseMillis = response.receivedResponseAtMillis
    }
    ...  
  }
Cache.Entry::writeTo

Cache.Entry calls the writeTo method to write the Response information to the editor. Let's check this method

    @Throws(IOException::class)
    fun writeTo(editor: DiskLruCache.Editor) {
      // Get the output stream sink and cache the data one by one  
      editor.newSink(ENTRY_METADATA).buffer().use { sink ->
        // Cache request url                                           
        sink.writeUtf8(url).writeByte('\n'.toInt())
        // Cache request method                                           
        sink.writeUtf8(requestMethod).writeByte('\n'.toInt())
        // Cache Vary response header size                                           
        sink.writeDecimalLong(varyHeaders.size.toLong()).writeByte('\n'.toInt())
        // Cache variable response header information                                          
        for (i in 0 until varyHeaders.size) {
          sink.writeUtf8(varyHeaders.name(i))
              .writeUtf8(": ")
              .writeUtf8(varyHeaders.value(i))
              .writeByte('\n'.toInt())
        }

        // Cache protocol, status code, status message                                           
        sink.writeUtf8(StatusLine(protocol, code, message).toString()).writeByte('\n'.toInt())
        // Cache response header size                                           
        sink.writeDecimalLong((responseHeaders.size + 2).toLong()).writeByte('\n'.toInt())
        // Cache response header information                                           
        for (i in 0 until responseHeaders.size) {
          sink.writeUtf8(responseHeaders.name(i))
              .writeUtf8(": ")
              .writeUtf8(responseHeaders.value(i))
              .writeByte('\n'.toInt())
        }
        // Cache request sending time                                           
        sink.writeUtf8(SENT_MILLIS)
            .writeUtf8(": ")
            .writeDecimalLong(sentRequestMillis)
            .writeByte('\n'.toInt())
        // Cache request response time                                           
        sink.writeUtf8(RECEIVED_MILLIS)
            .writeUtf8(": ")
            .writeDecimalLong(receivedResponseMillis)
            .writeByte('\n'.toInt())

        // Judge whether it is an HTTPS request. If so, record the TLS handshake information                                        
        if (isHttps) {
          sink.writeByte('\n'.toInt())
          sink.writeUtf8(handshake!!.cipherSuite.javaName).writeByte('\n'.toInt())
          writeCertList(sink, handshake.peerCertificates)
          writeCertList(sink, handshake.localCertificates)
          sink.writeUtf8(handshake.tlsVersion.javaName).writeByte('\n'.toInt())
        }
      }
    }

Here, the information stored in the Entry is written to the editor, and the Entry stores the Response information. Therefore, in fact, the Response information is written to the editor, and then the editor will save the information locally, which realizes the operation of saving the Response information locally. Here, BufferedSink in okio library is mainly used to realize write operation.

get

Let's look at the get method of Cache

  internal fun get(request: Request): Response? {
    // Take the request url as the key  
    val key = key(request.url)
    // Find out whether there is a cache corresponding to the key in DiskLruCache. If yes, the corresponding memory snapshot is returned. If not, null is returned  
    val snapshot: DiskLruCache.Snapshot = try {
      cache[key] ?: return null
    } catch (_: IOException) {
      return null // Give up because the cache cannot be read.
    }

    // Get the input stream according to the getSource method of memory snapshot, and then cache the data in the input stream in the construction method of Entry
    // Save it and construct the Entry object  
    val entry: Entry = try {
      Entry(snapshot.getSource(ENTRY_METADATA))
    } catch (_: IOException) {
      snapshot.closeQuietly()
      return null
    }

    // Use the information in the Entry object to construct the Response object  
    val response = entry.response(snapshot)
    // Judge whether the request matches the constructed Response. If not, null will be returned  
    if (!entry.matches(request, response)) {
      response.body?.closeQuietly()
      return null
    }
	// Return response
    return response
  }

The execution process of the get method is to first find out whether there is a corresponding cache in DiskLruCache according to the key corresponding to the requested url. If there is a cache, the Response is constructed according to the information in the cache, and then match whether the constructed Response matches the Request. If it matches, the Response is returned, and if not, null is returned.

Cache.Entry(Source)

Let's look at the constructor whose Entry parameter is Source and see how it constructs an Entry according to Source. Due to a lot of code, we only intercept a small part

    @Throws(IOException::class) constructor(rawSource: Source) {
      try {
        val source = rawSource.buffer()
        url = source.readUtf8LineStrict()
        requestMethod = source.readUtf8LineStrict()
        val varyHeadersBuilder = Headers.Builder()
        val varyRequestHeaderLineCount = readInt(source)
        for (i in 0 until varyRequestHeaderLineCount) {
          varyHeadersBuilder.addLenient(source.readUtf8LineStrict())
        }
        varyHeaders = varyHeadersBuilder.build()

        val statusLine = StatusLine.parse(source.readUtf8LineStrict())
        protocol = statusLine.protocol
        code = statusLine.code
        message = statusLine.message
		...
      } finally {
        rawSource.close()
      }
    }

Internally, it reads the information in the source and saves it. The reading operation here also uses the okio library.

Cache.Entry::response

Entry constructs the response object through the response method. The method is as follows

    fun response(snapshot: DiskLruCache.Snapshot): Response {
      val contentType = responseHeaders["Content-Type"]
      val contentLength = responseHeaders["Content-Length"]
      val cacheRequest = Request.Builder()
          .url(url)
          .method(requestMethod, null)
          .headers(varyHeaders)
          .build()
      return Response.Builder()
          .request(cacheRequest)
          .protocol(protocol)
          .code(code)
          .message(message)
          .headers(responseHeaders)
          .body(CacheResponseBody(snapshot, contentType, contentLength))
          .handshake(handshake)
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(receivedResponseMillis)
          .build()
    }

It can be seen that the Response object is actually constructed by using the information saved by the Entry.

Summary

It can be found that the Entry class is useful for both write and read processes. It seems that the Entry class is the bridge of Response cache in OkHttp.

update

Let's look at the Cache update method

  internal fun update(cached: Response, network: Response) {
    // Save the network Response information in the Entry
    val entry = Entry(network)
    // Obtain a memory snapshot according to the cache Response  
    val snapshot = (cached.body as CacheResponseBody).snapshot
    var editor: DiskLruCache.Editor? = null
    try {
      // Get editor object  
      editor = snapshot.edit() ?: return // edit() returns null if snapshot is not current.
      // Write the information saved in the entry (the information of the network Response) into the editor  
      entry.writeTo(editor)
      // Submit the contents of the editor and write them locally  
      editor.commit()
    } catch (_: IOException) {
      abortQuietly(editor)
    }
  }

With the previous foundation, it is easy to understand the update method.

remove

The Cache remove method is as follows

  @Throws(IOException::class)
  internal fun remove(request: Request) {
    cache.remove(key(request.url))
  }

Its implementation is very simple. It directly calls the remove method of DiskLruCache to remove the corresponding cache.

CacheStrategy

In the intercept method of cacheinterceptor, call CacheStrategy The Factory construction method creates a Factory object, and then calls the compute method of the object to create a CacheStrategy object.

The parameters of the CacheStrategy construction method are as follows

class CacheStrategy internal constructor(
  /** The request to send on the network, or null if this call doesn't use the network. */
  val networkRequest: Request?,
  /** The cached response to return or validate; or null if this call doesn't use a cache. */
  val cacheResponse: Response?
) {
    ...
}

CacheStrategy.Factory

Construction method

Let's look at cachestrategy Factory class

  class Factory(
    private val nowMillis: Long,
    internal val request: Request,
    private val cacheResponse: Response?
  ) {
    // The time when the server provided the cacheResponse  
    private var servedDate: Date? = null
    private var servedDateString: String? = null

    // Last modified time of cacheResponse  
    private var lastModified: Date? = null
    private var lastModifiedString: String? = null

    // The expiration time of cacheResponse. If both this field and Max age field are set, the max age field has higher priority  
    private var expires: Date? = null

    // The extended Header set by OkHttp indicates the first initiation time of the request corresponding to the cacheResponse  
    private var sentRequestMillis = 0L

    // The extended Header set by OkHttp indicates the time when the cacheResponse is first received  
    private var receivedResponseMillis = 0L

    // Etag identifier of cacheResponse  
    private var etag: String? = null

    // Lifetime of cacheResponse  
    private var ageSeconds = -1

    /**
     * Returns true if computeFreshnessLifetime used a heuristic. If we used a heuristic to serve a
     * cached response older than 24 hours, we are required to attach a warning.
     */
    private fun isFreshnessLifetimeHeuristic(): Boolean {
      return cacheResponse!!.cacheControl.maxAgeSeconds == -1 && expires == null
    }

    init {
      // If the cached response is found on the local disk, the corresponding information is saved in the Factory object  
      if (cacheResponse != null) {
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis
        val headers = cacheResponse.headers
        for (i in 0 until headers.size) {
          val fieldName = headers.name(i)
          val value = headers.value(i)
          when {
            fieldName.equals("Date", ignoreCase = true) -> {
              servedDate = value.toHttpDateOrNull()
              servedDateString = value
            }
            fieldName.equals("Expires", ignoreCase = true) -> {
              expires = value.toHttpDateOrNull()
            }
            fieldName.equals("Last-Modified", ignoreCase = true) -> {
              lastModified = value.toHttpDateOrNull()
              lastModifiedString = value
            }
            fieldName.equals("ETag", ignoreCase = true) -> {
              etag = value
            }
            fieldName.equals("Age", ignoreCase = true) -> {
              ageSeconds = value.toNonNegativeInt(-1)
            }
          }
        }
      }
    }
      
    fun compute(): CacheStrategy {...}  
    ...  
  }

In cachestrategy In the Factory construction method, it is mainly to judge whether the cached response cacheResponse exists. If so, the information corresponding to the response is saved in the Factory object.

compute

You can create a CacheStrategy object by calling the compute method of the Factory object

    /** Returns a strategy to satisfy [request] using [cacheResponse]. */
	// Use cacheResponse to return a policy that meets the Request
    fun compute(): CacheStrategy {
      val candidate = computeCandidate()

      // We're forbidden from using the network and the cache is insufficient.
      // We are banned from using the network and the cache is insufficient
      if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
        return CacheStrategy(null, null)
      }

      return candidate
    }
computeCandidate

The above compute method calls the computeCandidate method, which is as follows

    private fun computeCandidate(): CacheStrategy {
      // If the request has no corresponding cache response, the cache response is ignored and a network request is required
      if (cacheResponse == null) { 
        return CacheStrategy(request, null)
      }

      // If the request is an HTTPS request, but the TLS handshake related information is not saved in the cached response, the cached response is ignored and a network request is required
      if (request.isHttps && cacheResponse.handshake == null) {
        return CacheStrategy(request, null)
      }

      // Judge whether the response is allowed to be cached by the response code of cacheResponse. If not, ignore the cached response and require a network request 
      // (in fact, the value of the noStore instruction is also involved in the judgment.) 
      if (!isCacheable(cacheResponse, request)) {
        return CacheStrategy(request, null)
      }

      // CacheControl class: contains cache instructions from the server or client. These instructions indicate what response can be stored,
      // What needs can these stored responses meet.
      val requestCaching = request.cacheControl
      // noCache instruction description: noCache instructions can appear in requests and responses. If it appears at the location of the response, it indicates that it is published
      // Before caching a copy, you must verify the validity of the cache to the source server; If it appears in request, it indicates that a cache should not be used to respond
      // This requirement.  
      // hasConditions method: if the Request contains one of the headers if modified since or if none match,
      // The method returns true.
      // The meaning of If statement here: If the request does not allow the use of cached response, or the request header has If modified since / If none match, 	  //  The cached response is ignored and a network request is required. (the request sent by the client has If modified since or If none match
      // , the cached response will not be used. OkHttp adds if modified since or
      // If none match Header (logical) 
      if (requestCaching.noCache || hasConditions(request)) {
        return CacheStrategy(request, null)
      }

      val responseCaching = cacheResponse.cacheControl

      val ageMillis = cacheResponseAge()
      var freshMillis = computeFreshnessLifetime()

      if (requestCaching.maxAgeSeconds != -1) {
        freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
      }

      var minFreshMillis: Long = 0
      if (requestCaching.minFreshSeconds != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
      }

      var maxStaleMillis: Long = 0
      if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
      }

      // If the cacheResponse does not have a noCache instruction (you do not need to verify with the source server before publishing the cache), and the cacheResponse
      // If it is still within the lifetime, there is no need to make a network request and directly use the cached response  
      if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        val builder = cacheResponse.newBuilder()
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"")
        }
        val oneDayMillis = 24 * 60 * 60 * 1000L
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"")
        }
        return CacheStrategy(null, builder.build())
      }

      // Add headers of if none match / if modified since to the request Header. The server verifies these headers,
      // To determine whether the cache of the client is still valid. If the cache is still valid, 304 is returned, and the Response Body will not be included in the response  
      val conditionName: String
      val conditionValue: String?
      when {
        etag != null -> {
          conditionName = "If-None-Match"
          conditionValue = etag
        }

        lastModified != null -> {
          conditionName = "If-Modified-Since"
          conditionValue = lastModifiedString
        }

        servedDate != null -> {
          conditionName = "If-Modified-Since"
          conditionValue = servedDateString
        }

        // If there is no if none match / if modified since Header to add, the cached response will be ignored and a network request will be required   
        else -> return CacheStrategy(request, null) // No condition! Make a regular request.
      }
  
      val conditionalRequestHeaders = request.headers.newBuilder()
      conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)

      // Add a Header containing if none match / if modified since to the original Request  
      val conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build()
      // Returns a policy containing a network request and a cached response  
      return CacheStrategy(conditionalRequest, cacheResponse)
    }

The main function of the computeCandidate method is to create and return a CacheStrategy object according to the information of the request and cache response. The object indicates the following three situations

  • Ignore cached responses, subject to network requests
  • Do not make network requests, and directly use cached responses
  • Add the Header of if none match / if modified since to the request. At this time, combine the network request and cache response. If the server verifies that the cache response is still valid, it returns 304 and does not return the Response Body. If the server verifies that the cache response is invalid, it returns 200, and the returned response includes the Response Body

Summarize the steps of the computeCandidate method:

  1. The request has no corresponding cache response, and the network request is made directly
  2. The request is an HTTPS request, but the TLS handshake related data is not saved in the cache response. Ignore the cache and make a network request
  3. Whether the response is allowed to be cached. If not, make a network request directly
  4. If the request header contains a noCache instruction or if modified since / if none match header, the cache response is ignored and a network request is made
  5. If the cache response has no noCache instruction and the cache response has not expired, the cache response is used directly without network request
  6. If the cached response expires and the etag/lastModified/servedDate information is not saved, the network request is made directly
  7. If the cache response expires and the etag/Modified/servedDate information is saved in the cache response, add the information to the request header to make a network request, and save the cache response to the cache policy at the same time.

Another note:

  • In step 7, add etag/lastModified/servedDate information, that is, when adding if modified since / if none match header, if none match information is added before if modified since, which also shows that Etag / if none match has higher priority than last modified / if modified since.

Relationship combing

Let's sort out the construction process of CacheStrategy and its relationship with CacheInterceptor::intercept.

First, in CacheInterceptor::intercept, we will construct a cachestrategy Factory object, and the candidate cache is passed in as a construction parameter. In the Factory construction method, the candidate cache information is saved. Then, in the intercept method of the interceptor, the CacheStrategy object of the Factory object is invoked by the compute method of the Factory object, and the computeCandidate method is called inside the compute method. Based on the information of the candidate cache, build a cachestrategy object. The cachestrategy object indicates whether the network request needs to be made and whether the cache response is used. After obtaining the cachestrategy object, the intercept method will select the following according to the information in the policy:

  • report errors
  • Use cache
  • Make a network request
    • The server verifies that the cache resource is valid and uses the cache resource
    • The server verifies that the cache resource is invalid and uses the network request result

summary

OkHttp's Cache is mainly related to two classes: Cache class and CacheStrategy class. The Cache class is responsible for CRUD operations on the Response locally; CacheStrategy will judge whether the request is to make a network request, directly use the Cache Response, or let the server verify whether the Cache resources are valid according to the received request and the information of the candidate Cache. In its intercept method, the CacheInterceptor interceptor performs "error reporting, Cache usage, network request" and other operations according to the information obtained in the policy.

The CRUD operation of the Response by the Cache class depends on DiskLruCache, which implements the LruCache mechanism of the disk. Users can configure the Cache in OkHttpClient to customize the Cache address and Cache space size.

The general process is as follows:

reference resources

  1. Thoroughly understand the HTTP caching mechanism and principle - blog Garden.

Keywords: Android

Added by slawrence10 on Wed, 22 Dec 2021 05:17:40 +0200