OkHttp框架源码解析——缓存策略

前言

合理利用本地缓存可以有效地减少网络开销,减少响应延迟。

HTTP缓存策略

HTTP报头也定义了很多与缓存有关的域来控制缓存。

  • Expires

    超时时间,一般用在服务器的response报头中用于告知客户端对应资源的过期时间。当客户端需要再次请求相同资源时先比较其过期时间,如果尚未超过过期时间则直接返回缓存结果,如果已经超过则重新请求

  • Cache-Control

    相对值,单位为秒,表示当前资源的有效期,Cache-Control比Expires优先级更高

  • 条件GET请求

    • Last-Modified-Date

      客户端第一次请求时,服务器返回:Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT

      当客户端二次请求时,可以在头部加上header:If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT

      如果当前资源没有被二次修改,服务器返回304告知客户端直接复用本地缓存

    • ETag

      ETag是对资源文件的一种摘要,可以通过ETag值来判断文件是否有修改。

      当客户端第一次请求某资源时,服务器返回:ETag: “5694c7ef-24dc”

      客户端再次请求时,可在头部加上如下域:If-None-Match: “5694c7ef-24dc”

      如果文件并未改变,则服务器返回304告知客户端可以复用本地缓存

  • no-cache / no-store

    不使用缓存

  • only-if-chched

    只使用缓存

Cache源码分析

OkHttp的缓存工作都是在CacheInterceptor中完成的,Cache部分有如下几个关键类:

  • Cache:Cache管理器,其内部包含一个DiskLruCache将cache写入文件系统

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public final class Cache implements Closeable, Flushable {
    ...
    final DiskLruCache cache;
    ...
    // Cache内部通过以下三个统计指标来优化缓存效率
    private int networkCount;
    private int hitCount;
    private int requestCount;
    }
  • CacheStrategy:缓存策略,其内部维护一个request和response,通过指定request和response来描述是通过网络还是缓存获取response,抑或二者同时使用

    1
    2
    3
    4
    5
    public final class CacheStrategy {
    public final @Nullable Request networkRequest;
    public final @Nullable Response cacheResponse;
    ...
    }
  • CacheStrategy.Factory:缓存策略工厂类,根据实际请求返回对应的缓存策略

    1
    2
    3
    4
    5
    6
    public static class Factory {
    final long nowMillis;
    final Request request;
    final Response cacheResponse;
    ...
    }

既然实际的缓存工作都是在CacheInterceptor中完成的,那么接下来看下cacheInterceptor的核心方法intercept方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@Override
public Response intercept(Chain chain) throws IOException {
// 首先尝试获取缓存
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
// 获取缓存策略
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
// 如果有缓存,更新下相关统计指标:命中率
if (cache != null) {
cache.trackResponse(strategy);
}
// 如果当前缓存不符合要求,将其close
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body());
}
// 如果不能使用网络,同时又没有符合条件的缓存,直接抛504错误
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 (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
// 尝试通过网络获取回复
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// 如果既有缓存,同时又发起了请求,说明此时是一个Conditional Get请求
if (cacheResponse != null) {
// 如果服务端返回的是NOT_MODIFIED,缓存有效,将本地缓存和网络响应合并
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();
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
// 如果响应资源有更新,关掉原有资源
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)) {
// 将网络响应写入cache中
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
}
}
}
return response;
}

接下来看一下缓存策略是如何生成的,相关代码实现在CacheStrategy.Factory.get()方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
return new CacheStrategy(null, null);
}
return candidate;
}
private CacheStrategy getCandidate() {
// 若本地没有缓存,发起网络请求
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// 如果当前请求是HTTPS,而缓存没有TSL握手,重新发起网络请求
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
// 如果当前的缓存策略是不缓存或者是conditional get,发起网络请求
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
// 缓存age
long ageMillis = cacheResponseAge();
// 缓存保鲜时间
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());
}
//如果 age + min-fresh >= max-age && age + min-fresh < max-age + max-stale,则虽然缓存过期了, 但是缓存继续可以使用,只是在头部添加 110 警告码
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());
}
// 发起conditional get请求
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);
}
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);
}

可以看到其核心逻辑在getCandidate方法中,基本就是HTTP缓存策略的实现。

DiskLruCache

Cache内部通过DiskLruCache管理cache在文件系统层面的创建,读取,清理等等工作,接下来看下DiskLruCache的主要逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);
int redundantOpCount;
boolean hasJournalErrors;
boolean initialized;
boolean closed;
boolean mostRecentTrimFailed;
boolean mostRecentRebuildFailed;
private long nextSequenceNumber = 0;
private final Executor executor;
...
}
  • journalFile:DiskLruCache内部日志文件,对cache的每一次读写都对应一条日志记录,DiskLruCache通过分析日志文件和创建cache。日志文件格式如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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
    5行固定不变,分别为:常量:libcore.io.DiskLruCache;diskCache版本;应用程序版本;valueCount(后 文介绍),空行
    接下来每一行对应一个cache entry的一次状态记录,其格式为:[状态(DIRTY,CLEAN,READ,REMOVE),key,状态相关value(可选)]:
    - DIRTY:表明一个cache entry正在被创建或更新,每一个成功的DIRTY记录都应该对应一个CLEAN或REMOVE操 作。如果一个DIRTY缺少预期匹配的CLEAN/REMOVE,则对应entry操作失败,需要将其从lruEntries 中删除。
    - CLEAN:说明cache已经被成功操作,当前可以被正常读取。每一个CLEAN行还需要记录其每一个value的长度。
    - READ: 记录一次cache读取操作。
    - REMOVE:记录一次cache清除。

    日志文件的应用场景主要有四个:

    • DiskCacheLru初始化时通过读取日志创建cache容器:lruEntries。同时通过日志过滤操作不成功的cache项
    • 初始化完成后,为避免日志文件不断膨胀,对日志进行重建精简
    • 每当有cache操作时将其记录入日志文件中以备下次初始化时使用
    • 当冗余日志过多时,通过调用cleanUpRunnable线程重建日志
  • DiskLruCache.Entry:每一个DiskLruCache.Entry对应一个cache记录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    private final class Entry {
    final String key;
    final long[] lengths;
    final File[] cleanFiles;
    final File[] dirtyFiles;
    boolean readable;
    Editor currentEditor;
    long sequenceNumber;
    Entry(String key) {
    this.key = key;
    lengths = new long[valueCount];
    cleanFiles = new File[valueCount];
    dirtyFiles = new File[valueCount];
    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);
    }
    }
    void setLengths(String[] strings) throws IOException {
    if (strings.length != valueCount) {
    throw invalidLengths(strings);
    }
    try {
    for (int i = 0; i < strings.length; i++) {
    lengths[i] = Long.parseLong(strings[i]);
    }
    } catch (NumberFormatException e) {
    throw invalidLengths(strings);
    }
    }
    void writeLengths(BufferedSink writer) throws IOException {
    for (long length : lengths) {
    writer.writeByte(' ').writeDecimalLong(length);
    }
    }
    private IOException invalidLengths(String[] strings) throws IOException {
    throw new IOException("unexpected journal line: " + Arrays.toString(strings));
    }
    Snapshot snapshot() {
    if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();
    Source[] sources = new Source[valueCount];
    long[] lengths = this.lengths.clone();
    try {
    for (int i = 0; i < valueCount; i++) {
    sources[i] = fileSystem.source(cleanFiles[i]);
    }
    return new Snapshot(key, sequenceNumber, sources, lengths);
    } catch (FileNotFoundException e) {
    for (int i = 0; i < valueCount; i++) {
    if (sources[i] != null) {
    Util.closeQuietly(sources[i]);
    } else {
    break;
    }
    }
    try {
    removeEntry(this);
    } catch (IOException ignored) {
    }
    return null;
    }
    }
    }

    一个Entry主要由以下几部分构成:

    • key:每个cache都有一个key作为其标识符。当前cache的key为其对应URL的MD5字符串
    • cleanFiles / dirtyFiles:每一个Entry对应多个文件,其对应的文件数由DiskLruCache.valueCount指定。当前在Okttp中valueCount为2.即每个cache对应2个cleanFiles,2个DirtyFiles。其中第一个cleanFiles / dirtyFiles记录cache的meta数据(如URL,创建时间,SSL握手记录等等),第二个文件记录cache的真正内容。cleanFiles记录处于稳定状态的cache结果,dirtyFiles记录处于创建或更新状态的cache
    • currentEditor:entry编辑器,对entry的所有操作都是通过其编辑器完成,编辑器内部添加了同步锁
  • cleanRunnable:清理线程,用于重建精简日志

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    private final Runnable cleanupRunnable = new Runnable() {
    public void run() {
    synchronized (DiskLruCache.this) {
    if (!initialized | closed) {
    return;
    }
    try {
    trimToSize();
    } catch (IOException ignored) {
    mostRecentTrimFailed = true;
    }
    try {
    if (journalRebuildRequired()) {
    rebuildJournal();
    redundantOpCount = 0;
    }
    } catch (IOException e) {
    mostRecentRebuildFailed = true;
    journalWriter = Okio.buffer(Okio.blackhole());
    }
    }
    }
    };

    其触发条件在journalRebuildRequired()方法中:

    1
    2
    3
    4
    5
    boolean journalRebuildRequired() {
    final int redundantOpCompactThreshold = 2000;
    return redundantOpCount >= redundantOpCompactThreshold
    && redundantOpCount >= lruEntries.size();
    }

    当冗余日志超过日志文件本身的一般且总条数超过2000时执行。

  • SnapShot:cache快照,记录了特定cache在某一个特定时刻的内容。每次向DiskLruCache请求时返回的都是目标cache的一个快照,相关逻辑在DiskLruCache.get方法中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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++;
    journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
    if (journalRebuildRequired()) {
    executor.execute(cleanupRunnable);
    }
    return snapshot;
    }
  • LruEntries:管理cache entry的容器,其数据结构是LinkedHashMap。通过LinkedHashMap本身的实现逻辑达到cache的LRU替换。

  • FileSystem:使用okio对File的封装,简化了I / O操作。

  • DiskLruCache.edit:DiskLruCache可以看成是Cache在文件系统层的具体实现,所以其基本操作接口存在一一对应的关系。

    • Cache.get() ——> DiskLruCache.get()
    • Cache.put() ——> DiskLruCache.edit()
    • Cache.remove() ——> DiskLruCache.remove()
    • Cache.update() ——> DiskLruCache.edit()

    首先看一下Cache.put操作的逻辑:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    @Nullable
    CacheRequest put(Response response) {
    String requestMethod = response.request().method();
    if (HttpMethod.invalidatesCache(response.request().method())) {
    try {
    remove(response.request());
    } catch (IOException ignored) {
    }
    return null;
    }
    if (!requestMethod.equals("GET")) {
    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;
    }
    }

    可以看到核心逻辑代码如下:

    1
    editor = cache.edit(key(response.request().url()));

    相关代码在DiskLruCache.edit方法中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public @Nullable Editor edit(String key) throws IOException {
    return edit(key, ANY_SEQUENCE_NUMBER);
    }
    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;
    }
    if (entry != null && entry.currentEditor != null) {
    return null;
    }
    if (mostRecentTrimFailed || mostRecentRebuildFailed) {
    executor.execute(cleanupRunnable);
    return null;
    }
    journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
    journalWriter.flush();
    if (hasJournalErrors) {
    return null;
    }
    if (entry == null) {
    entry = new Entry(key);
    lruEntries.put(key, entry);
    }
    Editor editor = new Editor(entry);
    entry.currentEditor = editor;
    return editor;
    }

    edit方法返回对应CacheEntry的editor编辑器。接下来再来看下Cache.put()方法的entry.writeTo(editor)相关逻辑:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    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();
    }

    其主要逻辑就是将对应请求的meta数据写入对应CacheEntry的索引为ENTRY_METADATA(0)的dirtyfile中。

    最后再来看Cache.put()方法的返回值:return new CacheRequestImpl(editor)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    private final class CacheRequestImpl implements CacheRequest {
    private final DiskLruCache.Editor editor;
    private Sink cacheOut;
    private Sink body;
    boolean done;
    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;
    }
    }

    CacheRequestImpl实现CacheRequest接口,向外部类(主要是CacheInterceptor)透出,外部对象通过CacheRequestImpl更新或写入缓存数据。

    其中,close方法和abort方法会调用editor.commit和editor.abort;来更新日志,editor.commit还会将dirtyFile重置为cleanFile作为稳定可用的缓存,相关逻辑如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    public void commit() throws IOException {
    synchronized (DiskLruCache.this) {
    if (done) {
    throw new IllegalStateException();
    }
    if (entry.currentEditor == this) {
    completeEdit(this, true);
    }
    done = true;
    }
    }
    synchronized void completeEdit(Editor editor, boolean success) throws IOException {
    Entry entry = editor.entry;
    if (entry.currentEditor != editor) {
    throw new IllegalStateException();
    }
    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);
    long oldLength = entry.lengths[i];
    long newLength = fileSystem.size(clean);
    entry.lengths[i] = newLength;
    size = size - oldLength + newLength;
    }
    } else {
    fileSystem.delete(dirty);
    }
    }
    redundantOpCount++;
    entry.currentEditor = null;
    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);
    }
    }

小结

DiskLruCache主要有以下几个特点:

  • 通过LinkedHashMap实现LRU替换
  • 通过本地维护Cache操作日志保证Cache原子性与可用性,同时为防止日志过分膨胀定时执行日志精简
  • 每一个Cache项对应两个状态副本:DIRTY,CLEAN。CLEAN表示当前可用状态Cache,外部访问到cache快照均为CLEAN状态;DIRTY为更新态Cache。由于更新和创建都只操作DIRTY状态副本,实现了Cache的读写分离
  • 每一个Cache项有四个文件,两个状态(DIRTY,CLEAN),每个状态对应两个文件,一个文件存储Cache meta数据,一个文件存储Cache内容数据