OkHttp3 source code details cache strategy

Reasonable use of local cache can effectively reduce network overhead and response delay. The HTTP header also defines many cache related domains to control the cache. Let's talk about the implementation details of the cache part in OkHttp today.

  1. HTTP cache policy
    First of all, let's understand the related domains of the cache part in the HTTP protocol.

1.1 Expires
Timeout is generally used in the response header of the server to inform the client of the expiration time of the corresponding resource. When the client needs to request the same resource again, it first compares its expiration time. If it has not exceeded the expiration time, it directly returns the cache result. If it has exceeded the expiration time, it requests again.

1.2 Cache-Control
The relative value, in hours and seconds, indicates the validity period of the current resource. Cache control has a higher priority than Expires:

Cache-Control:max-age=31536000,public
1.3 conditional GET request
1.3.1 Last-Modified-Date
When the client first requests, the server returns:

Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT
When the client makes a second request, the following header can be added to the header:

If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT
If the current resource is not modified twice, the server returns 304 and tells the client to reuse the local cache directly.

1.3.2 ETag
ETag is a kind of summary of resource file. It can judge whether the file has been modified by ETag value. When the client requests a resource for the first time, the server returns:

ETag: "5694c7ef-24dc"
When the client requests again, the following fields can be added to the header:

If-None-Match: "5694c7ef-24dc"
If the file does not change, the server returns 304 to inform the client that the local cache can be reused.

1.4 no-cache/no-store
Do not use cache

1.5 only-if-cached
Cache only

  1. Cache source code analysis
    The caching of OkHttp is done in the CacheInterceptor. The Cache part has the following key classes:

