As time flies, today I want to bring you the last article from Zuihou about OkHttp caching. There is one interceptor in OkHttp’s chain of responsibility that specifically addresses OkHttp’s cache: the CacheInterceptor interceptor.

CacheInterceptor

The corresponding method is as follows. Let’s start with this method:

Public Response Intercept (Chain Chain) throws IOException {// Null Response cacheCandidate = cache! = null ? cache.get(chain.request()) : null; long now = System.currentTimeMillis(); CacheStrategy Strategy = new cacheStrategy.factory (now, chain-.request (), cacheCandidate).get(); // Cache policy Request networkRequest = strategy.networkRequest; // Cache policy Response cacheResponse = strategy.cacheResponse; // if (cache! = null) { cache.trackResponse(strategy); } // The local cache is not empty and the cache policy response is empty if (cacheCandidate! = null && cacheResponse == null) { closeQuietly(cacheCandidate.body()); Close it.} // If we're forbidden from using The network and the cache is insufficient, fail. if (networkRequest == null && cacheResponse == null) { return new Response.Builder() .request(chain.request()) .protocol(Protocol.HTTP_1_1) .code(504) .message("Unsatisfiable Request (only-if-cached)") .body(Util.EMPTY_RESPONSE) .sentRequestAtMillis(-1L) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); } // Cache policy request is null, // If we don't need the network, we're done. if (networkRequest == null) { return cacheResponse.newBuilder() .cacheResponse(stripBody(cacheResponse)) .build(); } // The cache is invalid, the next interceptor is executed to get the request Response networkResponse = null; try { networkResponse = chain.proceed(networkRequest); } finally { // If we're crashing on I/O or otherwise, don't leak the cache body. if (networkResponse == null && cacheCandidate ! = null) { closeQuietly(cacheCandidate.body()); } // If we have a cache response too, then we're doing a conditional get. If (cacheResponse! = null) { if (networkResponse.code() == HTTP_NOT_MODIFIED) { Response response = cacheResponse.newBuilder() .headers(combine(cacheResponse.headers(), networkResponse.headers())) .sentRequestAtMillis(networkResponse.sentRequestAtMillis()) .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis()) .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); networkResponse.body().close(); // Update 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; } else { closeQuietly(cacheResponse.body()); }} // No cache, Directly using the network Response Response Response. = networkResponse newBuilder () cacheResponse (stripBody (cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); if (cache ! = null) {/ / to the local cache if (HttpHeaders. HasBody (response) && CacheStrategy. IsCacheable (the response, networkRequest)) { // Offer this request to the cache. CacheRequest cacheRequest = cache.put(response); return cacheWritingResponse(cacheRequest, response); } if (HttpMethod.invalidatesCache(networkRequest.method())) { try { cache.remove(networkRequest); } catch (IOException ignored) { // The cache cannot be written. } } } return response; }Copy the code

This is the main method of the entire cache interceptor. It first fetches the cache from the cache, returns null if it doesn’t, and then fetches the cache policy via CacheStrategy, which is based on the results of the previous cache and the headers that are currently being sent. And get the result of whether or not to make the request. Due to the lack of space, this section will not be discussed in detail because it involves network protocols. Finally, his rules are as follows:




image.png


Because I have written the annotation process in the code, you can see the above method code to understand, its overall cache process is as follows:

  1. If there is a cache, fetch the cache otherwise null
  2. Get its cache policy requests and responses according to CacheStrategy
  3. The cache policy request and cache policy response are empty, and direct network return is prohibited
  4. If the cache policy request is null, the cache is used instead of the network if the cache is valid
  5. If the cache is invalid, the next interceptor is executed to retrieve the request
  6. If there is also a local cache, the response is selected based on the criteria and the cache is updated
  7. Without caching, the network response is used directly
  8. Add the cache

We can see that the cache class is responsible for adding, deleting, modifying and querying the cache. Let’s take a look at this class.

Cache

Add, delete, alter, and query a Cache using DiskLruCache

  • Add the cache
CacheRequest put(Response response) { String requestMethod = response.request().method(); / / if the request is "POST", "PUT", "PATCH", "PROPPATCH", "the REPORT" is to remove the cache if (HttpMethod. InvalidatesCache (response. The request () method ())) { try { remove(response.request()); } catch (IOException ignored) { } return null; } // Only GET requests are cached. requestMethod.equals("GET")) { return null; } if (HttpHeaders. HasVaryAll (response)) {return null; } // Build response into an Entry object. Entry = new Entry(response); DiskLruCache.Editor editor = null; Editor = cache.edit(key(response.request().url()))); if (editor == null) { return null; } // Write entry to the cache. WriteTo (editor); IO Sink object return new CacheRequestImpl(Editor); } catch (IOException e) { abortQuietly(editor); return null; }}Copy the code
  • Get the cache
String key = key(request.url()); String key = key(request.url()); DiskLruCache.Snapshot snapshot; Entry entry; Try {// Obtain the corresponding snapshot based on the key snapshot = cache.get(key); if (snapshot == null) { return null; } } catch (IOException e) { return null; } try {// Create an Entry object and get Sink Entry from snapshot.getSource() = new Entry(snapshot.getsource (ENTRY_METADATA)); } catch (IOException e) { Util.closeQuietly(snapshot); return null; } // Generate respson from entry and response, obtain request body from okio.buffer, then encapsulate various request information response response = entry.response(snapshot); if (! Entry. Matches (request, response)) {// Matches request with response. Util.closeQuietly(response.body()); return null; } return response; }Copy the code
  • Update the cache
Void update(Response cached, Response network) {// Create an Entry with Respon Entry = new Entry(network); DiskLruCache.Snapshot DiskLruCache.Snapshot Snapshot = ((CacheResponseBody) cached.body()).snapshot; DiskLruCache.Editor editor = null; Try {/ / get DiskLruCache. The Snapshot. Edit the object editor = the Snapshot. Edit (); // Returns null if snapshot is not current. if (editor ! = null) {// Write entry into the editor. WriteTo (editor); editor.commit(); } } catch (IOException e) { abortQuietly(editor); }}Copy the code
  • Delete the cache
Void remove(Request Request) throws IOException {// Delete cache cache.remove(key(request.url())) using the key converted from the URL. }Copy the code

The “add, delete, modify and query” function of a Cache is generally commented in code. The more important Cache handling class is DiskLruCache.

DiskLruCache

DiskLruCache by JakeWharton :[link.jianshu.com/t=https://g… IO is used to store data files. As you can see, shang has three internal classes, namely Entry, Snapshot, The Editor.

Entry

final String key; /** Lengths of this entry's files. */ final long[] lengths; final File[] cleanFiles; final File[] dirtyFiles; /** True if this entry has ever been published. */ boolean readable; /** The ongoing edit or null if this entry is not being edited. */ Editor currentEditor; /** The sequence number of the most recently committed edit to this entry. */ long sequenceNumber; Entry(String key) { this.key = key; lengths = new long[valueCount]; cleanFiles = new File[valueCount]; dirtyFiles = new File[valueCount]; // The names are repetitive so re-use the same builder to avoid allocations. StringBuilder fileBuilder = new StringBuilder(key).append('.'); int truncateTo = fileBuilder.length(); for (int i = 0; i < valueCount; i++) { fileBuilder.append(i); cleanFiles[i] = new File(directory, fileBuilder.toString()); fileBuilder.append(".tmp"); dirtyFiles[i] = new File(directory, fileBuilder.toString()); fileBuilder.setLength(truncateTo); }} // omit......Copy the code

An entity class is used to store cached data. Each URL corresponds to an entity. There is also a Snapshot object in Entry.

Snapshot snapshot() { if (! Thread.holdsLock(DiskLruCache.this)) throw new AssertionError(); Source[] sources = new Source[valueCount]; long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out. try { for (int i = 0; i < valueCount; i++) { sources[i] = fileSystem.source(cleanFiles[i]); } return new Snapshot(key, sequenceNumber, sources, lengths); } catch (FileNotFoundException e) { // A file must have been deleted manually! for (int i = 0; i < valueCount; i++) { if (sources[i] ! = null) { Util.closeQuietly(sources[i]); } else { break; } } // Since the entry is no longer valid, remove it so the metadata is accurate (i.e. the cache // size.) try { removeEntry(this); } catch (IOException ignored) { } return null; }}Copy the code

An Entry corresponds to a Snapshot object. Take a look at the Snapshot internal code:

public final class Snapshot implements Closeable { private final String key; private final long sequenceNumber; private final Source[] sources; private final long[] lengths; Snapshot(String key, long sequenceNumber, Source[] sources, long[] lengths) { this.key = key; this.sequenceNumber = sequenceNumber; this.sources = sources; this.lengths = lengths; } public String key() { return key; } /** * Returns an editor for this snapshot's entry, or null if either the entry has changed since * this snapshot was created or if another edit is in progress. */ public @Nullable Editor edit() throws IOException { return DiskLruCache.this.edit(key, sequenceNumber); } /** Returns the unbuffered stream with the value for {@code index}. */ public Source getSource(int index) { return sources[index]; } /** Returns the byte length of the value for {@code index}. */ public long getLength(int index) { return lengths[index]; } public void close() { for (Source in : sources) { Util.closeQuietly(in); }}}Copy the code

The initialized Snapshot simply stores some variables.

Editor

In the initialization of the Editor, you pass in an Editor, which is the class that edits entry. The source code is as follows:

public final class Editor { final Entry entry; final boolean[] written; private boolean done; Editor(Entry entry) { this.entry = entry; this.written = (entry.readable) ? null : new boolean[valueCount]; } void detach() { if (entry.currentEditor == this) { for (int i = 0; i < valueCount; i++) { try { fileSystem.delete(entry.dirtyFiles[i]); } catch (IOException e) { // This file is potentially leaked. Not much we can do about that. } } entry.currentEditor = null; Public Source newSource(int index) {synchronized (DiskLruCache. This) {if (done) {throw new IllegalStateException(); } if (! entry.readable || entry.currentEditor ! = this) { return null; } try { return fileSystem.source(entry.cleanFiles[index]); } catch (FileNotFoundException e) { return null; Public Sink newSink(int index) {synchronized (DiskLruCache. This) {if (done) {throw new IllegalStateException(); } if (entry.currentEditor ! = this) { return Okio.blackhole(); } if (! entry.readable) { written[index] = true; } File dirtyFile = entry.dirtyFiles[index]; Sink sink; try { sink = fileSystem.sink(dirtyFile); } catch (FileNotFoundException e) { return Okio.blackhole(); } return new FaultHidingSink(sink) { @Override protected void onException(IOException e) { synchronized (DiskLruCache.this) { detach(); }}}; }} // The job is to commit the data and release the lock, Public void commit() throws IOException {synchronized (DiskLruCache. This) {if (done) {throw new IllegalStateException(); } if (entry.currentEditor == this) { completeEdit(this, true); } done = true; }} // Stop editing, Public void abort() throws IOException {synchronized (DiskLruCache. This) {if (done) {throw new IllegalStateException(); } if (entry.currentEditor == this) { completeEdit(this, false); } done = true; }} public void abortUnlessCommitted() {synchronized (DiskLruCache. This) {if (! done && entry.currentEditor == this) { try { completeEdit(this, false); } catch (IOException ignored) { } } } } }Copy the code

The corresponding functions of each method are as follows:

  • Source newSource(int index) : Returns the cleanFile read stream with the specified index
  • Sink newSink(int index) : writes data to the dirtyFiles of the specified index
  • Commit () : The job is to commit the data, release the lock, and finally notify DiskLruCache to refresh the relevant data
  • Abort () : Terminates the edit and releases the lock
  • AbortUnlessCommitted () : Terminates unless you are editing

WriteTo (Editor); writeTo(editor); , the operations are as follows:

 public void writeTo(DiskLruCache.Editor editor) throws IOException {
      BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));

      sink.writeUtf8(url)
          .writeByte('\n');
      sink.writeUtf8(requestMethod)
          .writeByte('\n');
      sink.writeDecimalLong(varyHeaders.size())
          .writeByte('\n');
      for (int i = 0, size = varyHeaders.size(); i < size; i++) {
        sink.writeUtf8(varyHeaders.name(i))
            .writeUtf8(": ")
            .writeUtf8(varyHeaders.value(i))
            .writeByte('\n');
      }

      sink.writeUtf8(new StatusLine(protocol, code, message).toString())
          .writeByte('\n');
      sink.writeDecimalLong(responseHeaders.size() + 2)
          .writeByte('\n');
      for (int i = 0, size = responseHeaders.size(); i < size; i++) {
        sink.writeUtf8(responseHeaders.name(i))
            .writeUtf8(": ")
            .writeUtf8(responseHeaders.value(i))
            .writeByte('\n');
      }
      sink.writeUtf8(SENT_MILLIS)
          .writeUtf8(": ")
          .writeDecimalLong(sentRequestMillis)
          .writeByte('\n');
      sink.writeUtf8(RECEIVED_MILLIS)
          .writeUtf8(": ")
          .writeDecimalLong(receivedResponseMillis)
          .writeByte('\n');

      if (isHttps()) {
        sink.writeByte('\n');
        sink.writeUtf8(handshake.cipherSuite().javaName())
            .writeByte('\n');
        writeCertList(sink, handshake.peerCertificates());
        writeCertList(sink, handshake.localCertificates());
        sink.writeUtf8(handshake.tlsVersion().javaName()).writeByte('\n');
      }
      sink.close();
    }
Copy the code

IO operation, do not understand Ok. IO can go to see the relevant knowledge. BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA)); Editor. newSink gets the ok. IO version of the OutputStream(Sink) and generates the ok. IO input class. All that’s left is to write the data to the file with OK. IO and close the output class.

Response Response = entry.response(snapshot); In response, there is another method: CacheResponseBody().

CacheResponseBody(final DiskLruCache.Snapshot snapshot,String contentType, String contentLength) { this.snapshot = snapshot; this.contentType = contentType; this.contentLength = contentLength; Source source = snapshot.getSource(ENTRY_BODY); bodySource = Okio.buffer(new ForwardingSource(source) { @Override public void close() throws IOException { snapshot.close(); super.close(); }}); }Copy the code

New ForwardingSource(source) is the equivalent of InputStream(source) passed into ok.io to generate ok.io’s read class. All that is left is to read cached data and generate Response.

The Update() method in the Cache is the same as the Update() method in the Cache. The only difference is that it creates an Entry and then writes a new Entry to the Cache.

Delete entry: delete entry: delete entry: delete entry: delete entry

Boolean removeEntry(Entry Entry) throws IOException {// omit journalwriter.writeutf8 (REMOVE).writebyte (' ').writeUtf8(entry.key).writeByte('\n'); lruEntries.remove(entry.key); // omit return true; }Copy the code

Journalwriter. writeUtf8 means to delete from the local cache list of DiskLruCache, and lruentries. remove means to delete from the cache memory.

This is the end of the process of adding and deleting to check. In fact, there is more to DiskLruCache, but MY focus is OKhttp’s cache base is ok. IO, so I will stop here.

The content is a bit too much, if there are mistakes, please point out more