前言
合理利用本地缓存可以有效地减少网络开销,减少响应延迟。
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写入文件系统
123456789public 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,抑或二者同时使用
12345public final class CacheStrategy {public final Request networkRequest;public final Response cacheResponse;...}CacheStrategy.Factory:缓存策略工厂类,根据实际请求返回对应的缓存策略
123456public static class Factory {final long nowMillis;final Request request;final Response cacheResponse;...}
既然实际的缓存工作都是在CacheInterceptor中完成的,那么接下来看下cacheInterceptor的核心方法intercept方法源码:
|
|
接下来看一下缓存策略是如何生成的,相关代码实现在CacheStrategy.Factory.get()方法中:
|
|
可以看到其核心逻辑在getCandidate方法中,基本就是HTTP缓存策略的实现。
DiskLruCache
Cache内部通过DiskLruCache管理cache在文件系统层面的创建,读取,清理等等工作,接下来看下DiskLruCache的主要逻辑。
|
|
journalFile:DiskLruCache内部日志文件,对cache的每一次读写都对应一条日志记录,DiskLruCache通过分析日志文件和创建cache。日志文件格式如下。
123456789101112131415161718192021libcore.io.DiskLruCache11002CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054DIRTY 335c4c6028171cfddfbaae1a9c313c52CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342REMOVE 335c4c6028171cfddfbaae1a9c313c52DIRTY 1ab96a171faeeee38496d8b330771a7aCLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234READ 335c4c6028171cfddfbaae1a9c313c52READ 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记录
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182private 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:清理线程,用于重建精简日志
12345678910111213141516171819202122232425private 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()方法中:
12345boolean journalRebuildRequired() {final int redundantOpCompactThreshold = 2000;return redundantOpCount >= redundantOpCompactThreshold&& redundantOpCount >= lruEntries.size();}当冗余日志超过日志文件本身的一般且总条数超过2000时执行。
SnapShot:cache快照,记录了特定cache在某一个特定时刻的内容。每次向DiskLruCache请求时返回的都是目标cache的一个快照,相关逻辑在DiskLruCache.get方法中。
12345678910111213141516171819public 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操作的逻辑:
12345678910111213141516171819202122232425262728293031323334CacheRequest 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;}}可以看到核心逻辑代码如下:
1editor = cache.edit(key(response.request().url()));相关代码在DiskLruCache.edit方法中:
1234567891011121314151617181920212223242526272829303132333435363738public 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)相关逻辑:
123456789101112131415161718192021222324252627282930313233343536373839404142434445public 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)。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546private 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) {public void close() throws IOException {synchronized (Cache.this) {if (done) {return;}done = true;writeSuccessCount++;}super.close();editor.commit();}};}public void abort() {synchronized (Cache.this) {if (done) {return;}done = true;writeAbortCount++;}Util.closeQuietly(cacheOut);try {editor.abort();} catch (IOException ignored) {}}public Sink body() {return body;}}CacheRequestImpl实现CacheRequest接口,向外部类(主要是CacheInterceptor)透出,外部对象通过CacheRequestImpl更新或写入缓存数据。
其中,close方法和abort方法会调用editor.commit和editor.abort;来更新日志,editor.commit还会将dirtyFile重置为cleanFile作为稳定可用的缓存,相关逻辑如下。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970public 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内容数据