@[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)鍵的接口: Sink 和 Source 祭椰,對比 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)的具體流程蒲祈。在這之前我們需要明確一下幾個前提:
- OkhttpClient 設(shè)置支持緩存甘萧。
- 網(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í)行下一個攔截器為界將緩存流程分為兩部分:
- 觸發(fā)之后攔截器之前的操作
- 觸發(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)、思想有進一步的體會栓票。