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

@[toc]

本文以一個不同的角度來解讀 Okhttp3 實現(xiàn)緩存功能的思路赖舟,即:對于對于的緩存空間(文件夾)中的緩存文件的生成時機、不同時期下個文件的狀態(tài)隘竭、不同時期下日志文件讀寫塘秦。通過這些方法來真正理解 Okhttp3 的緩存功能。如果你理解 DiskLrcCache 開源庫的設(shè)計动看,那么對于 Okhttp3 的緩存實現(xiàn)你就已經(jīng)掌握了尊剔,因為前者以后者為基礎(chǔ),你甚至沒有看本文的必要菱皆。

1. 需要了解的概念

緩存功能的實現(xiàn)须误,理所當(dāng)然的涉及文件的讀寫操作、緩存機制方案的設(shè)計仇轻。Okhttp3 緩存功能的實現(xiàn)涉及到 Okio 和 DiskLruCache京痢,在闡述具體緩存流程之前,我們需要了解兩者的一些基本概念篷店。

1.2 Okio

Okio 中有兩個關(guān)鍵的接口: SinkSource 祭椰,對比 Java 中 I/O 流概念,我們可以把 Sink 看作 OutputStream , 把 Source 看作 InputStream 疲陕。

類的結(jié)構(gòu)圖如下:

[圖片上傳失敗...(image-fb6118-1548689150138)]

其具體實現(xiàn)非本文重點方淤,有興趣自己可以查看源碼。

1.1 DiskLruCache

Okhttp3 中 DiskLruCache 與JakeWharton 大神的 DiskLruCache 指導(dǎo)思想一致蹄殃,但是具體細(xì)節(jié)不同携茂,比如前者使用 Okio 進行 IO 操作,更加高效诅岩。

在 DiskLruCache 有幾個重要概念讳苦,了解它們,才能對 DiskLruCache 的實現(xiàn)原理有基本的認(rèn)識吩谦。

為了能夠表達(dá)的更加直觀鸳谜,我們看一下一張圖片進行緩存時緩存文件的具體內(nèi)容:


在這里插入圖片描述

1.2.1 日志文件 journal

該文件為 DiskLruCache 內(nèi)部的日志文件,對 cache 的每一次操作都對應(yīng)一條日志式廷,并寫入到 journal 文件中咐扭,同時也可以通過 journal 文件的分析創(chuàng)建 cache。

打開上圖中 journal 文件,具體內(nèi)容為:

libcore.io.DiskLruCache
1
201105
2

DIRTY 0e39614b6f9e1f83c82cf663e453a9d7
CLEAN 0e39614b6f9e1f83c82cf663e453a9d7 4687 14596

在 DiskLruCache.java 類中草描,我們可以看到對 journal 文件內(nèi)容的描述,在這里自己對其簡單翻譯策严,有興趣的朋友可以看 JakeWharton 的描述: DiskLruCache穗慕。

文件的前五行構(gòu)成頭部,格式一般固定妻导。
第一行: 常量 -- libcore.io.DiskLruCache 逛绵;
第二行: 硬盤緩存版本號 --  1
第三行: 應(yīng)用版本號 -- 201105
第四行: 一個有意義的值 -- 2
第五行: 空白行

頭部后的每一行都是 Cache 中 Entry 狀態(tài)的一條記錄。
每條記錄的信息包括: 狀態(tài)值(DIRTY CLEAN READ REMOVE) 緩存信息entry的key值 狀態(tài)相關(guān)的值(可選)倔韭。 

下面對記錄的狀態(tài)進行說明:
DIRTY: 該狀態(tài)表明一個 entry 正在被創(chuàng)建或更新术浪。每一個成功的 DIRTY 操作記錄后應(yīng)該 CLEAN 或 REMOVE 操作記錄,否則被臨時創(chuàng)建的文件會被刪除寿酌。
CLEAN: 該狀態(tài)表明一個 entry 已經(jīng)被成功的創(chuàng)建胰苏,并且可以被讀取,后面記錄了對應(yīng)兩個文件文件(具體哪兩個文件后面會談到)的字節(jié)數(shù)醇疼。
READ: 該狀態(tài)表明正在跟蹤 LRU 的訪問硕并。
REMOVE: 該狀態(tài)表明entry被刪除了。

需要注意的是在這里 DIRTY 并不是 “臟”秧荆、“臟數(shù)據(jù)” 的意思倔毙,而是這個數(shù)據(jù)的狀態(tài)不為最終態(tài)、穩(wěn)定態(tài)乙濒,該文件現(xiàn)在正在被操作陕赃,
而 CLEAN 并不是數(shù)據(jù)被清除,而是表示該文件的操作已經(jīng)完成颁股。同時在后續(xù)的 dirtyFiles 和 cleanFiles 也表示此含義么库。

關(guān)于日志文件在整個緩存系統(tǒng)中的作用,在后續(xù)過程中用到它的時候在具體闡述豌蟋。

1.2.2 DiskLruCache.Entry

每個 DiskLruCache.Entry 對象代表對每一個 URl 在緩存中的操作對象廊散,該類成員變量的具體含義如下:

private final class Entry {
        final String key; // Entry 的 key
        final long[] lengths; // key.0 key.1 文件字節(jié)數(shù)的數(shù)組
        final File[] cleanFiles; // 穩(wěn)定的文件數(shù)組
        final File[] dirtyFiles;// 正在執(zhí)行操作的文件數(shù)組
        boolean readable;// 如果該條目被提交了,為 true
        Editor currentEditor;// 正在執(zhí)行的編輯對象梧疲,在沒有編輯時為 null
        long sequenceNumber;// 編輯條目的最近提交的序列號
        
        ...
        ...
}

具體操作在緩存實現(xiàn)流程中闡述允睹。

1.2.3 DiskLruCache.SnapShot

此類為緩存的快照,為緩存空間中特定時刻的緩存的狀態(tài)幌氮、內(nèi)容缭受,該類成員變量的具體含義:

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;// 對應(yīng) Entry 中的 lengths,為文件字節(jié)大小
        
        ...
        ...
}

1.2.3 DiskLruCache.Editor

該類為 DiskLruCache 的編輯器该互,顧名思義該類是對 DiskLruCache 執(zhí)行的一系列操作米者,如:abort() 、 commit() 等。

Entry publish 的含義是什么蔓搞?胰丁??喂分?锦庸?

2. 緩存實現(xiàn)的有關(guān)流程

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

  1. OkhttpClient 設(shè)置支持緩存甘萧。
  2. 網(wǎng)絡(luò)請求頭部中的字段設(shè)置為支持緩存(Http 協(xié)議首部字段值對緩存的實現(xiàn)有影響,具體看參見 圖解 HTTP梆掸、HTTP 權(quán)威指南)扬卷。

由多個攔截器構(gòu)成的攔截器鏈?zhǔn)?Okhttp3 網(wǎng)絡(luò)請求的執(zhí)行關(guān)鍵,可以說整個網(wǎng)絡(luò)請求能夠正確的執(zhí)行是有整個鏈驅(qū)動的 (責(zé)任鏈模式)酸钦。仿照 RxJava 是事件驅(qū)動的怪得,那么 Okhttp3 是攔截器驅(qū)動的。

關(guān)于緩存功能實現(xiàn)的攔截器為 CacheInterceptor, CacheInterceptor 位于攔截器鏈中間位置钝鸽,那么以執(zhí)行下一個攔截器為界將緩存流程分為兩部分:

  1. 觸發(fā)之后攔截器之前的操作
  2. 觸發(fā)之后攔截器之后的操作

即以 networkResponse = chain.proceed(networkRequest); 為分界

1. 觸發(fā)之后攔截器之前的操作

Response cacheCandidate = cache != null
                ? cache.get(chain.request())// 執(zhí)行 DiskLruCache#initialize()
                : null;//本地緩存

        long now = System.currentTimeMillis();
        // 緩存策略
        CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
        //策略中的請求
        Request networkRequest = strategy.networkRequest;
        ////策略中的響應(yīng)
        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.
        }

        //緩存和網(wǎng)絡(luò)皆為空汇恤,返回code 為504 的響應(yīng)
        // 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 日志文件的初始化

當(dāng)執(zhí)行如下代碼時會按照調(diào)用鏈執(zhí)行相關(guān)邏輯:

Response cacheCandidate = cache != null
                ? cache.get(chain.request())// 執(zhí)行 DiskLruCache#initialize()
                : null;//本地緩存

首先檢查在緩存中是否存在該 request 對應(yīng)的緩存數(shù)據(jù)拔恰,如果有的話就返回 Response因谎,如果沒有就置 null。