Cache: cache manager, which contains a DiskLruCache to write cache to file system:

  • Cache Optimization


    *
  • To measure cache effectiveness, this class tracks three statistics:

  • {@linkplain #requestCount() Request Count:} the number of HTTP
  • requests issued since this cache was created.
  • {@linkplain #networkCount() Network Count:} the number of those
  • requests that required network use.
  • {@linkplain #hitCount() Hit Count:} the number of those requests
  • whose responses were served by the cache.

*Sometimes a request will result in a conditional cache hit. If the cache contains a stale copy ofthe response, the client will issue a conditional {@code GET}. The server will then send eitherthe updated response if it has changed, or a short 'not modified' response if the client's copyis still valid. Such responses increment both the network count and hit count.
*

The best way to improve the cache hit rate is by configuring the web server to return

cacheable responses. Although this client honors all href="https://yq.aliyun.com/go/articleRenderRedirect?url=http%3A%2F%2Ftools.ietf.org%2Fhtml%2Frfc7234">HTTP/1.1 (RFC 7234)data-url="http://tools.ietf.org/html/rfc7234"> cache headers, it doesn't cachepartial responses.
The internal Cache optimizes the Cache efficiency through three statistical indicators: requestcount, networkcount and hitcount

CacheStrategy: cache policy. It maintains a request and response internally. By specifying the request and response, it describes whether to obtain the response through the network or cache, or both

[CacheStrategy.java]
/**

  • Given a request and cached response, this figures out whether to use the network, the cache, or
  • both.
    *
  • Selecting a cache strategy may add conditions to the request (like the "If-Modified-Since"

  • header for conditional GETs) or warnings to the cached response (if the cached data is
  • potentially stale).
    */

public final class CacheStrategy {
/* The request to send on the network, or null if this call doesn't use the network. /
public final Request networkRequest;

/* The cached response to return or validate; or null if this call doesn't use a cache. /
public final Response cacheResponse;
......
}
CacheStrategy$Factory: the cache policy factory class returns the corresponding cache policy according to the actual request

Since the actual cache work is done in CacheInterceptor, let's look at the source code of the core method of CahceInterceptor, intercept method:

[CacheInterceptor.java]
@Override public Response intercept(Chain chain) throws IOException {

//First try to get the cache
Response cacheCandidate = cache != null
    ? cache.get(chain.request())
    : null;

long now = System.currentTimeMillis();

//Get cache policy
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;

//If there is a cache, update the following statistics: hit rate
if (cache != null) {
  cache.trackResponse(strategy);
}

//If the current cache does not meet the requirements, close it
if (cacheCandidate != null && cacheResponse == null) {
  closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}

// If the network cannot be used, and there is no qualified cache, the 504 error will be thrown directly
if (networkRequest == null && cacheResponse == null) {
  return new Response.Builder()
      .request(chain.request())
      .protocol(Protocol.HTTP_1_1)
      .code(504)
      .message("Unsatisfiable Request (only-if-cached)")
      .body(Util.EMPTY_RESPONSE)
      .sentRequestAtMillis(-1L)
      .receivedResponseAtMillis(System.currentTimeMillis())
      .build();
}

// If there is a cache and the network is not used, the cache result will be returned directly
if (networkRequest == null) {
  return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}

//Try to get a reply through the network
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 there is both cache and request, it means that it is a Conditional Get request at this time
if (cacheResponse != null) {
  // If the server returns not modified, the cache is valid. Merge the local cache and network response
  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();
    cache.update(cacheResponse, response);
    return response;
  } else {// If the response resource is updated, turn off the original cache
    closeQuietly(cacheResponse.body());
  }
}

Response response = networkResponse.newBuilder()
    .cacheResponse(stripBody(cacheResponse))
    .networkResponse(stripBody(networkResponse))
    .build();

if (cache != null) {
  if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
    // Write network response to 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;

}
The core logic is marked out in the code in the form of Chinese annotation. You can read the code. From the above code, it can be seen that almost all actions are made based on the CacheStrategy. Next, let's see how the cache strategy is generated. The relevant code is implemented in the CacheStrategy$Factory.get() method:

[CacheStrategy$Factory]

/**
 * Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
 */
public CacheStrategy get() {
  CacheStrategy candidate = getCandidate();

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

  return candidate;
}

/** Returns a strategy to use assuming the request can use the network. */
private CacheStrategy getCandidate() {
  // If there is no cache locally, initiate a network request
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }

  // If the current request is HTTPS and the cache does not have TLS handshake, restart the network request
  if (request.isHttps() && cacheResponse.handshake() == null) {
    return new CacheStrategy(request, null);
  }

  // If this response shouldn't have been stored, it should never be used
  // as a response source. This check should be redundant as long as the
  // persistence store is well-behaved and the rules are constant.
  if (!isCacheable(cacheResponse, request)) {
    return new CacheStrategy(request, null);
  }
    

  //If the current cache policy is not to cache or conditional get, initiate a network request
  CacheControl requestCaching = request.cacheControl();
  if (requestCaching.noCache() || hasConditions(request)) {
    return new CacheStrategy(request, null);
  }

  //ageMillis: cache age
  long ageMillis = cacheResponseAge();
  //freshMillis: cache freshness time
  long freshMillis = computeFreshnessLifetime();

  if (requestCaching.maxAgeSeconds() != -1) {
    freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
  }

  long minFreshMillis = 0;
  if (requestCaching.minFreshSeconds() != -1) {
    minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
  }

  long maxStaleMillis = 0;
  CacheControl responseCaching = cacheResponse.cacheControl();
  if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
  }

  //If age + min fresh > = max age & & age + min fresh < Max age + Max stale, although the cache is expired, / / the cache can continue to be used, only adding 110 warning codes in the header
  if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis)      {
    Response.Builder builder = cacheResponse.newBuilder();
    if (ageMillis + minFreshMillis >= freshMillis) {
      builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
    }
    long oneDayMillis = 24 * 60 * 60 * 1000L;
    if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
      builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
    }
    return new CacheStrategy(null, builder.build());
  }

  // Initiate a conditional get request
  String conditionName;
  String conditionValue;
  if (etag != null) {
    conditionName = "If-None-Match";
    conditionValue = etag;
  } else if (lastModified != null) {
    conditionName = "If-Modified-Since";
    conditionValue = lastModifiedString;
  } else if (servedDate != null) {
    conditionName = "If-Modified-Since";
    conditionValue = servedDateString;
  } else {
    return new CacheStrategy(request, null); // No condition! Make a regular request.
  }

  Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
  Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

  Request conditionalRequest = request.newBuilder()
      .headers(conditionalRequestHeaders.build())
      .build();
  return new CacheStrategy(conditionalRequest, cacheResponse);
}

You can see that the core logic is in the getCandidate function. Basically, it is the implementation of HTTP Caching Protocol. The core code logic has been explained by Chinese annotation. You can just read the code directly.

  1. DiskLruCache
    In the cache, DiskLruCache is used to manage the creation, reading and cleaning of the cache at the file system level. Next, let's look at the main logic of DiskLruCache:

public final class DiskLruCache implements Closeable, Flushable {

final FileSystem fileSystem;
final File directory;
private final File journalFile;
private final File journalFileTmp;
private final File journalFileBackup;
private final int appVersion;
private long maxSize;
final int valueCount;
private long size = 0;
BufferedSink journalWriter;
final LinkedHashMap lruEntries = new LinkedHashMap<>(0, 0.75f, true);

// Must be read and written when synchronized on 'this'.
boolean initialized;
boolean closed;
boolean mostRecentTrimFailed;
boolean mostRecentRebuildFailed;

/**

  • To differentiate between old and current snapshots, each entry is given a sequence number each
  • time an edit is committed. A snapshot is stale if its sequence number is not equal to its
  • entry's sequence number.
    */

private long nextSequenceNumber = 0;

/* Used to run 'cleanupRunnable' for journal rebuilds. /
private final Executor executor;
private final Runnable cleanupRunnable = new Runnable() {

public void run() {
    ......
}

};
...
}
3.1 journalFile
The internal log file of DiskLruCache corresponds to a log record for each read and write of the cache. DiskLruCache analyzes and creates the cache by analyzing the log. The format of log file is as follows:

  libcore.io.DiskLruCache
  1
  100
  2

  CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
  DIRTY 335c4c6028171cfddfbaae1a9c313c52
  CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
  REMOVE 335c4c6028171cfddfbaae1a9c313c52
  DIRTY 1ab96a171faeeee38496d8b330771a7a
  CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
  READ 335c4c6028171cfddfbaae1a9c313c52
  READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
 
 //The first five lines are fixed, which are: constant: libcore.io.DiskLruCache; diskCache version; application version; valuecount (later), blank lines
 
 //Next, each line corresponds to a status record of a cache entry. Its format is: [DIRTY,CLEAN,READ,REMOVE, key, status related value (optional)]:
 - DIRTY:Show a cache entry Being created or updated, every successful DIRTY All records should correspond to one CLEAN or REMOVE Operation. If one DIRTY Missing expected match CLEAN/REMOVE,Corresponding entry Operation failed, need to transfer it from lruEntries Delete in
 - CLEAN:Explain cache It has been operated successfully and can be read normally at present. Every last CLEAN Lines also need to record each of them value Length
 - READ: Record once cache read operation
 - REMOVE:Record once cache Eliminate
 

There are four application scenarios for log files:

During DiskCacheLru initialization, the cache container is created by reading the log file: lruEntries. At the same time, the cache items that failed in the log filtering operation. The related logic is in DiskLruCache.readJournalLine,DiskLruCache.processJournal
After initialization, in order to avoid the expansion of log files, the log is rebuilt and simplified. The specific logic is in DiskLruCache.rebuildJournal
Every time there is a cache operation, it is recorded in the log file for the next initialization
When there are too many redundant logs, rebuild the logs by calling the cleanUpRunnable thread
3.2 DiskLruCache.Entry
Each DiskLruCache.Entry corresponds to a cache record:

private final class 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);
  }
}
...
 
    /**
 * Returns a snapshot of this entry. This opens all streams eagerly to guarantee that we see a
 * single published snapshot. If we opened streams lazily then the streams could come from
 * different edits.
 */
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;
  }
}

}
An Entry consists of the following parts:

Key: each cache has a key as its identifier. The key of the current cache is the MD5 string of its corresponding URL
cleanFiles/dirtyFiles: each Entry corresponds to multiple files, and the corresponding number of files is specified by DiskLruCache.valueCount. valueCount is currently 2 in OkHttp. That is, each cache corresponds to two cleanFiles and two dirtyFiles. The first cleanFiles/dirtyFiles record the meta data of cache (such as URL, creation time, SSL handshake record, etc.), and the second file records the real content of cache. cleanFiles records the cache results in a stable state, and dirtyFiles records the cache in a created or updated state
Current editor: entry editor, through which all operations on entry are completed. Synchronization lock added inside editor
3.3 cleanupRunnable
Clean up threads to rebuild thin logs:

private final Runnable cleanupRunnable = new Runnable() {

public void run() {
  synchronized (DiskLruCache.this) {
    if (!initialized | closed) {
      return; // Nothing to do
    }

    try {
      trimToSize();
    } catch (IOException ignored) {
      mostRecentTrimFailed = true;
    }

    try {
      if (journalRebuildRequired()) {
        rebuildJournal();
        redundantOpCount = 0;
      }
    } catch (IOException e) {
      mostRecentRebuildFailed = true;
      journalWriter = Okio.buffer(Okio.blackhole());
    }
  }
}

};
The trigger condition is in the journalRebuildRequired() method:

/**

  • We only rebuild the journal when it will halve the size of the journal and eliminate at least
  • 2000 ops.
    */

boolean journalRebuildRequired() {

final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold
    && redundantOpCount >= lruEntries.size();

}
Execute when the redundant log exceeds the general number of log files and the total number of logs exceeds 2000

3.4 SnapShot
Cache snapshot, which records the contents of a specific cache at a specific time. Each time a request is made to DiskLruCache, a snapshot of the target cache is returned. The related logic is in DiskLruCache.get:

[DiskLruCache.java]
/**

  • Returns a snapshot of the entry named {@code key}, or null if it doesn't exist is not currently
    1. If a value is returned, it is moved to the head of the LRU queue.
      */

public synchronized Snapshot get(String key) throws IOException {

initialize();

checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null || !entry.readable) return null;

Snapshot snapshot = entry.snapshot();
if (snapshot == null) return null;

redundantOpCount++;
//Log record
journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
if (journalRebuildRequired()) {
  executor.execute(cleanupRunnable);
}

return snapshot;

}
3.5 lruEntries
The container for managing the cache entry, whose data structure is LinkedHashMap. LRU replacement of cache is achieved through the implementation logic of LinkedHashMap itself

3.6 FileSystem
Using Okio to encapsulate File simplifies I/O operation.

3.7 DiskLruCache.edit
DiskLruCache can be regarded as the specific implementation of Cache in the file system layer, so its basic operation interface has a one-to-one correspondence relationship:

Cache.get() —>DiskLruCache.get()
Cache.put() - > disklrucache. Edit() / / cache insert
Cache.remove()—>DiskLruCache.remove()
Cache.update() - > disklrucache. Edit() / / cache update
The get operation has been introduced in 3.4. The remove operation is relatively simple, and the put and update logic are similar. Because of space limitations, only the logic of the Cache.put operation is introduced here. For other operations, you can see the code:

[okhttp3.Cache.java]
CacheRequest put(Response response) {

String requestMethod = response.request().method();

if (HttpMethod.invalidatesCache(response.request().method())) {
  try {
    remove(response.request());
  } catch (IOException ignored) {
    // The cache cannot be written.
  }
  return null;
}
if (!requestMethod.equals("GET")) {
  // Don't cache non-GET responses. We're technically allowed to cache
  // HEAD requests and some POST requests, but the complexity of doing
  // so is high and the benefit is low.
  return null;
}

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

Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
  editor = cache.edit(key(response.request().url()));
  if (editor == null) {
    return null;
  }
  entry.writeTo(editor);
  return new CacheRequestImpl(editor);
} catch (IOException e) {
  abortQuietly(editor);
  return null;
}

}
You can see the core logic in editor = cache.edit (key (response. Request(). Url());, and the relevant code in DiskLruCache.edit:

[okhttp3.internal.cache.DiskLruCache.java]
synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {

initialize();

checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
    || entry.sequenceNumber != expectedSequenceNumber)) {
  return null; // Snapshot is stale.
}
if (entry != null && entry.currentEditor != null) {
  return null; // The current cache entry is being operated by another object
}
if (mostRecentTrimFailed || mostRecentRebuildFailed) {
  // The OS has become our enemy! If the trim job failed, it means we are storing more data than
  // requested by the user. Do not allow edits so we do not go over that limit any further. If
  // the journal rebuild failed, the journal writer will not be active, meaning we will not be
  // able to record the edit, causing file leaks. In both cases, we want to retry the clean up
  // so we can get out of this state!
  executor.execute(cleanupRunnable);
  return null;
}

// Log access to DIRTY record
journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
journalWriter.flush();

if (hasJournalErrors) {
  return null; // Don't edit; the journal can't be written.
}

if (entry == null) {
  entry = new Entry(key);
  lruEntries.put(key, entry);
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
return editor;

}
The edit method returns the editor editor of the corresponding CacheEntry. Next, let's look at the entry.writeTo(editor); of the Cache.put() method, and its related logic:

[okhttp3.internal.cache.DiskLruCache.java]
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());
    // The handshake's TLS version is null on HttpsURLConnection and on older cached responses.
    if (handshake.tlsVersion() != null) {
      sink.writeUtf8(handshake.tlsVersion().javaName())
          .writeByte('\n');
    }
  }
  sink.close();
}

