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
value | explain |
---|---|
private | Client can cache |
public | Both client and proxy servers can cache |
max-age=xxx | Cached data expires in xxx seconds |
no-cache | You 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:
-
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.
-
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:
- Read candidate Cache from Cache with Request as key
- 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
- 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
- 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
- Conduct network operation, submit the request to the following interceptor for processing, and obtain the returned Response at the same time
- 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
- 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:
networkRequest | cacheResponse | Corresponding processing |
---|---|---|
null | null | An error is reported directly, and the status code returns 504 |
null | non-null | Directly return cached Response |
non-null | null | Returns the Response obtained by the network. If the caching conditions are met, the Response is cached |
non-null | non-null | If 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
- 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
- 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:
- The request has no corresponding cache response, and the network request is made directly
- 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
- Whether the response is allowed to be cached. If not, make a network request directly
- 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
- If the cache response has no noCache instruction and the cache response has not expired, the cache response is used directly without network request
- If the cached response expires and the etag/lastModified/servedDate information is not saved, the network request is made directly
- 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: