從緩存文件的角度幫你理解 Okhttp3 緩存原理

@[toc]

本文以一個不同的角度來解讀 Okhttp3 實現緩存功能的思路,即:對於對於的緩存空間(文件夾)中的緩存文件的生成時機、不同時期下個文件的狀態、不同時期下日誌文件讀寫。通過這些方法來真正理解 Okhttp3 的緩存功能。如果你理解 DiskLrcCache 開源庫的設計,那麼對於 Okhttp3 的緩存實現你就已經掌握了,因爲前者以後者爲基礎,你甚至沒有看本文的必要。

1. 需要了解的概念

緩存功能的實現,理所當然的涉及文件的讀寫操作、緩存機制方案的設計。Okhttp3 緩存功能的實現涉及到 Okio 和 DiskLruCache,在闡述具體緩存流程之前,我們需要了解兩者的一些基本概念。

1.2 Okio

Okio 中有兩個關鍵的接口: SinkSource ,對比 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. 緩存實現的有關流程

簡單介紹了幾個概念,在這一節具體查看一下緩存實現的具體流程。在這之前我們需要明確一下幾個前提:

  1. OkhttpClient 設置支持緩存。
  2. 網絡請求頭部中的字段設置爲支持緩存(Http 協議首部字段值對緩存的實現有影響,具體看參見 圖解 HTTPHTTP 權威指南)。

由多個攔截器構成的攔截器鏈是 Okhttp3 網絡請求的執行關鍵,可以說整個網絡請求能夠正確的執行是有整個鏈驅動的 (責任鏈模式)。仿照 RxJava 是事件驅動的,那麼 Okhttp3 是攔截器驅動的。

關於緩存功能實現的攔截器爲 CacheInterceptor, CacheInterceptor 位於攔截器鏈中間位置,那麼以執行下一個攔截器爲界將緩存流程分爲兩部分:

  1. 觸發之後攔截器之前的操作
  2. 觸發之後攔截器之後的操作

即以 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 實現緩存功能的大致流程基本結束,但是其中還是有很多的邏輯和細節是自己沒有發現和不能理解的,其源碼還是需要不斷的去閱讀去理解,需要對其中的實現、思想有進一步的體會。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章