- 1.OkHttp源碼解析(一):OKHttp初階
- 2 OkHttp源碼解析(二):OkHttp連接的"前戲"——HTTP的那些事
- 3 OkHttp源碼解析(三):OKHttp中階之線程池和消息隊列
- 4 OkHttp源碼解析(四):OKHttp中階之攔截器及調(diào)用鏈
- 5 OkHttp源碼解析(五):OKHttp中階之OKio簡介
- 6 OkHttp源碼解析(六):OKHttp中階之緩存基礎
- 7 OkHttp源碼解析(七):OKHttp中階之緩存機制
- 8 OkHttp源碼解析(八):OKHttp中階之連接與請求值前奏
- 9 OkHttp源碼解析(九):OKHTTP連接中三個"核心"RealConnection锦援、ConnectionPool仓技、StreamAllocation
- 10 OkHttp源碼解析(十) OKHTTP中連接與請求
- 11 OkHttp的感謝
上一章主要講解了HTTP中的緩存以及OKHTTP中的緩存嘲恍,今天我們主要講解OKHTTP中緩存體系的精髓---DiskLruCache,由于篇幅限制脐区,今天內(nèi)容看似不多,大概分為兩個部分
1.DiskLruCache內(nèi)部類詳解
2.DiskLruCache類詳解
3.OKHTTP的緩存的實現(xiàn)---CacheInterceptor的具體執(zhí)行流程
一她按、DiskLruCache
在看DiskLruCache前先看下他的幾個內(nèi)部類
1牛隅、Entry.class(DiskLruCache的內(nèi)部類)
Entry內(nèi)部類是實際用于存儲的緩存數(shù)據(jù)的實體類炕柔,每一個url對應一個Entry實體
private final class Entry {
final String key;
/** 實體對應的緩存文件 */
/** Lengths of this entry's files. */
final long[] lengths; //文件比特數(shù)
final File[] cleanFiles;
final File[] dirtyFiles;
/** 實體是否可讀,可讀為true媒佣,不可讀為false*/
/** True if this entry has ever been published. */
boolean readable;
/** 編輯器匕累,如果實體沒有被編輯過,則為null*/
/** The ongoing edit or null if this entry is not being edited. */
Editor currentEditor;
/** 最近提交的Entry的序列號 */
/** The sequence number of the most recently committed edit to this entry. */
long sequenceNumber;
//構(gòu)造器 就一個入?yún)?key默伍,而key又是url欢嘿,所以,一個url對應一個Entry
Entry(String key) {
this.key = key;
//valueCount在構(gòu)造DiskLruCache時傳入的參數(shù)默認大小為2
//具體請看Cache類的構(gòu)造函數(shù)巡验,里面通過DiskLruCache.create()方法創(chuàng)建了DiskLruCache际插,并且傳入一個值為2的ENTRY_COUNT常量
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();
//由于valueCount為2,所以循環(huán)了2次,一共創(chuàng)建了4份文件
//分別為key.1文件和key.1.tmp文件
// key.2文件和key.2.tmp文件
for (int i = 0; i < valueCount; i++) {
fileBuilder.append(i);
cleanFiles[i] = new File(directory, fileBuilder.toString());
fileBuilder.append(".tmp");
dirtyFiles[i] = new File(directory, fileBuilder.toString());
fileBuilder.setLength(truncateTo);
}
}
通過上述代碼咱們知道了显设,一個url對應一個Entry對象框弛,同時,每個Entry對應兩個文件捕捂,key.1存儲的是Response的headers瑟枫,key.2文件存儲的是Response的body
2、Snapshot (DiskLruCache的內(nèi)部類)
/** A snapshot of the values for an entry. */
public final class Snapshot implements Closeable {
private final String key; //也有一個key
private final long sequenceNumber; //序列號
private final Source[] sources; //可以讀入數(shù)據(jù)的流 這么多的流主要是從cleanFile中讀取數(shù)據(jù)
private final long[] lengths; //與上面的流一一對應
//構(gòu)造器就是對上面這些屬性進行賦值
Snapshot(String key, long sequenceNumber, Source[] sources, long[] lengths) {
this.key = key;
this.sequenceNumber = sequenceNumber;
this.sources = sources;
this.lengths = lengths;
}
public String key() {
return key;
}
//edit方法主要就是調(diào)用DiskLruCache的edit方法了指攒,入?yún)⑹窃揝napshot對象的兩個屬性key和sequenceNumber.
/**
* Returns an editor for this snapshot's entry, or null if either the entry has changed since
* this snapshot was created or if another edit is in progress.
*/
public Editor edit() throws IOException {
return DiskLruCache.this.edit(key, sequenceNumber);
}
/** Returns the unbuffered stream with the value for {@code index}. */
public Source getSource(int index) {
return sources[index];
}
/** Returns the byte length of the value for {@code index}. */
public long getLength(int index) {
return lengths[index];
}
public void close() {
for (Source in : sources) {
Util.closeQuietly(in);
}
}
}
這時候再回來看下Entry里面的snapshot()方法
/**
* Returns a snapshot of this entry. This opens all streams eagerly to guarantee that we see a
* single published snapshot. If we opened streams lazily then the streams could come from
* different edits.
*/
Snapshot snapshot() {
//首先判斷 線程是否有DiskLruCache對象的鎖
if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();
//new了一個Souce類型數(shù)組慷妙,容量為2
Source[] sources = new Source[valueCount];
//clone一個long類型的數(shù)組,容量為2
long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
//獲取cleanFile的Source允悦,用于讀取cleanFile中的數(shù)據(jù)膝擂,并用得到的souce、Entry.key隙弛、Entry.length架馋、sequenceNumber數(shù)據(jù)構(gòu)造一個Snapshot對象
try {
for (int i = 0; i < valueCount; i++) {
sources[i] = fileSystem.source(cleanFiles[i]);
}
return new Snapshot(key, sequenceNumber, sources, lengths);
} catch (FileNotFoundException e) {
// A file must have been deleted manually!
for (int i = 0; i < valueCount; i++) {
if (sources[i] != null) {
Util.closeQuietly(sources[i]);
} else {
break;
}
}
// Since the entry is no longer valid, remove it so the metadata is accurate (i.e. the cache
// size.)
try {
removeEntry(this);
} catch (IOException ignored) {
}
return null;
}
}
由上面代碼可知Spapshot里面的key,sequenceNumber全闷,sources叉寂,lenths都是一個entry,其實也就可以說一個Entry對象一一對應一個Snapshot對象
3、Editor.class(DiskLruCache的內(nèi)部類)
Editro類的屬性和構(gòu)造器貌似看不到什么東西总珠,不過通過構(gòu)造器屏鳍,我們知道,在構(gòu)造一個Editor的時候必須傳入一個Entry局服,莫非Editor是對這個Entry操作類钓瞭。
/** Edits the values for an entry. */
public final class Editor {
final Entry entry;
final boolean[] written;
private boolean done;
Editor(Entry entry) {
this.entry = entry;
this.written = (entry.readable) ? null : new boolean[valueCount];
}
/**
* Prevents this editor from completing normally. This is necessary either when the edit causes
* an I/O error, or if the target entry is evicted while this editor is active. In either case
* we delete the editor's created files and prevent new files from being created. Note that once
* an editor has been detached it is possible for another editor to edit the entry.
*這里說一下detach方法,當編輯器(Editor)處于io操作的error的時候腌逢,或者editor正在被調(diào)用的時候而被清
*除的降淮,為了防止編輯器可以正常的完成。我們需要刪除編輯器創(chuàng)建的文件,并防止創(chuàng)建新的文件佳鳖。如果編
*輯器被分離霍殴,其他的編輯器可以編輯這個Entry
*/
void detach() {
if (entry.currentEditor == this) {
for (int i = 0; i < valueCount; i++) {
try {
fileSystem.delete(entry.dirtyFiles[i]);
} catch (IOException e) {
// This file is potentially leaked. Not much we can do about that.
}
}
entry.currentEditor = null;
}
}
/**
* Returns an unbuffered input stream to read the last committed value, or null if no value has
* been committed.
* 獲取cleanFile的輸入流 在commit的時候把done設為true
*/
public Source newSource(int index) {
synchronized (DiskLruCache.this) {
//如果已經(jīng)commit了,不能讀取了
if (done) {
throw new IllegalStateException();
}
//如果entry不可讀系吩,并且已經(jīng)有編輯器了(其實就是dirty)
if (!entry.readable || entry.currentEditor != this) {
return null;
}
try {
//通過filesystem獲取cleanFile的輸入流
return fileSystem.source(entry.cleanFiles[index]);
} catch (FileNotFoundException e) {
return null;
}
}
}
/**
* Returns a new unbuffered output stream to write the value at {@code index}. If the underlying
* output stream encounters errors when writing to the filesystem, this edit will be aborted
* when {@link #commit} is called. The returned output stream does not throw IOExceptions.
* 獲取dirty文件的輸出流来庭,如果在寫入數(shù)據(jù)的時候出現(xiàn)錯誤,會立即停止穿挨。返回的輸出流不會拋IO異常
*/
public Sink newSink(int index) {
synchronized (DiskLruCache.this) {
//已經(jīng)提交月弛,不能操作
if (done) {
throw new IllegalStateException();
}
//如果編輯器是不自己的,不能操作
if (entry.currentEditor != this) {
return Okio.blackhole();
}
//如果entry不可讀科盛,把對應的written設為true
if (!entry.readable) {
written[index] = true;
}
//如果文件
File dirtyFile = entry.dirtyFiles[index];
Sink sink;
try {
//如果fileSystem獲取文件的輸出流
sink = fileSystem.sink(dirtyFile);
} catch (FileNotFoundException e) {
return Okio.blackhole();
}
return new FaultHidingSink(sink) {
@Override protected void onException(IOException e) {
synchronized (DiskLruCache.this) {
detach();
}
}
};
}
}
/**
* Commits this edit so it is visible to readers. This releases the edit lock so another edit
* may be started on the same key.
* 寫好數(shù)據(jù)帽衙,一定不要忘記commit操作對數(shù)據(jù)進行提交,我們要把dirtyFiles里面的內(nèi)容移動到cleanFiles里才能夠讓別的editor訪問到
*/
public void commit() throws IOException {
synchronized (DiskLruCache.this) {
if (done) {
throw new IllegalStateException();
}
if (entry.currentEditor == this) {
completeEdit(this, true);
}
done = true;
}
}
/**
* Aborts this edit. This releases the edit lock so another edit may be started on the same
* key.
*/
public void abort() throws IOException {
synchronized (DiskLruCache.this) {
if (done) {
throw new IllegalStateException();
}
if (entry.currentEditor == this) {
//這個方法是DiskLruCache的方法在后面講解
completeEdit(this, false);
}
done = true;
}
}
public void abortUnlessCommitted() {
synchronized (DiskLruCache.this) {
if (!done && entry.currentEditor == this) {
try {
completeEdit(this, false);
} catch (IOException ignored) {
}
}
}
}
}
哎贞绵,看到這個了類的注釋厉萝,發(fā)現(xiàn)Editor的確就是編輯entry類的。
Editor里面的幾個方法Source newSource(int index) 榨崩,Sink newSink(int index)谴垫,commit(),abort()母蛛,abortUnlessCommitted() 翩剪,既然是編輯器,我們看到上面的方法應該可以猜到彩郊,上面的方法一次對應如下
方法 | 意義 |
---|---|
Source newSource(int index) | 返回指定index的cleanFile的讀入流 |
Sink newSink(int index) | 向指定index的dirtyFiles文件寫入數(shù)據(jù) |
commit() | 這里執(zhí)行的工作是提交數(shù)據(jù)粪薛,并釋放鎖难审,最后通知DiskLruCache刷新相關數(shù)據(jù) |
abort() | 終止編輯栗竖,并釋放鎖 |
abortUnlessCommitted() | 除非正在編輯替废,否則終止 |
abort()和abortUnlessCommitted()最后都會執(zhí)行completeEdit(Editor, boolean) 這個方法這里簡單說下:
success情況提交:dirty文件會被更名為clean文件粒竖,entry.lengths[i]值會被更新娩脾,DiskLruCache,size會更新(DiskLruCache,size代表的是所有整個緩存文件加起來的總大屑铡)先舷,redundantOpCount++哩盲,在日志中寫入一條Clean信息
failed情況:dirty文件被刪除前方,redundantOpCount++,日志中寫入一條REMOVE信息
至此DiskLruCache的內(nèi)部類就全部介紹結(jié)束了×停現(xiàn)在咱們正式關注下DiskLruCache類
二惠险、DiskLruCache類詳解
(一)、重要屬性
DiskLruCache里面有一個屬性是lruEntries如下:
private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);
/** Used to run 'cleanupRunnable' for journal rebuilds. */
private final Executor executor;
LinkedHashMap自帶Lru算法的光環(huán)屬性抒线,詳情請看LinkedHashMap源碼說明
DiskLruCache也有一個線程池屬性 executor班巩,不過該池最多有一個線程工作,用于清理,維護緩存數(shù)據(jù)抱慌。創(chuàng)建一個DiskLruCache對象的方法是調(diào)用該方法逊桦,而不是直接調(diào)用構(gòu)造器。
(二)抑进、構(gòu)造函數(shù)和創(chuàng)建對象
DiskLruCache有一個構(gòu)造函數(shù)强经,但是不是public的所以DiskLruCache只能被包內(nèi)中類調(diào)用,不能在外面直接new寺渗。不過DiskLruCache提供了一個靜態(tài)方法create匿情,對外提供DiskLruCache對象
//DiskLruCache.java
/**
* Create a cache which will reside in {@code directory}. This cache is lazily initialized on
* first access and will be created if it does not exist.
*
* @param directory a writable directory
* @param valueCount the number of values per cache entry. Must be positive.
* @param maxSize the maximum number of bytes this cache should use to store
*/
public static DiskLruCache create(FileSystem fileSystem, File directory, int appVersion,
int valueCount, long maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
//這個executor其實就是DiskLruCache里面的executor
// Use a single background thread to evict entries.
Executor executor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true));
return new DiskLruCache(fileSystem, directory, appVersion, valueCount, maxSize, executor);
}
static final String JOURNAL_FILE = "journal";
static final String JOURNAL_FILE_TEMP = "journal.tmp";
static final String JOURNAL_FILE_BACKUP = "journal.bkp"
DiskLruCache(FileSystem fileSystem, File directory, int appVersion, int valueCount, long maxSize,
Executor executor) {
this.fileSystem = fileSystem;
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE);
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
this.valueCount = valueCount;
this.maxSize = maxSize;
this.executor = executor;
}
該構(gòu)造器會在制定的目錄下創(chuàng)建三份文件,這三個文件是DiskLruCache的工作日志文件信殊。在執(zhí)行DiskLruCache的任何方法之前都會執(zhí)行initialize()方法來完成DiskLruCache的初始化炬称,有人會想為什么不在DiskLruCache的構(gòu)造器中完成對該方法的調(diào)用,其實是為了延遲初始化涡拘,因為初始化會創(chuàng)建一系列的文件和對象玲躯,所以做了延遲初始化。
(三)鲸伴、初始化
那么來看下initialize里面的代碼
public synchronized void initialize() throws IOException {
//斷言府蔗,當持有自己鎖的時候。繼續(xù)執(zhí)行汞窗,沒有持有鎖姓赤,直接拋異常
assert Thread.holdsLock(this);
//如果已經(jīng)初始化過,則不需要再初始化仲吏,直接rerturn
if (initialized) {
return; // Already initialized.
}
// If a bkp file exists, use it instead.
//如果有journalFileBackup文件
if (fileSystem.exists(journalFileBackup)) {
// If journal file also exists just delete backup file.
//如果有journalFile文件
if (fileSystem.exists(journalFile)) {
//有journalFile文件 則刪除journalFileBackup文件
fileSystem.delete(journalFileBackup);
} else {
//沒有journalFile不铆,則將journalFileBackUp更名為journalFile
fileSystem.rename(journalFileBackup, journalFile);
}
}
// Prefer to pick up where we left off.
if (fileSystem.exists(journalFile)) {
//如果有journalFile文件,則對該文件裹唆,則分別調(diào)用readJournal()方法和processJournal()方法
try {
readJournal();
processJournal();
//設置初始化過標志
initialized = true;
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 {
//如果沒有journalFile則刪除
delete();
} finally {
closed = false;
}
}
//重新建立journal文件
rebuildJournal();
initialized = true;
}
大家發(fā)現(xiàn)沒有誓斥,如論是否有journal文件,最后都會將initialized設為true,該值不會再被設置為false许帐,除非DiskLruCache對象唄銷毀劳坑。這表明initialize()放啊在DiskLruCache對象的整個生命周期中只會執(zhí)行一次。該動作完成日志的寫入和lruEntries集合的初始化成畦。
這里面分別調(diào)用了readJournal()方法和processJournal()方法距芬,那咱們依次分析下這兩個方法,這里面有大量的okio里面的代碼,如果大家對okio不熟悉能讀上一篇文章循帐。
private void readJournal() throws IOException {
//獲取journalFile的source即輸入流
BufferedSource source = Okio.buffer(fileSystem.source(journalFile));
try {
//讀取相關數(shù)據(jù)
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;
//校驗通過框仔,開始逐行讀取數(shù)據(jù)
while (true) {
try {
readJournalLine(source.readUtf8LineStrict());
lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
//讀取出來的行數(shù)減去lruEntriest的集合的差值,即日志多出的"冗余"記錄
redundantOpCount = lineCount - lruEntries.size();
// If we ended on a truncated line, rebuild the journal before appending to it.
//source.exhausted()表示是否還多余字節(jié)拄养,如果沒有多余字節(jié)离斩,返回true,有多月字節(jié)返回false
if (!source.exhausted()) {
//如果有多余字節(jié),則重新構(gòu)建下journal文件跛梗,主要是寫入頭文件寻馏,以便下次讀的時候,根據(jù)頭文件進行校驗
rebuildJournal();
} else {
//獲取這個文件的Sink
journalWriter = newJournalWriter();
}
} finally {
Util.closeQuietly(source);
}
}
這里說一下ource.readUtf8LineStrict()方法茄袖,這個方法是BufferedSource接口的方法操软,具體實現(xiàn)是RealBufferedSource,所以大家要去RealBufferedSource里面去找具體實現(xiàn)宪祥。我這里簡單說下聂薪,就是從source里面按照utf-8編碼取出一行的數(shù)據(jù)。這里面讀取了magic蝗羊,version藏澳,appVersionString,valueCountString耀找,blank翔悠,然后進行校驗,這個數(shù)據(jù)是在"寫"的時候野芒,寫入的蓄愁,具體情況看DiskLruCache的rebuildJournal()方法。隨后記錄redundantOpCount的值狞悲,該值的含義就是判斷當前日志中記錄的行數(shù)和lruEntries集合容量的差值撮抓,即日志中多出來的"冗余"記錄。
讀取的時候又調(diào)用了readJournalLine()方法摇锋,咱們來研究下這個方法
private void readJournalLine(String line) throws IOException {
獲取空串的position丹拯,表示頭
int firstSpace = line.indexOf(' ');
//空串的校驗
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
//第一個字符的位置
int keyBegin = firstSpace + 1;
// 方法返回第一個空字符在此字符串中第一次出現(xiàn),在指定的索引即keyBegin開始搜索荸恕,所以secondSpace是愛這個字符串中的空字符(不包括這一行最左側(cè)的那個空字符)
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
//如果沒有中間的空字符
if (secondSpace == -1) {
//截取剩下的全部字符串構(gòu)成key
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
//如果解析的是REMOVE信息乖酬,則在lruEntries里面刪除這個key
lruEntries.remove(key);
return;
}
} else {
//如果含有中間間隔的空字符,則截取這個中間間隔到左側(cè)空字符之間的字符串融求,構(gòu)成key
key = line.substring(keyBegin, secondSpace);
}
//獲取key后咬像,根據(jù)key取出Entry對象
Entry entry = lruEntries.get(key);
//如果Entry為null,則表明內(nèi)存中沒有生宛,則new一個施掏,并把它放到內(nèi)存中。
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
//如果是CLEAN開頭
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
//line.substring(secondSpace + 1) 為獲取中間空格后面的內(nèi)容茅糜,然后按照空字符分割,設置entry的屬性素挽,表明是干凈的數(shù)據(jù)蔑赘,不能編輯。
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)) {
//如果是以DIRTY開頭,則設置一個新的Editor缩赛,表明可編輯
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);
}
}
這里面主要是具體的解析耙箍,如果每次解析的是非REMOVE信息,利用該key創(chuàng)建一個entry酥馍,如果是判斷信息是CLEAN則設置ENTRY為可讀辩昆,并設置entry.currentEditor表明當前Entry不可編輯,調(diào)用entry.setLengths(String[])旨袒,設置該entry.lengths的初始值汁针。如果判斷是Dirty則設置enry.currentEdtor=new Editor(entry);表明當前Entry處于被編輯狀態(tài)砚尽。
通過上面我得到了如下的結(jié)論:
- 1施无、如果是CLEAN的話,對這個entry的文件長度進行更新
- 2必孤、如果是DIRTY猾骡,說明這個值正在被操作,還沒有commit敷搪,于是給entry分配一個Editor兴想。
- 3、如果是READ赡勘,說明這個值被讀過了嫂便,什么也不做。
看下journal文件你就知道了
1 * libcore.io.DiskLruCache
2 * 1
3 * 100
4 * 2
5 *
6 * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
7 * DIRTY 335c4c6028171cfddfbaae1a9c313c52
8 * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
9 * REMOVE 335c4c6028171cfddfbaae1a9c313c52
10 * DIRTY 1ab96a171faeeee38496d8b330771a7a
11 * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
12 * READ 335c4c6028171cfddfbaae1a9c313c52
13 * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
然后又調(diào)用了processJournal()方法狮含,那我們來看下:
/**
* Computes the initial size and collects garbage as a part of opening the cache. Dirty entries
* are assumed to be inconsistent and will be deleted.
*/
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();
}
}
}
先是刪除了journalFileTmp文件
然后調(diào)用for循環(huán)獲取鏈表中的所有Entry顽悼,如果Entry的中Editor!=null,則表明Entry數(shù)據(jù)時臟的DIRTY几迄,所以不能讀蔚龙,進而刪除Entry下的緩存文件,并且將Entry從lruEntries中移除映胁。如果Entry的Editor==null木羹,則證明該Entry下的緩存文件可用,記錄它所有緩存文件的緩存數(shù)量解孙,結(jié)果賦值給size坑填。
readJournal()方法里面調(diào)用了rebuildJournal(),initialize()方法同樣會readJourna弛姜,但是這里說明下:readJournal里面調(diào)用的rebuildJournal()是有條件限制的脐瑰,initialize()是一定會調(diào)用的。那我們來研究下readJournal()
/**
* Creates a new journal that omits redundant information. This replaces the current journal if it
* exists.
*/
synchronized void rebuildJournal() throws IOException {
//如果寫入流不為空
if (journalWriter != null) {
//關閉寫入流
journalWriter.close();
}
//通過okio獲取一個寫入BufferedSinke
BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp));
try {
//寫入相關信息和讀取向?qū)⒕剩@時候大家想下readJournal
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里面的值
for (Entry entry : lruEntries.values()) {
//如果editor不為null苍在,則為DIRTY數(shù)據(jù)
if (entry.currentEditor != null) {
在開頭寫上 DIRTY绝页,然后寫上 空字符
writer.writeUtf8(DIRTY).writeByte(' ');
//把entry的key寫上
writer.writeUtf8(entry.key);
//換行
writer.writeByte('\n');
} else {
//如果editor為null,則為CLEAN數(shù)據(jù), 在開頭寫上 CLEAN寂恬,然后寫上 空字符
writer.writeUtf8(CLEAN).writeByte(' ');
//把entry的key寫上
writer.writeUtf8(entry.key);
//結(jié)尾接上兩個十進制的數(shù)字续誉,表示長度
entry.writeLengths(writer);
//換行
writer.writeByte('\n');
}
}
} finally {
//最后關閉寫入流
writer.close();
}
//如果存在journalFile
if (fileSystem.exists(journalFile)) {
//把journalFile文件重命名為journalFileBackup
fileSystem.rename(journalFile, journalFileBackup);
}
然后又把臨時文件,重命名為journalFile
fileSystem.rename(journalFileTmp, journalFile);
//刪除備份文件
fileSystem.delete(journalFileBackup);
//拼接一個新的寫入流
journalWriter = newJournalWriter();
//設置沒有error標志
hasJournalErrors = false;
//設置最近重新創(chuàng)建journal文件成功
mostRecentRebuildFailed = false;
}
總結(jié)下:
獲取一個寫入流初肉,將lruEntries集合中的Entry對象寫入tmp文件中酷鸦,根據(jù)Entry的currentEditor的值判斷是CLEAN還是DIRTY,寫入該Entry的key,如果是CLEAN還要寫入文件的大小bytes牙咏。然后就是把journalFileTmp更名為journalFile臼隔,然后將journalWriter跟文件綁定,通過它來向journalWrite寫入數(shù)據(jù)眠寿,最后設置一些屬性躬翁。
我們可以砍到,rebuild操作是以lruEntries為準盯拱,把DIRTY和CLEAN的操作都寫回到journal中盒发。但發(fā)現(xiàn)沒有,其實沒有改動真正的value狡逢,只不過重寫了一些事務的記錄宁舰。事實上,lruEntries和journal文件共同確定了cache數(shù)據(jù)的有效性奢浑。lruEntries是索引蛮艰,journal是歸檔。至此序列化部分就已經(jīng)結(jié)束了
(四)雀彼、關于Cache類調(diào)用的幾個方法
上回書說道Cache調(diào)用DiskCache的幾個方法壤蚜,如下:
- 1.DiskLruCache.get(String)獲取DiskLruCache.Snapshot
- 2.DiskLruCache.remove(String)移除請求
- 3.DiskLruCache.edit(String);獲得一個DiskLruCache.Editor對象徊哑,
- 4.DiskLruCache.Editor.newSink(int)袜刷;獲得一個sink流 (具體看Editor類)
- 5.DiskLruCache.Snapshot.getSource(int);獲取一個Source對象莺丑。 (具體看Editor類)
- 6.DiskLruCache.Snapshot.edit()著蟹;獲得一個DiskLruCache.Editor對象,
1梢莽、DiskLruCache.Snapshot get(String)方法
public synchronized Snapshot get(String key) throws IOException {
//初始化
initialize();
//檢查緩存是否已經(jīng)關閉
checkNotClosed();
//檢驗key
validateKey(key);
//如果以上都通過萧豆,先獲取內(nèi)存中的數(shù)據(jù),即根據(jù)key在linkedList查找
Entry entry = lruEntries.get(key);
//如果沒有值昏名,或者有值涮雷,但是值不可讀
if (entry == null || !entry.readable) return null;
//獲取entry里面的snapshot的值
Snapshot snapshot = entry.snapshot();
//如果有snapshot為null,則直接返回null
if (snapshot == null) return null;
//如果snapshot不為null
//計數(shù)器自加1
redundantOpCount++;
//把這個內(nèi)容寫入文檔中
journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
//如果超過上限
if (journalRebuildRequired()) {
//開始清理
executor.execute(cleanupRunnable);
}
//返回數(shù)據(jù)
return snapshot;
}
/**
* We only rebuild the journal when it will halve the size of the journal and eliminate at least
* 2000 ops.
*/
boolean journalRebuildRequired() {
//最大計數(shù)單位
final int redundantOpCompactThreshold = 2000;
//清理的條件
return redundantOpCount >= redundantOpCompactThreshold
&& redundantOpCount >= lruEntries.size();
}
主要就是先去拿snapshot轻局,然后會用journalWriter向journal寫入一條read記錄洪鸭,最后判斷是否需要清理膜钓。
清理的條件是當前redundantOpCount大于2000,并且redundantOpCount的值大于linkedList里面的size卿嘲。咱們接著看下清理任務
private final Runnable cleanupRunnable = new Runnable() {
public void run() {
synchronized (DiskLruCache.this) {
//如果沒有初始化或者已經(jīng)關閉了,則不需要清理
if (!initialized | closed) {
return; // Nothing to do
}
try {
trimToSize();
} catch (IOException ignored) {
//如果拋異常了夫壁,設置最近的一次清理失敗
mostRecentTrimFailed = true;
}
try {
//如果需要清理了
if (journalRebuildRequired()) {
//重新創(chuàng)建journal文件
rebuildJournal();
//計數(shù)器歸于0
redundantOpCount = 0;
}
} catch (IOException e) {
//如果拋異常了拾枣,設置最近的一次構(gòu)建失敗
mostRecentRebuildFailed = true;
journalWriter = Okio.buffer(Okio.blackhole());
}
}
}
};
void trimToSize() throws IOException {
//如果超過上限
while (size > maxSize) {
//取出一個Entry
Entry toEvict = lruEntries.values().iterator().next();
//刪除這個Entry
removeEntry(toEvict);
}
mostRecentTrimFailed = false;
}
boolean removeEntry(Entry entry) throws IOException {
if (entry.currentEditor != null) {
//讓這個editor正常的結(jié)束
entry.currentEditor.detach(); // Prevent the edit from completing normally.
}
for (int i = 0; i < valueCount; i++) {
//刪除entry對應的clean文件
fileSystem.delete(entry.cleanFiles[i]);
//緩存大小減去entry的小小
size -= entry.lengths[i];
//設置entry的緩存為0
entry.lengths[i] = 0;
}
//計數(shù)器自加1
redundantOpCount++;
//在journalWriter添加一條刪除記錄
journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');
//linkedList刪除這個entry
lruEntries.remove(entry.key);
//如果需要重新構(gòu)建
if (journalRebuildRequired()) {
//開啟清理任務
executor.execute(cleanupRunnable);
}
return true;
}
看下cleanupRunnable對象,看他的run方法得知盒让,主要是調(diào)用了trimToSize()和rebuildJournal()兩個方法對緩存數(shù)據(jù)進行維護梅肤。rebuildJournal()前面已經(jīng)說過了,這里主要關注下trimToSize()方法邑茄,trimToSize()方法主要是遍歷lruEntries(注意:這個遍歷科室通過accessOrder來的姨蝴,也就是隱含了LRU這個算法),來一個一個移除entry直到size小于maxSize肺缕,而removeEntry操作就是講editor里的diryFile以及cleanFiles進行刪除就是左医,并且要向journal文件里寫入REMOVE操作,以及刪除lruEntrie里面的對象同木。
cleanup主要是用來調(diào)整整個cache的大小浮梢,以防止它過大,同時也能用來rebuildJournal彤路,如果trim或者rebuild不成功秕硝,那之前edit里面也是沒有辦法獲取Editor來進行數(shù)據(jù)修改操作的。
下面來看下boolean remove(String key)方法
/**
* Drops the entry for {@code key} if it exists and can be removed. If the entry for {@code key}
* is currently being edited, that edit will complete normally but its value will not be stored.
*根據(jù)key來刪除對應的entry洲尊,如果entry存在則將會被刪除远豺,如果這個entry正在被編輯,編輯將被正常結(jié)束坞嘀,但是編輯的內(nèi)容不會保存
* @return true if an entry was removed.
*/
public synchronized boolean remove(String key) throws IOException {
//初始化
initialize();
//檢查是否被關閉
checkNotClosed();
//key是否符合要求
validateKey(key);
//根據(jù)key來獲取Entry
Entry entry = lruEntries.get(key);
//如果entry躯护,返回false表示刪除失敗
if (entry == null) return false;
//然后刪除這個entry
boolean removed = removeEntry(entry);
//如果刪除成功且緩存大小小于最大值,則設置最近清理標志位
if (removed && size <= maxSize) mostRecentTrimFailed = false;
return removed;
}
這這部分很簡單姆吭,就是先做判斷榛做,然后通過key獲取Entry,然后刪除entry
那我們繼續(xù)内狸,來看下DiskLruCache.edit(String)检眯;方法
/**
* Returns an editor for the entry named {@code key}, or null if another edit is in progress.
* 返回一entry的編輯器,如果其他正在編輯昆淡,則返回null
* 我的理解是根據(jù)key找entry锰瘸,然后根據(jù)entry找他的編輯器
*/
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
//初始化
initialize();
//流關閉檢測
checkNotClosed();
//檢測key
validateKey(key);
//根據(jù)key找到Entry
Entry entry = lruEntries.get(key);
//如果快照是舊的
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Snapshot is stale.
}
//如果 entry.currentEditor != null 表明正在編輯,是DIRTY
if (entry != null && entry.currentEditor != null) {
return null; // Another edit is in progress.
}
//如果最近清理失敗昂灵,或者最近重新構(gòu)建失敗避凝,我們需要開始清理任務
//我大概翻譯下注釋:操作系統(tǒng)已經(jīng)成為我們的敵人舞萄,如果清理任務失敗,它意味著我們存儲了過多的數(shù)據(jù)管削,因此我們允許超過這個限制倒脓,所以不建議編輯。如果構(gòu)建日志失敗含思,writer這個寫入流就會無效崎弃,所以文件無法及時更新,導致我們無法繼續(xù)編輯含潘,會引起文件泄露饲做。如果滿足以上兩種情況,我們必須進行清理遏弱,擺脫這種不好的狀態(tài)盆均。
if (mostRecentTrimFailed || mostRecentRebuildFailed) {
// The OS has become our enemy! If the trim job failed, it means we are storing more data than
// requested by the user. Do not allow edits so we do not go over that limit any further. If
// the journal rebuild failed, the journal writer will not be active, meaning we will not be
// able to record the edit, causing file leaks. In both cases, we want to retry the clean up
// so we can get out of this state!
//開啟清理任務
executor.execute(cleanupRunnable);
return null;
}
// Flush the journal before creating files to prevent file leaks.
//寫入DIRTY
journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
journalWriter.flush();
//如果journal有錯誤,表示不能編輯漱逸,返回null
if (hasJournalErrors) {
return null; // Don't edit; the journal can't be written.
}
//如果entry==null泪姨,則new一個,并放入lruEntries
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
//根據(jù)entry 構(gòu)造一個Editor
Editor editor = new Editor(entry);
entry.currentEditor = editor;
return editor;
}
上面代碼注釋說的很清楚虹脯,這里就提幾個注意事項
注意事項:
(1)如果已經(jīng)有個別的editor在操作這個entry了驴娃,那就返回null
(2)無時無刻不在進行cleanup判斷進行cleanup操作
(3)會把當前的key在journal文件標記為dirty狀態(tài),表示這條記錄正在被編輯
(4)如果沒有entry循集,會new一個出來
這個方法已經(jīng)結(jié)束了唇敞,那我們來看下 在Editor內(nèi)部類commit()方法里面調(diào)用的completeEdit(Editor,success)方法
synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
//如果entry的編輯器不是editor則拋異常
if (entry.currentEditor != editor) {
throw new IllegalStateException();
}
// If this edit is creating the entry for the first time, every index must have a value.
//如果successs是true,且entry不可讀表明 是第一次寫回,必須保證每個index里面要有數(shù)據(jù)咒彤,這是為了保證完整性
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;
}
}
}
//遍歷entry下的所有文件
for (int i = 0; i < valueCount; i++) {
File dirty = entry.dirtyFiles[i];
if (success) {
//把dirtyFile重命名為cleanFile疆柔,完成數(shù)據(jù)遷移;
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 {
//刪除dirty數(shù)據(jù)
fileSystem.delete(dirty);
}
}
//計數(shù)器加1
redundantOpCount++;
//編輯器指向null
entry.currentEditor = null;
if (entry.readable | success) {
//開始寫入數(shù)據(jù)
entry.readable = true;
journalWriter.writeUtf8(CLEAN).writeByte(' ');
journalWriter.writeUtf8(entry.key);
entry.writeLengths(journalWriter);
journalWriter.writeByte('\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
//刪除key,并且記錄
lruEntries.remove(entry.key);
journalWriter.writeUtf8(REMOVE).writeByte(' ');
journalWriter.writeUtf8(entry.key);
journalWriter.writeByte('\n');
}
journalWriter.flush();
//檢查是否需要清理
if (size > maxSize || journalRebuildRequired()) {
executor.execute(cleanupRunnable);
}
}
這樣下來镶柱,數(shù)據(jù)都寫入cleanFile了旷档,currentEditor也重新設為null,表明commit徹底結(jié)束了歇拆。
總結(jié)起來DiskLruCache主要的特點:
- 1鞋屈、通過LinkedHashMap實現(xiàn)LRU替換
- 2、通過本地維護Cache操作日志保證Cache原子性與可用性故觅,同時為防止日志過分膨脹定時執(zhí)行日志精簡厂庇。
- 3、 每一個Cache項對應兩個狀態(tài)副本:DIRTY输吏,CLEAN权旷。CLEAN表示當前可用的Cache。外部訪問到cache快照均為CLEAN狀態(tài)贯溅;DIRTY為編輯狀態(tài)的cache拄氯。由于更新和創(chuàng)新都只操作DIRTY狀態(tài)的副本躲查,實現(xiàn)了讀和寫的分離。
- 4译柏、每一個url請求cache有四個文件镣煮,兩個狀態(tài)(DIRY,CLEAN)鄙麦,每個狀態(tài)對應兩個文件:一個0文件對應存儲meta數(shù)據(jù)怎静,一個文件存儲body數(shù)據(jù)。
至此所有的關于緩存的相關類都介紹完畢黔衡,為了幫助大家更好的理解緩存,咱們在重新看下CacheInterceptor里面執(zhí)行的流程
三.OKHTTP的緩存的實現(xiàn)---CacheInterceptor的具體執(zhí)行流程
(一)原理和注意事項:
1腌乡、原理
(1)盟劫、okhttp的網(wǎng)絡緩存是基于http協(xié)議,不清楚請仔細看上一篇文章
(2)与纽、使用DiskLruCache的緩存策略侣签,具體請看本片文章的第一章節(jié)
2、注意事項:
1急迂、目前只支持GET影所,其他請求方式需要自己實現(xiàn)。
2僚碎、需要服務器配合猴娩,通過head設置相關頭來控制緩存
3、創(chuàng)建OkHttpClient時候需要配置Cache
(二)流程:
1勺阐、如果配置了緩存卷中,則從緩存中取出(可能為null)
2、獲取緩存的策略.
3渊抽、監(jiān)測緩存
4蟆豫、如果禁止使用網(wǎng)絡(比如飛行模式),且緩存無效,直接返回
5懒闷、如果緩存有效十减,使用網(wǎng)絡,不使用網(wǎng)絡
6愤估、如果緩存無效帮辟,執(zhí)行下一個攔截器
7、本地有緩存灵疮、根據(jù)條件判斷是使用緩存還是使用網(wǎng)絡的response
8织阅、把response緩存到本地
(三)源碼對比:
@Override public Response intercept(Chain chain) throws IOException {
//1、如果配置了緩存震捣,則從緩存中取出(可能為null)
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
//2荔棉、獲取緩存的策略.
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
//3闹炉、監(jiān)測緩存
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// If we're forbidden from using the network and the cache is insufficient, fail.
//4、如果禁止使用網(wǎng)絡(比如飛行模式),且緩存無效润樱,直接返回
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();
}
//5、如果緩存有效壹若,使用網(wǎng)絡,不使用網(wǎng)絡
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
//6店展、如果緩存無效,執(zhí)行下一個攔截器
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
//7柳弄、本地有緩存概说、根據(jù)條件判斷是使用緩存還是使用網(wǎng)絡的response
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
//這個response是用來返回的
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
//8糖赔、把response緩存到本地
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
(四)倒序具體分析:
1、什么是“倒序具體分析”放典?
這里的倒序具體分析是指先分析緩存逝变,在分析使用緩存,因為第一次使用的時候奋构,肯定沒有緩存骨田,所以肯定先發(fā)起請求request,然后收到響應response的時候声怔,緩存起來态贤,等下次調(diào)用的時候,才具體獲取緩存策略醋火。
PS:由于涉及到的類全部講過了一遍了悠汽,下面涉及的代碼就不全部粘貼了,只贊貼核心代碼了芥驳。
2柿冲、先分析獲取響應response的流程,保存的流程是如下
在CacheInterceptor的代碼是
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
}
核心代碼是CacheRequest cacheRequest = cache.put(response);
cache就是咱們設置的Cache對象,put(reponse)方法就是調(diào)用Cache類的put方法
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
先是 用resonse作為參數(shù)來構(gòu)造Cache.Entry對象兆旬,這里強烈提示下假抄,是Cache.Entry對象,不是DiskLruCache.Entry對象。 然后 調(diào)用的是DiskLruCache類的edit(String key)方法宿饱,而DiskLruCache類的edit(String key)方法調(diào)用的是DiskLruCache類的edit(String key, long expectedSequenceNumber)方法,在DiskLruCache類的edit(String key, long expectedSequenceNumber)方法里面其實是通過lruEntries的 lruEntries.get(key)方法獲取的DiskLruCache.Entry對象熏瞄,然后通過這個DiskLruCache.Entry獲取對應的編輯器,獲取到編輯器后, 再次這個編輯器(editor)通過okio把Cache.Entry寫入這個編輯器(editor)對應的文件上谬以。注意强饮,這里是寫入的是http中的header的內(nèi)容 ,最后 返回一個CacheRequestImpl對象
緊接著又調(diào)用了 CacheInterceptor.cacheWritingResponse(CacheRequest, Response)方法
主要就是通過配置好的cache寫入緩存为黎,都是通過Cache和DiskLruCache來具體實現(xiàn)
總結(jié):緩存實際上是一個比較復雜的邏輯邮丰,單獨的功能塊,實際上不屬于OKhttp上的功能铭乾,實際上是通過是http協(xié)議和DiskLruCache做了處理炕檩。
LinkedHashMap可以實現(xiàn)LRU算法,并且在這個case里骤星,它被用作對DiskCache的內(nèi)存索引
告訴你們一個秘密舆吮,Universal-Imager-Loader里面的DiskLruCache的實現(xiàn)跟這里的一模一樣色冀,除了io使用inputstream/outputstream
使用LinkedHashMap和journal文件同時記錄做過的操作锋恬,其實也就是有索引了,這樣就相當于有兩個備份索守,可以互相恢復狀態(tài)
通過dirtyFiles和cleanFiles卵佛,可以實現(xiàn)更新和讀取同時操作疾牲,在commit的時候?qū)leanFiles的內(nèi)容進行更新就好了