從快取檔案的角度幫你理解 Okhttp3 快取原理
@[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.0key.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.0key.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 實現快取功能的大致流程基本結束,但是其中還是有很多的邏輯和細節是自己沒有發現和不能理解的,其原始碼還是需要不斷的去閱讀去理解,需要對其中的實現、思想有進一步的體會。