@[toc]
本文以一個不同的角度來解讀 Okhttp3 實現緩存功能的思路,即:對於對於的緩存空間(文件夾)中的緩存文件的生成時機、不同時期下個文件的狀態、不同時期下日誌文件讀寫。通過這些方法來真正理解 Okhttp3 的緩存功能。如果你理解 DiskLrcCache 開源庫的設計,那麼對於 Okhttp3 的緩存實現你就已經掌握了,因爲前者以後者爲基礎,你甚至沒有看本文的必要。
1. 需要了解的概念
緩存功能的實現,理所當然的涉及文件的讀寫操作、緩存機制方案的設計。Okhttp3 緩存功能的實現涉及到 Okio 和 DiskLruCache,在闡述具體緩存流程之前,我們需要了解兩者的一些基本概念。
1.2 Okio
Okio 中有兩個關鍵的接口: Sink 和 Source ,對比 Java 中 I/O 流概念,我們可以把 Sink 看作 OutputStream , 把 Source 看作 InputStream 。
類的結構圖如下:
[圖片上傳失敗...(image-fb6118-1548689150138)]
其具體實現非本文重點,有興趣自己可以查看源碼。
1.1 DiskLruCache
Okhttp3 中 DiskLruCache 與JakeWharton 大神的 DiskLruCache 指導思想一致,但是具體細節不同,比如前者使用 Okio 進行 IO 操作,更加高效。
在 DiskLruCache 有幾個重要概念,瞭解它們,才能對 DiskLruCache 的實現原理有基本的認識。
爲了能夠表達的更加直觀,我們看一下一張圖片進行緩存時緩存文件的具體內容:
1.2.1 日誌文件 journal
該文件爲 DiskLruCache 內部的日誌文件,對 cache 的每一次操作都對應一條日誌,並寫入到 journal 文件中,同時也可以通過 journal 文件的分析創建 cache。
打開上圖中 journal 文件,具體內容爲:
libcore.io.DiskLruCache
1
201105
2
DIRTY 0e39614b6f9e1f83c82cf663e453a9d7
CLEAN 0e39614b6f9e1f83c82cf663e453a9d7 4687 14596
在 DiskLruCache.java 類中,我們可以看到對 journal 文件內容的描述,在這裏自己對其簡單翻譯,有興趣的朋友可以看 JakeWharton 的描述: DiskLruCache。
文件的前五行構成頭部,格式一般固定。
第一行: 常量 -- libcore.io.DiskLruCache ;
第二行: 硬盤緩存版本號 -- 1
第三行: 應用版本號 -- 201105
第四行: 一個有意義的值 -- 2
第五行: 空白行
頭部後的每一行都是 Cache 中 Entry 狀態的一條記錄。
每條記錄的信息包括: 狀態值(DIRTY CLEAN READ REMOVE) 緩存信息entry的key值 狀態相關的值(可選)。
下面對記錄的狀態進行說明:
DIRTY: 該狀態表明一個 entry 正在被創建或更新。每一個成功的 DIRTY 操作記錄後應該 CLEAN 或 REMOVE 操作記錄,否則被臨時創建的文件會被刪除。
CLEAN: 該狀態表明一個 entry 已經被成功的創建,並且可以被讀取,後面記錄了對應兩個文件文件(具體哪兩個文件後面會談到)的字節數。
READ: 該狀態表明正在跟蹤 LRU 的訪問。
REMOVE: 該狀態表明entry被刪除了。
需要注意的是在這裏 DIRTY 並不是 “髒”、“髒數據” 的意思,而是這個數據的狀態不爲最終態、穩定態,該文件現在正在被操作,
而 CLEAN 並不是數據被清除,而是表示該文件的操作已經完成。同時在後續的 dirtyFiles 和 cleanFiles 也表示此含義。
關於日誌文件在整個緩存系統中的作用,在後續過程中用到它的時候在具體闡述。
1.2.2 DiskLruCache.Entry
每個 DiskLruCache.Entry 對象代表對每一個 URl 在緩存中的操作對象,該類成員變量的具體含義如下:
private final class Entry {
final String key; // Entry 的 key
final long[] lengths; // key.0 key.1 文件字節數的數組
final File[] cleanFiles; // 穩定的文件數組
final File[] dirtyFiles;// 正在執行操作的文件數組
boolean readable;// 如果該條目被提交了,爲 true
Editor currentEditor;// 正在執行的編輯對象,在沒有編輯時爲 null
long sequenceNumber;// 編輯條目的最近提交的序列號
...
...
}
具體操作在緩存實現流程中闡述。
1.2.3 DiskLruCache.SnapShot
此類爲緩存的快照,爲緩存空間中特定時刻的緩存的狀態、內容,該類成員變量的具體含義:
public final class Snapshot implements Closeable {
private final String key;
private final long sequenceNumber; // 編輯條目的最近提交的序列號
private final Source[] sources;// 緩存中 key.0 key.1 文件的 Okio 輸入流
private final long[] lengths;// 對應 Entry 中的 lengths,爲文件字節大小
...
...
}
1.2.3 DiskLruCache.Editor
該類爲 DiskLruCache 的編輯器,顧名思義該類是對 DiskLruCache 執行的一系列操作,如:abort() 、 commit() 等。
Entry publish 的含義是什麼?????
2. 緩存實現的有關流程
簡單介紹了幾個概念,在這一節具體查看一下緩存實現的具體流程。在這之前我們需要明確一下幾個前提:
由多個攔截器構成的攔截器鏈是 Okhttp3 網絡請求的執行關鍵,可以說整個網絡請求能夠正確的執行是有整個鏈驅動的 (責任鏈模式)。仿照 RxJava 是事件驅動的,那麼 Okhttp3 是攔截器驅動的。
關於緩存功能實現的攔截器爲 CacheInterceptor, CacheInterceptor 位於攔截器鏈中間位置,那麼以執行下一個攔截器爲界將緩存流程分爲兩部分:
- 觸發之後攔截器之前的操作
- 觸發之後攔截器之後的操作
即以 networkResponse = chain.proceed(networkRequest);
爲分界
1. 觸發之後攔截器之前的操作
Response cacheCandidate = cache != null
? cache.get(chain.request())// 執行 DiskLruCache#initialize()
: 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);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
//緩存和網絡皆爲空,返回code 爲504 的響應
// 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();
}
// If we don't need the network, we're done. 緩存策略請求爲null,則使用緩存
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
1.1 日誌文件的初始化
當執行如下代碼時會按照調用鏈執行相關邏輯:
Response cacheCandidate = cache != null
? cache.get(chain.request())// 執行 DiskLruCache#initialize()
: null;//本地緩存
首先檢查在緩存中是否存在該 request 對應的緩存數據,如果有的話就返回 Response,如果沒有就置 null。
調用鏈來到以下方法:
@Nullable
Response get(Request request) {
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
snapshot = cache.get(key);// 在這裏會執行
...
return response;
}
在 snapshot = cache.get(key);
處執行相應的初始化操作。
在此過程中執行一個特別重要的操作,需要對緩存中的 journal 系列日誌文件(包括 journal journal.bak) 進行新建、重建、讀取等操作,具體查看源碼:
// DiskLruCache#initialize()
public synchronized void initialize() throws IOException {
assert Thread.holdsLock(this);
if (initialized) {// 代碼 1
return; // Already initialized.
}
// If a bkp file exists, use it instead. journal文件備份是否存在
if (fileSystem.exists(journalFileBackup)) {// 代碼 2
// 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();// 代碼 3
processJournal(); // 代碼 4
initialized = true; // 代碼 5
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();// 代碼 6
initialized = true;// 代碼 7
}
1. App 啓動後的初始化
在啓動 App 是標誌位 initialized = false
,那麼由 代碼 1
可知此時需要執行初始化操作。
if (initialized) {// 代碼 1
return; // Already initialized.
}
1.1 若 journal 日誌文件存在
如果存在 journal.bak 那麼將該文件重命名爲 journal。
接下來對 journal 日誌文件所做的操作如 代碼 3、4 、5
所示,具體作用做如下闡述。代碼 3
要做的是讀取日誌文件 journal 並根據日誌內容初始化 LinkedHashMap<String, Entry> lruEntries
中的元素,DiskLruCache 正是通過 LinkedHashMap 來實現 LRU 功能的。我們看一下 readJournal() 的具體代碼:
private void readJournal() throws IOException {
BufferedSource source = Okio.buffer(fileSystem.source(journalFile));
try {
String magic = source.readUtf8LineStrict();
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) {// 不斷執行如下操作,直到文件尾部,結束如下操作
try {
readJournalLine(source.readUtf8LineStrict());
lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
redundantOpCount = lineCount - lruEntries.size();
// If we ended on a truncated line, rebuild the journal before appending to it.
if (!source.exhausted()) {
rebuildJournal();
} else {
journalWriter = newJournalWriter();
}
} finally {
Util.closeQuietly(source);
}
}
在方法的開始讀取 journal 日誌文件的頭部做基本的判斷,如不滿足要求則拋出異常。接下來在 該方法中通過方法 -- readJournalLine(source.readUtf8LineStrict());
讀取 journal 日誌文件的每一行,根據日誌文件的每一行生成 Entry 存入 lruEntries 中用來實現 LRU 功能。
private void readJournalLine(String line) throws IOException {
...
...
// 一頓操作得到 key 的值
// 根據日誌文件中 key 值獲得或者生成 Entry,存入 lruEntries 中
Entry entry = lruEntries.get(key);
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
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)) {
entry.currentEditor = new Editor(entry);
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
// This work was already done by calling lruEntries.get().
} else {
throw new IOException("unexpected journal line: " + line);
}
}
readJournal() 執行完畢後相當於對 lruEntries 進行初始化。lruEntries 元素的個數等於該 App 在此緩存文件夾下緩存文件的個數。在此過程中如果 lruEntries 中沒有此行日誌中的 key 對應的 Entry 對象,因爲現在爲進入 App 中的對緩存空間的初始化,所以都需要新建該類的對象:
// 根據日誌文件中 key 值獲得或者生成 Entry,存入 lruEntries 中
Entry entry = lruEntries.get(key);
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
新建 Entry 對象的過程對於整個緩存體系的構建也十分重要,代碼如下:
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()); // key.0 key.1
fileBuilder.append(".tmp");
dirtyFiles[i] = new File(directory, fileBuilder.toString());// key.0.tmp key.1.tmp
fileBuilder.setLength(truncateTo);
}
}
新建對象過程中會根據 valueCount = 2; 的值定義緩存文件分別爲 key.0、key.1、key.0.tmp、key.1.tmp ,其中 key.0 爲穩定狀態下的請求的 mate 數據,key.1 爲穩定狀態下的緩存數據,而 key.0.tmp、key.1.tmp 分別爲 mate 數據和緩存數據的臨時文件,此時並不會真正的新建文件。
在這裏需要明確的是 cleanFiles 和 dirtyFiles 都是 Entry 的成員變量,也就是說是通過 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];
}
} else {
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
fileSystem.delete(entry.cleanFiles[t]);
fileSystem.delete(entry.dirtyFiles[t]);
}
i.remove();
}
}
}
何爲無用的文件 ?
如果文件夾下存在
entry.currentEditor != null;
的文件,說明此文件爲處在編輯狀態下,但是此時的時機爲剛打開 App 後的初始化狀態,所有的文件均不應該處在編輯狀態,所以此狀態下的文件即爲無用的文件,需要被刪除。
執行完畢後標誌位 initialized 置位爲 true 並中斷執行 (return;) 返回操作去執行其他操作。
1.2 若 journal 日誌文件不存在
若 journal 日誌文件不存在,那麼不會執行 代碼 2、3、4、5 直接執行代碼 6 -- rebuildJournal() ,具體執行操作如下:
synchronized void rebuildJournal() throws IOException {
if (journalWriter != null) {
journalWriter.close();
}
//產生 journal.tmp 文件
BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp));
try {// 寫入 journal 文件內容
writer.writeUtf8(MAGIC).writeByte('\n');
writer.writeUtf8(VERSION_1).writeByte('\n');
writer.writeDecimalLong(appVersion).writeByte('\n');
writer.writeDecimalLong(valueCount).writeByte('\n');
writer.writeByte('\n');
/**
* 將 lruEntries 的值重新寫入 journal 文件
*/
for (Entry entry : lruEntries.values()) {
if (entry.currentEditor != null) { // 當前的 editor 不爲 null 說明當前 journal 爲非穩定態
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();
}
// journal.tmp --> journal
if (fileSystem.exists(journalFile)) {
fileSystem.rename(journalFile, journalFileBackup);
}
fileSystem.rename(journalFileTmp, journalFile);
fileSystem.delete(journalFileBackup);
journalWriter = newJournalWriter();
hasJournalErrors = false;
mostRecentRebuildFailed = false;
}
十分重要的操作爲 : Okio.buffer(fileSystem.sink(journalFileTmp)); ,因爲此時 journal 不存在,那麼此行代碼執行的操作正是新建journal 臨時文件 -- journal.tmp ,寫入文件頭部文件後將 journal.tmp 重命名爲 journal 。前文解析 journal 文件內容的含義,此處代碼正好可以作爲印證。
1.2 初始化後
經過初始化後最終獲取 DiskLruCache 快照 DiskLruCache$Snapshot 對象,並進行相關包裝返回 Response 對象爲緩存中的Response 對象。
@Nullable
Response get(Request request) {
...
...
try {
snapshot = cache.get(key);// 在這裏會執行 initialize(),進行一次初始化
if (snapshot == null) {
return null;
}
...
...
Response response = entry.response(snapshot);
...
...
return response;
}
至此,以上即爲進入 CacheInterceptor 後的第一步操作,說實話工作量真是大,開啓了 Debug 模式 n 遍才稍微把基本流程搞明白。
Response cacheCandidate = cache != null
? cache.get(chain.request())// 執行 DiskLruCache#initialize() ,會對 journal 文件進行一些操作
: null;//本地緩存
1.3 緩存策略
緩存策略的獲取主要涉及代碼如下:
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
具體執行代碼位置:
CacheStrategy#getCandidate()
,由於具體業務邏輯比較容易理解,根據緩存響應、請求中頭部關於緩存的字段進行相關判斷,得出緩存策略,在這裏不做過多闡釋。
2. 觸發之後攔截器之後的操作
觸發之後的攔截器後,進行相關的一系列操作,根據責任鏈模式邏輯還是會最終回來,接着此攔截器的邏輯繼續執行。此時整個請求的狀態爲已經成功得到網絡響應,那麼我們要做的就是對網絡響應進行緩存,具體代碼如下:
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);// 將 response 寫入內存中,此時進行的步驟: 創建 0.tmp(已經寫入數據) 和 1.tmp(尚未寫入數據)
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
跟隨 CacheRequest cacheRequest = cache.put(response); 執行如下邏輯:
CacheRequest put(Response response) {
...
...
//由Response對象構建一個Entry對象,Entry是Cache的一個內部類
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;// disk 緩存的編輯
try {
editor = cache.edit(key(response.request().url()));// key(response.request().url()) 根據 URL生成唯一 key
if (editor == null) {
return null;
}
//把這個entry寫入
//方法內部是通過Okio.buffer(editor.newSink(ENTRY_METADATA));獲取到一個BufferedSink對象,隨後將Entry中存儲的Http報頭數據寫入到sink流中。
entry.writeTo(editor);// 觸發生成 0.tmp
//構建一個CacheRequestImpl對象,構造器中通過editor.newSink(ENTRY_BODY)方法獲得Sink對象
return new CacheRequestImpl(editor);// 觸發生成 1.tmp
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
Cache#writeTo()
// 寫入 0.tmp 數據 // 寫入 的dirtyfile 文件的 buffersink 輸出流
public void writeTo(DiskLruCache.Editor editor) throws IOException {
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));//新建 key.0.tmp
// TODO: 在這裏出現了 0.tmp
sink.writeUtf8(url)
.writeByte('\n');
....
}
非常明顯的操作在此處創建了 key.0.tmp 文件,並寫入數據,此處寫入的數據爲 mate 數據
CacheRequestImpl(final DiskLruCache.Editor editor) {
this.editor = editor;
this.cacheOut = editor.newSink(ENTRY_BODY);// 在這裏生成 1.tmp
this.body = new ForwardingSink(cacheOut) {
@Override
public void close() throws IOException {
synchronized (Cache.this) {
if (done) {
return;
}
done = true;
writeSuccessCount++;
}
super.close();
editor.commit();//最終調用了此函數,0.tmp 1.tmp --》 key.0 key.1
}
};
}
在初始化 CacheRequestImpl 對象時創建了 key.1.tmp 文件。
執行如上操作後回到 CacheInterceptor 執行 cacheWritingResponse() 方法:
private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
throws IOException {
// Some apps return a null body; for compatibility we treat that like a null cache request.
if (cacheRequest == null) return response;
Sink cacheBodyUnbuffered = cacheRequest.body();
if (cacheBodyUnbuffered == null) return response;
final BufferedSource source = response.body().source();
final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);
Source cacheWritingSource = new Source() {
boolean cacheRequestClosed;
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead;
try {
bytesRead = source.read(sink, byteCount);
} catch (IOException e) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
cacheRequest.abort(); // Failed to write a complete cache response.
}
throw e;
}
if (bytesRead == -1) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
cacheBody.close(); // The cache response is complete!
}
return -1;
}
sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
cacheBody.emitCompleteSegments();
return bytesRead;
}
@Override
public Timeout timeout() {
return source.timeout();
}
@Override
public void close() throws IOException {
if (!cacheRequestClosed
&& !discard(this, HttpCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
cacheRequestClosed = true;
cacheRequest.abort();
}
source.close();
}
};
return response.newBuilder()
.body(new RealResponseBody(response.headers(), Okio.buffer(cacheWritingSource)))
.build();
執行一系列操作,使用 Okio 這個庫不斷的向 key.1.tmp 寫入數據,具體操作過程實在是太過繁雜,而且牽涉到 Okio 庫原理,自己在這麼短時間無法理清具體流程。
對於數據寫入的切入點自己還沒有很好的認識,在何處真正進行寫文件操作自己只能夠通過 Debug 知道其走向,但是對其原理還沒有理解。
最後會執行 CacheRequestImpl 對象的close 方法,
CacheRequestImpl(final DiskLruCache.Editor editor) {
this.editor = editor;
this.cacheOut = editor.newSink(ENTRY_BODY);//在這裏生成 1.tmp
this.body = new ForwardingSink(cacheOut) {
@Override
public void close() throws IOException {
synchronized (Cache.this) {
if (done) {
return;
}
done = true;
writeSuccessCount++;
}
super.close();
editor.commit();// 最終調用了此函數,0.tmp 1.tmp -> key.0 key.1
}
};
}
執行 editor.commit(); 該方法會調用的 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;
}
}
}
// key.0.tmp key.1.tmp --> key.0 key.1
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);
}
}
....
}
該方法中最終會將 key.0.tmp 、key.1.tmp 分別 重命名爲 key.0 、key.1 ,這兩個文件分別爲兩個文件的穩定狀態,同時更新 journal 日誌記錄。
至此 Okhttp3 實現緩存功能的大致流程基本結束,但是其中還是有很多的邏輯和細節是自己沒有發現和不能理解的,其源碼還是需要不斷的去閱讀去理解,需要對其中的實現、思想有進一步的體會。