調(diào)用鏈來到以下方法:

@Nullable
Response get(Request request) {
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
        snapshot = cache.get(key);// 在這里會執(zhí)行 
        ...
    return response;
}

snapshot = cache.get(key); 處執(zhí)行相應(yīng)的初始化操作颜懊。

在此過程中執(zhí)行一個特別重要的操作财岔,需要對緩存中的 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 是標(biāo)志位 initialized = false,那么由 代碼 1 可知此時需要執(zhí)行初始化操作咸这。

if (initialized) {// 代碼 1 
    return; // Already initialized.
}
1.1 若 journal 日志文件存在

如果存在 journal.bak 那么將該文件重命名為 journal夷恍。

接下來對 journal 日志文件所做的操作如 代碼 3、4 媳维、5 所示酿雪,具體作用做如下闡述。代碼 3 要做的是讀取日志文件 journal 并根據(jù)日志內(nèi)容初始化 LinkedHashMap<String, Entry> lruEntries 中的元素侄刽,DiskLruCache 正是通過 LinkedHashMap 來實現(xiàn) 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) {// 不斷執(zhí)行如下操作,直到文件尾部州丹,結(jié)束如下操作
                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 日志文件的每一行,根據(jù)日志文件的每一行生成 Entry 存入 lruEntries 中用來實現(xiàn) LRU 功能吓揪。

  private void readJournalLine(String line) throws IOException {
        ...
        ...
        // 一頓操作得到 key 的值
        
        // 根據(jù)日志文件中 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() 執(zhí)行完畢后相當(dāng)于對 lruEntries 進行初始化。lruEntries 元素的個數(shù)等于該 App 在此緩存文件夾下緩存文件的個數(shù)柠辞。在此過程中如果 lruEntries 中沒有此行日志中的 key 對應(yīng)的 Entry 對象赁炎,因為現(xiàn)在為進入 App 中的對緩存空間的初始化,所以都需要新建該類的對象:

 // 根據(jù)日志文件中 key 值獲得或者生成 Entry钾腺,存入 lruEntries 中
    Entry entry = lruEntries.get(key);
        if (entry == null) {
        entry = new Entry(key);
        lruEntries.put(key, entry);
    }

新建 Entry 對象的過程對于整個緩存體系的構(gòu)建也十分重要,代碼如下:

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.
    //名稱是重復(fù)的讥裤,所以要重復(fù)使用相同的構(gòu)建器以避免分配
    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);
        }
    }

新建對象過程中會根據(jù) valueCount = 2; 的值定義緩存文件分別為 key.0放棒、key.1、key.0.tmp己英、key.1.tmp ,其中 key.0 為穩(wěn)定狀態(tài)下的請求的 mate 數(shù)據(jù)间螟,key.1 為穩(wěn)定狀態(tài)下的緩存數(shù)據(jù),而 key.0.tmp损肛、key.1.tmp 分別為 mate 數(shù)據(jù)和緩存數(shù)據(jù)的臨時文件,此時并不會真正的新建文件厢破。

在這里需要明確的是 cleanFiles 和 dirtyFiles 都是 Entry 的成員變量,也就是說是通過 Entry 的對象對兩者進行讀取并進行相關(guān)操作的治拿。

processJournal() 方法實現(xiàn)了緩存文件夾下刪除無用的文件摩泪。

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; 的文件劫谅,說明此文件為處在編輯狀態(tài)下见坑,但是此時的時機為剛打開 App 后的初始化狀態(tài),所有的文件均不應(yīng)該處在編輯狀態(tài)捏检,所以此狀態(tài)下的文件即為無用的文件荞驴,需要被刪除。

執(zhí)行完畢后標(biāo)志位 initialized 置位為 true 并中斷執(zhí)行 (return;) 返回操作去執(zhí)行其他操作贯城。

1.2 若 journal 日志文件不存在

若 journal 日志文件不存在熊楼,那么不會執(zhí)行 代碼 2、3能犯、4鲫骗、5 直接執(zhí)行代碼 6 -- rebuildJournal() ,具體執(zhí)行操作如下:

synchronized void rebuildJournal() throws IOException {
        if (journalWriter != null) {
            journalWriter.close();
        }
        //產(chǎn)生 journal.tmp 文件
        BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp));
        try {// 寫入 journal 文件內(nèi)容
            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) { // 當(dāng)前的 editor 不為 null 說明當(dāng)前 journal 為非穩(wěn)定態(tài)
                    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 不存在悲雳,那么此行代碼執(zhí)行的操作正是新建journal 臨時文件 -- journal.tmp ,寫入文件頭部文件后將 journal.tmp 重命名為 journal 挎峦。前文解析 journal 文件內(nèi)容的含義,此處代碼正好可以作為印證合瓢。

1.2 初始化后

經(jīng)過初始化后最終獲取 DiskLruCache 快照 DiskLruCache$Snapshot 對象坦胶,并進行相關(guān)包裝返回 Response 對象為緩存中的Response 對象。

 @Nullable
    Response get(Request request) {
        ...
        ...
        try {
            snapshot = cache.get(key);// 在這里會執(zhí)行 initialize(),進行一次初始化
            if (snapshot == null) {
                return null;
            }
        ...
        ...
        Response response = entry.response(snapshot);

        ...
        ...

        return response;
    }

至此,以上即為進入 CacheInterceptor 后的第一步操作,說實話工作量真是大顿苇,開啟了 Debug 模式 n 遍才稍微把基本流程搞明白峭咒。

Response cacheCandidate = cache != null
                ? cache.get(chain.request())// 執(zhí)行 DiskLruCache#initialize() ,會對 journal 文件進行一些操作
                : null;//本地緩存

1.3 緩存策略

緩存策略的獲取主要涉及代碼如下:

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

具體執(zhí)行代碼位置:
CacheStrategy#getCandidate()纪岁,由于具體業(yè)務(wù)邏輯比較容易理解凑队,根據(jù)緩存響應(yīng)、請求中頭部關(guān)于緩存的字段進行相關(guān)判斷幔翰,得出緩存策略漩氨,在這里不做過多闡釋。

2. 觸發(fā)之后攔截器之后的操作

觸發(fā)之后的攔截器后遗增,進行相關(guān)的一系列操作叫惊,根據(jù)責(zé)任鏈模式邏輯還是會最終回來,接著此攔截器的邏輯繼續(xù)執(zhí)行做修。此時整個請求的狀態(tài)為已經(jīng)成功得到網(wǎng)絡(luò)響應(yīng)霍狰,那么我們要做的就是對網(wǎng)絡(luò)響應(yīng)進行緩存,具體代碼如下:

if (cache != null) {
    if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
    // Offer this request to the cache.
    CacheRequest cacheRequest = cache.put(response);// 將 response 寫入內(nèi)存中饰及,此時進行的步驟: 創(chuàng)建 0.tmp(已經(jīng)寫入數(shù)據(jù)) 和 1.tmp(尚未寫入數(shù)據(jù))
        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); 執(zhí)行如下邏輯:

CacheRequest put(Response response) {
        ...
        ...
        
        //由Response對象構(gòu)建一個Entry對象,Entry是Cache的一個內(nèi)部類
        Entry entry = new Entry(response);
        DiskLruCache.Editor editor = null;// disk 緩存的編輯
        try {
            editor = cache.edit(key(response.request().url()));// key(response.request().url()) 根據(jù) URL生成唯一 key
            if (editor == null) {
                return null;
            }
            //把這個entry寫入
            //方法內(nèi)部是通過Okio.buffer(editor.newSink(ENTRY_METADATA));獲取到一個BufferedSink對象蔗坯,隨后將Entry中存儲的Http報頭數(shù)據(jù)寫入到sink流中。
            entry.writeTo(editor);// 觸發(fā)生成 0.tmp
            //構(gòu)建一個CacheRequestImpl對象燎含,構(gòu)造器中通過editor.newSink(ENTRY_BODY)方法獲得Sink對象
            return new CacheRequestImpl(editor);// 觸發(fā)生成 1.tmp
        } catch (IOException e) {
            abortQuietly(editor);
            return null;
        }
    }

Cache#writeTo()

// 寫入 0.tmp 數(shù)據(jù) // 寫入 的dirtyfile 文件的 buffersink 輸出流
public void writeTo(DiskLruCache.Editor editor) throws IOException {
    BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));//新建 key.0.tmp
    // TODO: 在這里出現(xiàn)了 0.tmp
    sink.writeUtf8(url)
            .writeByte('\n');
    ....
}

