public static DiskLruCache create(FileSystem fileSystem, File directory, int appVersion,
int valueCount, long maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
// Use a single background thread to evict entries.
Executor executor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true));
return new DiskLruCache(fileSystem, directory, appVersion, valueCount, maxSize, executor);
}
DiskLruCache(FileSystem fileSystem, File directory, int appVersion, int valueCount, long maxSize,
Executor executor) {
this.fileSystem = fileSystem;
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE);
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
this.valueCount = valueCount;
this.maxSize = maxSize;
this.executor = executor;
}
create方法是一个静态方法,创建了一个线程池,最大线程个数为1,60s空闲该线程就会被销毁。阻塞队列是LinkedBlockQueue。
在DiskLruCache中,这个线程池只会被用来执行cleanupRunnable。cleanUpRunnable用于在某些时机(如get,edit等方法)执行trimToSize或rebuildJournal方法。
DiskLruCache被创建的时候,就创建了3个文件,journalFile,journalFileTmp,journalFIleBackup。
edit
// DiskLruCache
public @Nullable Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);// -1
}
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; // Another edit is in progress.
}
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;
}
// Flush the journal before creating files to prevent file leaks.
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;
}
initialize
public synchronized void initialize() throws IOException {
assert Thread.holdsLock(this);
if (initialized) {
return; // Already initialized.
}
// If a bkp file exists, use it instead.
if (fileSystem.exists(journalFileBackup)) {
// If journal file also exists just delete backup file.
if (fileSystem.exists(journalFile)) {
fileSystem.delete(journalFileBackup);
} else {
fileSystem.rename(journalFileBackup, journalFile);
}
}
// Prefer to pick up where we left off.
if (fileSystem.exists(journalFile)) {
try {
readJournal();//
processJournal();
initialized = true;
return;
} catch (IOException journalIsCorrupt) {
Platform.get().log(WARN, "DiskLruCache " + directory + " is corrupt: "
+ journalIsCorrupt.getMessage() + ", removing", journalIsCorrupt);
}
// The cache is corrupted, attempt to delete the contents of the directory. This can throw and
// we'll let that propagate out as it likely means there is a severe filesystem problem.
try {
delete();
} finally {
closed = false;
}
}
rebuildJournal();
initialized = true;
}
initialize干的事情:
- 如果存在journalFileBackup,且没有journalFile,就将journalFileBackup重命名成journalFile,否则将journalFileBackup删除。--其实就是journalFileBackup就是个备份。
- 如果有journalFile,就执行readJournal和processJournal方法。细节下面再说,大概就是将文件中的内容转换成lruEntries。
- 如果没有journalFile,删除directory下面的所有文件(其实就是清空缓存),再执行rebuildJournal(将lruEntries转变成文件存储)。
总结一下,如果有journalFile,就读取内容到lruEntries,如果没有,就将lruEntries的内容硬盘化,存储到文件。
那么下面要分析的主要就是3个方法:
- readJournal
- processJournal
- rebuildJournal
readJournal
private void readJournal() throws IOException {
BufferedSource source = Okio.buffer(fileSystem.source(journalFile));// 读取journalFile
try {
String magic = source.readUtf8LineStrict();// journalFile的格式校验
String version = source.readUtf8LineStrict();
String appVersionString = source.readUtf8LineStrict();
String valueCountString = source.readUtf8LineStrict();
String blank = source.readUtf8LineStrict();
if (!MAGIC.equals(magic)
|| !VERSION_1.equals(version)
|| !Integer.toString(appVersion).equals(appVersionString)
|| !Integer.toString(valueCount).equals(valueCountString)
|| !"".equals(blank)) {
throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
+ valueCountString + ", " + blank + "]");
}
int lineCount = 0;
while (true) {// 逐行读取journalFile
try {
readJournalLine(source.readUtf8LineStrict());
lineCount++;
} catch (EOFException endOfJournal) {// 读完之后直接抛异常,结束while循环
break;
}
}
redundantOpCount = lineCount - lruEntries.size();
// If we ended on a truncated line, rebuild the journal before appending to it.
if (!source.exhausted()) {// 如果都读完了,发现source还有内容,这说明文件出错了。rebuildJournal
rebuildJournal();
} else {
journalWriter = newJournalWriter();// 否则创建journalWriter
}
} finally {
Util.closeQuietly(source);
}
}
readJournal
- 首先读取了journalFile,读取File前面的meta-data做了格式校验。
- 然后逐行读取,调用readJournalLine。
- 如果读完之后,发现source还没读取完(文件很可能已经损坏了),这时候重建journal文件。否则,给journalWriter重新赋值。
readJournalLine
private void readJournalLine(String line) throws IOException {
int firstSpace = line.indexOf(' ');
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
int keyBegin = firstSpace + 1;
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
if (secondSpace == -1) {
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
lruEntries.remove(key);
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}// 前面这一大段,都是为了获取这一行中的key
Entry entry = lruEntries.get(key);// 如果当前的lruEntries中,没有这个key
if (entry == null) {
entry = new Entry(key);// 就新创建一个这个Key对应的Entry。这里的Entry存储了这个key对应的cleanFiles和dirtyFiles
lruEntries.put(key, entry);
}
// CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
// clean一行的格式,中间的是key,后面的2个数字会存入entry的lengths
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {// 如果这一行是clean开头,说明这个Entry处于可读状态
String[] parts = line.substring(secondSpace + 1).split(" ");
entry.readable = true;
entry.currentEditor = null;
entry.setLengths(parts);
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {// dirty代表这个entry正在被创建/更新,dirty后面应该跟clean/remove
entry.currentEditor = new Editor(entry);
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {// read代表entry正在
// This work was already done by calling lruEntries.get().
} else {
throw new IOException("unexpected journal line: " + line);
}
}
readJournalLine,根据journalFile中每一行,取出key,如果lruEntries中没有这个key,就会新建一个Entry(Entry实际是Request的缓存。里面维护了两个文件,一个文件存储了Request和Response的header,另一个文件存储了Response的body)。
取出一行的标志:
- CLEAN 设置entry为readable,并且给entry设置lengths。
- DIRTY 为entry设置Editor(为了给后面processJournal清空Entry中的文件)。
processJournal
private void processJournal() throws IOException {
fileSystem.delete(journalFileTmp);
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
Entry entry = i.next();
if (entry.currentEditor == null) {
for (int t = 0; t < valueCount; t++) {
size += entry.lengths[t];// 统计所有文件的size
}
} else {
entry.currentEditor = null;// entry.currentEditor != null,代表这是一行dirty 中的key代表的entry,删除entry中的对应文件
for (int t = 0; t < valueCount; t++) {
fileSystem.delete(entry.cleanFiles[t]);
fileSystem.delete(entry.dirtyFiles[t]);
}
i.remove();
}
}
}
这个方法主要统计所有entry的size(内存中不可能维护所有Request的Response,为了控制内存大小,DiskLruCache会有一个maxSize的限制,一旦entry的size超过maxSize,就开始移除entry,这也是这个类为什么叫LruCache的关系)。
除了统计size,还会移除所有dirty entry对应的文件。一般dirty行后会跟clean / remove行。clean会将entry的currentEditor置为空。而remove行会将entry移除。
所以,如果到processJournal方法中,entry的currentEditor != null,这说明,很可能这个entry还没有edit完成(请求还没有结束),所以这个entry对应的文件是无效的,所以要将entry的文件全都清除,并将entry从lruEntries中移除。
rebuildJournal
synchronized void rebuildJournal() throws IOException {
if (journalWriter != null) {// 走到rebuildJournal一般代表journalFile中的文件已经被损坏(有可能只写入一部分,文件就意外关闭了)
journalWriter.close();
}
// 往journalFileTmp写入lruEntries
BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp));
try {
writer.writeUtf8(MAGIC).writeByte('\n');
writer.writeUtf8(VERSION_1).writeByte('\n');
writer.writeDecimalLong(appVersion).writeByte('\n');
writer.writeDecimalLong(valueCount).writeByte('\n');
writer.writeByte('\n');
for (Entry entry : lruEntries.values()) {
if (entry.currentEditor != null) {// dirty的标志就是currentEditor != null
writer.writeUtf8(DIRTY).writeByte(' ');
writer.writeUtf8(entry.key);
writer.writeByte('\n');
} else {
writer.writeUtf8(CLEAN).writeByte(' ');
writer.writeUtf8(entry.key);
entry.writeLengths(writer);
writer.writeByte('\n');
}
}
} finally {
writer.close();
}
if (fileSystem.exists(journalFile)) {
fileSystem.rename(journalFile, journalFileBackup);
}
fileSystem.rename(journalFileTmp, journalFile);
fileSystem.delete(journalFileBackup);
journalWriter = newJournalWriter();// 对应journalFile的sink
hasJournalErrors = false;
mostRecentRebuildFailed = false;
}
相比readJournal和processJournal是读取journalFile的内容到lruEntries的过程,rebuildJournal就是将lruEntries写到journalFile的过程。
这三个方法写了一长串,但实际上都是initialize干的活。
回顾下之前总结的initialize的细节:
如果有journalFile,就将journalFile转换成lruEntries。如果这一行的标志是 DIRTY,就将key对应的Entry的currentEditor设置成不为空。如果是CLEAN,就将Entry设置成readable=true。
如果没有journalFile,就遍历lruEntries,将key写入文件,如果key对应的Entry的currentEditor != null,标志成DIRTY行,否则标志成clean行。
再回看edit方法
synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {// 返回一个Editor
initialize();// journalFile与lruEntries的相互转换,只会执行一遍
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
/**中间省略**/
// Flush the journal before creating files to prevent file leaks.
journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');// edit方法写入一个DIRTY行
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;// 给key对应的Entry设置Editor
return editor;
}
edit方法,最终,是往journalFile写入一个DIRTY行,并为key对应的Entry创建了一个Editor。
Editor实际是对Entry中的两个文件进行读写的。(dirtyFile和cleanFile)
看下Editor的结构:
newSink对应写入,newSource对应读取,commit方法用于将dirtyFiles重命名为cleanFiles。
newSink和newSource方法都有一个int参数。
看引用方式,可以知道Entry的cleanFiles和dirtyFiles之所以是一个数组,并且values为2,是因为一个File会存储META_DATA(包含的具体信息后面会讲),一个File存储Response 的body。
所以这样就可以清楚Entry的具体存储内容:
- key对应request的url。
- dirtyFiles和cleanFiles都是一个长度为2的File[]数组,ENTRY_METADATA索引对应的File存储的是Request的信息。ENTRY_BODY存储的是Response。
实例
上面介绍了DiskLruCache的部分代码,主要包括lruEntries与journalFile的相互转换。最后看出Entry的cleanFile实际是存储Request和Response信息的载体。这实际就是缓存了。对应DiskLruCache总的DiskCache(硬盘缓存)部分。而Lru部分(内存策略)后面再说。
现在先介绍,CacheInterceptor是怎么利用DiskLruCache的。
读取DiskLruCache中存储的Response
cache.get
// Cache
@Nullable Response get(Request request) {
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
snapshot = cache.get(key);// 获取key对应的Entry的SnapShot
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
try {
entry = new Entry(snapshot.getSource(ENTRY_METADATA));// 传入ENTRY_METADATA对应的文件Source
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
Response response = entry.response(snapshot);// 将文件转成Response
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
// DiskLruCache
public synchronized Snapshot get(String key) throws IOException {
initialize();
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null || !entry.readable) return null;
// 跟原来的entry的值几乎一样,只是file[]变成了source[],方便读取文件
Snapshot snapshot = entry.snapshot();
if (snapshot == null) return null;
/****/
return snapshot;
}
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]);// 获取cleanFile对应的Source
}
return new Snapshot(key, sequenceNumber, sources, lengths);
} catch (FileNotFoundException e) {
/****/
}
}
总结下:
cache.get方法(传入Request,获取Request对应的Response)
通过调用DiskLruCache.get方法,从lruEntries通过key(request.url),获取到对应的Entry(使用文件存储了META_DATA和Response的body)的snapShot。
然后读取snapshot[ENTRY_METADATA]对应的文件内容,构建Response的Header。
读取snapshot[ENTRY_BODY],构建Response的body。
META_DATA存储的内容包括:
url ,method,request的header(2个),response code,response的header(3个),加密算法,证书链,TLS版本号
具体构建如下:
// Cache.Entry
// 负责将文件转成Response
public Response response(DiskLruCache.Snapshot snapshot) {
String contentType = responseHeaders.get("Content-Type");
String contentLength = responseHeaders.get("Content-Length");
Request cacheRequest = new Request.Builder()
.url(url)
.method(requestMethod, null)
.headers(varyHeaders)
.build();
return new Response.Builder()
.request(cacheRequest)
.protocol(protocol)
.code(code)
.message(message)
.headers(responseHeaders)
.body(new CacheResponseBody(snapshot, contentType, contentLength))
.handshake(handshake)
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(receivedResponseMillis)
.build();
}
而在CacheResponseBody中,使用ENTRY_BODY读取了Response的body。
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();
}
});
}
DiskLruCache的LRU机制
LRU
Least-Recently-Used,最近最少使用算法,是一种内存数据淘汰策略。这里的内存数据,指的是DiskLruCache中的lruEntries。
// DiskLruCache
void trimToSize() throws IOException {
while (size > maxSize) {
Entry toEvict = lruEntries.values().iterator().next();
removeEntry(toEvict);
}
mostRecentTrimFailed = false;
}
DiskLruCache有一个trimToSize的方法。从命名就可以看出,这个方法是为了将lruEntries的大小缩减至size。
这里的size会在lruEntries有变动的时候,进行变化。比如之前在edit方法的时候,有个initialize方法。这个方法在将journal文件读到lruEntries的时候,就通过Entry(DiskLruCache)的lengths数组统计过size。
并在每一次Editor的commit方法中(有改动才会调用commit方法),重新计算size。
因此,这个size实际就是lruEntries维护的所有Entry的文件大小。
而trimToSize也很简单,只是当size > maxSize的时候,就移除lruEntries中的Entry。
lruEntries是一个LinkedHashMap,可以记录Entry的插入顺序。
调移除的时候,调用values().interator()会从最先插入的Entry开始移除。
但其实这里并没有完全符合Lru。Lru即使是最先插入,但是如果调用频率很高的话,还是不会被最新移除的。
但是考虑到http请求的实际情况,很少会出现,老早请求的Request,还会不断地请求。这样也许就情有可原了。