The main logic is to write the meta data of the corresponding request to the dirtyfile with the index of the corresponding CacheEntry as entry ﹣ metadata (0).

Finally, see the return new CacheRequestImpl(editor) of the Cache.put() method

[okhttp3.Cache$CacheRequestImpl]
private final class CacheRequestImpl implements CacheRequest {

private final DiskLruCache.Editor editor;
private Sink cacheOut;
private Sink body;
boolean done;

public CacheRequestImpl(final DiskLruCache.Editor editor) {
  this.editor = editor;
  this.cacheOut = editor.newSink(ENTRY_BODY);
  this.body = new ForwardingSink(cacheOut) {
    @Override public void close() throws IOException {
      synchronized (Cache.this) {
        if (done) {
          return;
        }
        done = true;
        writeSuccessCount++;
      }
      super.close();
      editor.commit();
    }
  };
}

@Override public void abort() {
  synchronized (Cache.this) {
    if (done) {
      return;
    }
    done = true;
    writeAbortCount++;
  }
  Util.closeQuietly(cacheOut);
  try {
    editor.abort();
  } catch (IOException ignored) {
  }
}

@Override public Sink body() {
  return body;
}

}
The close and abort methods will call editor.abort and editor.commit to update the log, and editor.commit will reset dirtyFile to cleanFile as a stable and available cache. The related logic is in okhttp3.internal.cache.DiskLruCache$Editor.completeEdit:

[okhttp3.internal.cache.DiskLruCache$Editor.completeEdit]
synchronized void completeEdit(Editor editor, boolean success) throws IOException {

Entry entry = editor.entry;
if (entry.currentEditor != editor) {
  throw new IllegalStateException();
}

// If this edit is creating the entry for the first time, every index must have a value.
if (success && !entry.readable) {
  for (int i = 0; i < valueCount; i++) {
    if (!editor.written[i]) {
      editor.abort();
      throw new IllegalStateException("Newly created entry didn't create value for index " + i);
    }
    if (!fileSystem.exists(entry.dirtyFiles[i])) {
      editor.abort();
      return;
    }
  }
}

for (int i = 0; i < valueCount; i++) {
  File dirty = entry.dirtyFiles[i];
  if (success) {
    if (fileSystem.exists(dirty)) {
      File clean = entry.cleanFiles[i];
      fileSystem.rename(dirty, clean);//Set dirtyfile to cleanfile
      long oldLength = entry.lengths[i];
      long newLength = fileSystem.size(clean);
      entry.lengths[i] = newLength;
      size = size - oldLength + newLength;
    }
  } else {
    fileSystem.delete(dirty);//Delete dirtyfile if it fails
  }
}

redundantOpCount++;
entry.currentEditor = null;
//Update log
if (entry.readable | success) {
  entry.readable = true;
  journalWriter.writeUtf8(CLEAN).writeByte(' ');
  journalWriter.writeUtf8(entry.key);
  entry.writeLengths(journalWriter);
  journalWriter.writeByte('\n');
  if (success) {
    entry.sequenceNumber = nextSequenceNumber++;
  }
} else {
  lruEntries.remove(entry.key);
  journalWriter.writeUtf8(REMOVE).writeByte(' ');
  journalWriter.writeUtf8(entry.key);
  journalWriter.writeByte('\n');
}
journalWriter.flush();

if (size > maxSize || journalRebuildRequired()) {
  executor.execute(cleanupRunnable);
}

}
CacheRequestImpl implements the CacheRequest interface, which is exposed to external classes (mainly CacheInterceptor), and external objects update or write cache data through CacheRequestImpl.

3.8 summary
In summary, DiskLruCache has the following characteristics:

LRU replacement through LinkedHashMap
Maintain the Cache operation log locally to ensure the atomicity and availability of the Cache. At the same time, perform the log reduction regularly to prevent the log from over expanding
Each cache entry corresponds to two state copies: DIRTY and CLEAN. CLEAN indicates the cache in the current available state, and all cache snapshots accessed externally are in CLEAN state; DIRTY indicates the cache in update state. Since the update and creation only operate on the DIRTY state copy, the read-write separation of cache is realized
Each Cache item has four files, two states (DIRTY,CLEAN). Each state corresponds to two files: one file stores Cache meta data, and one file stores Cache content data

Author: Li yabrush
Original link: https://www.jianshu.com/p/87da91631a70

Keywords: Operation & Maintenance network snapshot Java OkHttp

Added by Fog Juice on Mon, 18 Nov 2019 11:58:13 +0200