非常明顯的操作在此處創(chuàng)建了 key.0.tmp 文件胀溺,并寫入數(shù)據(jù)桩警,此處寫入的數(shù)據(jù)為 mate 數(shù)據(jù)

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();//最終調(diào)用了此函數(shù)翘狱,0.tmp 1.tmp --》 key.0  key.1 
        }
    };
}

在初始化 CacheRequestImpl 對象時創(chuàng)建了 key.1.tmp 文件盖溺。

執(zhí)行如上操作后回到 CacheInterceptor 執(zhí)行 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();

執(zhí)行一系列操作,使用 Okio 這個庫不斷的向 key.1.tmp 寫入數(shù)據(jù)铣除,具體操作過程實在是太過繁雜谚咬,而且牽涉到 Okio 庫原理,自己在這么短時間無法理清具體流程尚粘。

對于數(shù)據(jù)寫入的切入點自己還沒有很好的認(rèn)識择卦,在何處真正進行寫文件操作自己只能夠通過 Debug 知道其走向,但是對其原理還沒有理解郎嫁。

最后會執(zhí)行 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();// 最終調(diào)用了此函數(shù),0.tmp 1.tmp -> key.0  key.1 
                }
            };
        }

執(zhí)行 editor.commit(); 該方法會調(diào)用的 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 盔腔,這兩個文件分別為兩個文件的穩(wěn)定狀態(tài)杠茬,同時更新 journal 日志記錄月褥。


至此 Okhttp3 實現(xiàn)緩存功能的大致流程基本結(jié)束,但是其中還是有很多的邏輯和細(xì)節(jié)是自己沒有發(fā)現(xiàn)和不能理解的瓢喉,其源碼還是需要不斷的去閱讀去理解宁赤,需要對其中的實現(xiàn)、思想有進一步的體會栓票。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末决左,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子走贪,更是在濱河造成了極大的恐慌佛猛,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坠狡,死亡現(xiàn)場離奇詭異挚躯,居然都是意外死亡,警方通過查閱死者的電腦和手機擦秽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來漩勤,“玉大人感挥,你說我怎么就攤上這事≡桨埽” “怎么了触幼?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長究飞。 經(jīng)常有香客問我置谦,道長,這世上最難降的妖魔是什么亿傅? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任媒峡,我火速辦了婚禮,結(jié)果婚禮上葵擎,老公的妹妹穿的比我還像新娘谅阿。我一直安慰自己,他們只是感情好酬滤,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布签餐。 她就那樣靜靜地躺著,像睡著了一般盯串。 火紅的嫁衣襯著肌膚如雪氯檐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天体捏,我揣著相機與錄音冠摄,去河邊找鬼糯崎。 笑死,一個胖子當(dāng)著我的面吹牛耗拓,可吹牛的內(nèi)容都是我干的拇颅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼乔询,長吁一口氣:“原來是場噩夢啊……” “哼樟插!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起竿刁,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤黄锤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后食拜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸵熟,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年负甸,在試婚紗的時候發(fā)現(xiàn)自己被綠了流强。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡呻待,死狀恐怖打月,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蚕捉,我是刑警寧澤奏篙,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站迫淹,受9級特大地震影響秘通,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜敛熬,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一肺稀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧应民,春花似錦盹靴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至辕狰,卻和暖如春改备,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蔓倍。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工悬钳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留盐捷,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓默勾,卻偏偏與公主長得像碉渡,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子母剥,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 參考資源 官網(wǎng) 國內(nèi)博客 GitHub官網(wǎng) 鑒于一些關(guān)于OKHttp3源碼的解析文檔過于碎片化滞诺,本文系統(tǒng)的,由淺入...
    風(fēng)骨依存閱讀 12,506評論 11 82
  • 1.OkHttp源碼解析(一):OKHttp初階 2 OkHttp源碼解析(二):OkHttp連接的"前戲"——H...
    隔壁老李頭閱讀 7,036評論 2 28
  • DiskLurCache 使用教程 源碼解析 使用 打開緩存 打開緩存函數(shù)public static DiskLr...
    super小立立閱讀 920評論 1 1
  • LruCache與DiskLruCache 文章目錄 一 Lru算法 二 LruCache原理分析2.1 寫入緩存...
    Fitz_Lee閱讀 466評論 0 1
  • 今天自己要開始為期一周的上班時間环疼。 這段時間是自己需要堅持的一段時間习霹。 這段時間自己的主要目標(biāo)還是省錢。 貌似省錢...
    不完美的你我他閱讀 177評論 0 2