- 1.OkHttp源碼解析(一):OKHttp初階
- 2 OkHttp源碼解析(二):OkHttp連接的"前戲"——HTTP的那些事
- 3 OkHttp源碼解析(三):OKHttp中階之線程池和消息隊(duì)列
- 4 OkHttp源碼解析(四):OKHttp中階之?dāng)r截器及調(diào)用鏈
- 5 OkHttp源碼解析(五):OKHttp中階之OKio簡(jiǎn)介
- 6 OkHttp源碼解析(六):OKHttp中階之緩存基礎(chǔ)
- 7 OkHttp源碼解析(七):OKHttp中階之緩存機(jī)制
- 8 OkHttp源碼解析(八):OKHttp中階之連接與請(qǐng)求值前奏
- 9 OkHttp源碼解析(九):OKHTTP連接中三個(gè)"核心"RealConnection、ConnectionPool、StreamAllocation
- 10 OkHttp源碼解析(十) OKHTTP中連接與請(qǐng)求
- 11 OkHttp的感謝
上一章主要講解了HTTP中的緩存以及OKHTTP中的緩存,今天我們主要講解OKHTTP中緩存體系的精髓---DiskLruCache,由于篇幅限制,今天內(nèi)容看似不多,大概分為兩個(gè)部分
1.DiskLruCache內(nèi)部類詳解
2.DiskLruCache類詳解
3.OKHTTP的緩存的實(shí)現(xiàn)---CacheInterceptor的具體執(zhí)行流程
一、DiskLruCache
在看DiskLruCache前先看下他的幾個(gè)內(nèi)部類
1、Entry.class(DiskLruCache的內(nèi)部類)
Entry內(nèi)部類是實(shí)際用于存儲(chǔ)的緩存數(shù)據(jù)的實(shí)體類,每一個(gè)url對(duì)應(yīng)一個(gè)Entry實(shí)體
private final class Entry {
final String key;
/** 實(shí)體對(duì)應(yīng)的緩存文件 */
/** Lengths of this entry's files. */
final long[] lengths; //文件比特?cái)?shù)
final File[] cleanFiles;
final File[] dirtyFiles;
/** 實(shí)體是否可讀,可讀為true,不可讀為false*/
/** True if this entry has ever been published. */
boolean readable;
/** 編輯器,如果實(shí)體沒(méi)有被編輯過(guò),則為null*/
/** The ongoing edit or null if this entry is not being edited. */
Editor currentEditor;
/** 最近提交的Entry的序列號(hào) */
/** The sequence number of the most recently committed edit to this entry. */
long sequenceNumber;
//構(gòu)造器 就一個(gè)入?yún)?key,而key又是url,所以,一個(gè)url對(duì)應(yīng)一個(gè)Entry
Entry(String key) {
this.key = key;
//valueCount在構(gòu)造DiskLruCache時(shí)傳入的參數(shù)默認(rèn)大小為2
//具體請(qǐng)看Cache類的構(gòu)造函數(shù),里面通過(guò)DiskLruCache.create()方法創(chuàng)建了DiskLruCache,并且傳入一個(gè)值為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);
}
}
通過(guò)上述代碼咱們知道了,一個(gè)url對(duì)應(yīng)一個(gè)Entry對(duì)象,同時(shí),每個(gè)Entry對(duì)應(yīng)兩個(gè)文件,key.1存儲(chǔ)的是Response的headers,key.2文件存儲(chǔ)的是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; //也有一個(gè)key
private final long sequenceNumber; //序列號(hào)
private final Source[] sources; //可以讀入數(shù)據(jù)的流 這么多的流主要是從cleanFile中讀取數(shù)據(jù)
private final long[] lengths; //與上面的流一一對(duì)應(yīng)
//構(gòu)造器就是對(duì)上面這些屬性進(jìn)行賦值
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對(duì)象的兩個(gè)屬性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);
}
}
}
這時(shí)候再回來(lái)看下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對(duì)象的鎖
if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();
//new了一個(gè)Souce類型數(shù)組,容量為2
Source[] sources = new Source[valueCount];
//clone一個(gè)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)造一個(gè)Snapshot對(duì)象
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都是一個(gè)entry,其實(shí)也就可以說(shuō)一個(gè)Entry對(duì)象一一對(duì)應(yīng)一個(gè)Snapshot對(duì)象
3、Editor.class(DiskLruCache的內(nèi)部類)
Editro類的屬性和構(gòu)造器貌似看不到什么東西,不過(guò)通過(guò)構(gòu)造器,我們知道,在構(gòu)造一個(gè)Editor的時(shí)候必須傳入一個(gè)Entry,莫非Editor是對(duì)這個(gè)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.
*這里說(shuō)一下detach方法,當(dāng)編輯器(Editor)處于io操作的error的時(shí)候,或者editor正在被調(diào)用的時(shí)候而被清
*除的,為了防止編輯器可以正常的完成。我們需要?jiǎng)h除編輯器創(chuàng)建的文件,并防止創(chuàng)建新的文件。如果編
*輯器被分離,其他的編輯器可以編輯這個(gè)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的時(shí)候把done設(shè)為true
*/
public Source newSource(int index) {
synchronized (DiskLruCache.this) {
//如果已經(jīng)commit了,不能讀取了
if (done) {
throw new IllegalStateException();
}
//如果entry不可讀,并且已經(jīng)有編輯器了(其實(shí)就是dirty)
if (!entry.readable || entry.currentEditor != this) {
return null;
}
try {
//通過(guò)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ù)的時(shí)候出現(xiàn)錯(cuò)誤,會(huì)立即停止。返回的輸出流不會(huì)拋IO異常
*/
public Sink newSink(int index) {
synchronized (DiskLruCache.this) {
//已經(jīng)提交,不能操作
if (done) {
throw new IllegalStateException();
}
//如果編輯器是不自己的,不能操作
if (entry.currentEditor != this) {
return Okio.blackhole();
}
//如果entry不可讀,把對(duì)應(yīng)的written設(shè)為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操作對(duì)數(shù)據(jù)進(jìn)行提交,我們要把dirtyFiles里面的內(nèi)容移動(dòng)到cleanFiles里才能夠讓別的editor訪問(wèn)到
*/
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) {
//這個(gè)方法是DiskLruCache的方法在后面講解
completeEdit(this, false);
}
done = true;
}
}
public void abortUnlessCommitted() {
synchronized (DiskLruCache.this) {
if (!done && entry.currentEditor == this) {
try {
completeEdit(this, false);
} catch (IOException ignored) {
}
}
}
}
}
哎,看到這個(gè)了類的注釋,發(fā)現(xiàn)Editor的確就是編輯entry類的。
Editor里面的幾個(gè)方法Source newSource(int index) ,Sink newSink(int index),commit(),abort(),abortUnlessCommitted() ,既然是編輯器,我們看到上面的方法應(yīng)該可以猜到,上面的方法一次對(duì)應(yīng)如下
| 方法 | 意義 |
|---|---|
| Source newSource(int index) | 返回指定index的cleanFile的讀入流 |
| Sink newSink(int index) | 向指定index的dirtyFiles文件寫入數(shù)據(jù) |
| commit() | 這里執(zhí)行的工作是提交數(shù)據(jù),并釋放鎖,最后通知DiskLruCache刷新相關(guān)數(shù)據(jù) |
| abort() | 終止編輯,并釋放鎖 |
| abortUnlessCommitted() | 除非正在編輯,否則終止 |
abort()和abortUnlessCommitted()最后都會(huì)執(zhí)行completeEdit(Editor, boolean) 這個(gè)方法這里簡(jiǎn)單說(shuō)下:
success情況提交:dirty文件會(huì)被更名為clean文件,entry.lengths[i]值會(huì)被更新,DiskLruCache,size會(huì)更新(DiskLruCache,size代表的是所有整個(gè)緩存文件加起來(lái)的總大?。?,redundantOpCount++,在日志中寫入一條Clean信息
failed情況:dirty文件被刪除,redundantOpCount++,日志中寫入一條REMOVE信息
至此DiskLruCache的內(nèi)部類就全部介紹結(jié)束了。現(xiàn)在咱們正式關(guān)注下DiskLruCache類
二、DiskLruCache類詳解
(一)、重要屬性
DiskLruCache里面有一個(gè)屬性是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)屬性,詳情請(qǐng)看LinkedHashMap源碼說(shuō)明
DiskLruCache也有一個(gè)線程池屬性 executor,不過(guò)該池最多有一個(gè)線程工作,用于清理,維護(hù)緩存數(shù)據(jù)。創(chuàng)建一個(gè)DiskLruCache對(duì)象的方法是調(diào)用該方法,而不是直接調(diào)用構(gòu)造器。
(二)、構(gòu)造函數(shù)和創(chuàng)建對(duì)象
DiskLruCache有一個(gè)構(gòu)造函數(shù),但是不是public的所以DiskLruCache只能被包內(nèi)中類調(diào)用,不能在外面直接new。不過(guò)DiskLruCache提供了一個(gè)靜態(tài)方法create,對(duì)外提供DiskLruCache對(duì)象
//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");
}
//這個(gè)executor其實(shí)就是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)造器會(huì)在制定的目錄下創(chuàng)建三份文件,這三個(gè)文件是DiskLruCache的工作日志文件。在執(zhí)行DiskLruCache的任何方法之前都會(huì)執(zhí)行initialize()方法來(lái)完成DiskLruCache的初始化,有人會(huì)想為什么不在DiskLruCache的構(gòu)造器中完成對(duì)該方法的調(diào)用,其實(shí)是為了延遲初始化,因?yàn)槌跏蓟瘯?huì)創(chuàng)建一系列的文件和對(duì)象,所以做了延遲初始化。
(三)、初始化
那么來(lái)看下initialize里面的代碼
public synchronized void initialize() throws IOException {
//斷言,當(dāng)持有自己鎖的時(shí)候。繼續(xù)執(zhí)行,沒(méi)有持有鎖,直接拋異常
assert Thread.holdsLock(this);
//如果已經(jīng)初始化過(guò),則不需要再初始化,直接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 {
//沒(méi)有journalFile,則將journalFileBackUp更名為journalFile
fileSystem.rename(journalFileBackup, journalFile);
}
}
// Prefer to pick up where we left off.
if (fileSystem.exists(journalFile)) {
//如果有journalFile文件,則對(duì)該文件,則分別調(diào)用readJournal()方法和processJournal()方法
try {
readJournal();
processJournal();
//設(shè)置初始化過(guò)標(biāo)志
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 {
//如果沒(méi)有journalFile則刪除
delete();
} finally {
closed = false;
}
}
//重新建立journal文件
rebuildJournal();
initialized = true;
}
大家發(fā)現(xiàn)沒(méi)有,如論是否有journal文件,最后都會(huì)將initialized設(shè)為true,該值不會(huì)再被設(shè)置為false,除非DiskLruCache對(duì)象唄銷毀。這表明initialize()放啊在DiskLruCache對(duì)象的整個(gè)生命周期中只會(huì)執(zhí)行一次。該動(dòng)作完成日志的寫入和lruEntries集合的初始化。
這里面分別調(diào)用了readJournal()方法和processJournal()方法,那咱們依次分析下這兩個(gè)方法,這里面有大量的okio里面的代碼,如果大家對(duì)okio不熟悉能讀上一篇文章。
private void readJournal() throws IOException {
//獲取journalFile的source即輸入流
BufferedSource source = Okio.buffer(fileSystem.source(journalFile));
try {
//讀取相關(guān)數(shù)據(jù)
String magic = source.readUtf8LineStrict();
String version = source.readUtf8LineStrict();
String appVersionString = source.readUtf8LineStrict();
String valueCountString = source.readUtf8LineStrict();
String blank = source.readUtf8LineStrict();
//做校驗(yàn)
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;
//校驗(yàn)通過(guò),開始逐行讀取數(shù)據(jù)
while (true) {
try {
readJournalLine(source.readUtf8LineStrict());
lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
//讀取出來(lái)的行數(shù)減去lruEntriest的集合的差值,即日志多出的"冗余"記錄
redundantOpCount = lineCount - lruEntries.size();
// If we ended on a truncated line, rebuild the journal before appending to it.
//source.exhausted()表示是否還多余字節(jié),如果沒(méi)有多余字節(jié),返回true,有多月字節(jié)返回false
if (!source.exhausted()) {
//如果有多余字節(jié),則重新構(gòu)建下journal文件,主要是寫入頭文件,以便下次讀的時(shí)候,根據(jù)頭文件進(jìn)行校驗(yàn)
rebuildJournal();
} else {
//獲取這個(gè)文件的Sink
journalWriter = newJournalWriter();
}
} finally {
Util.closeQuietly(source);
}
}
這里說(shuō)一下ource.readUtf8LineStrict()方法,這個(gè)方法是BufferedSource接口的方法,具體實(shí)現(xiàn)是RealBufferedSource,所以大家要去RealBufferedSource里面去找具體實(shí)現(xiàn)。我這里簡(jiǎn)單說(shuō)下,就是從source里面按照utf-8編碼取出一行的數(shù)據(jù)。這里面讀取了magic,version,appVersionString,valueCountString,blank,然后進(jìn)行校驗(yàn),這個(gè)數(shù)據(jù)是在"寫"的時(shí)候,寫入的,具體情況看DiskLruCache的rebuildJournal()方法。隨后記錄redundantOpCount的值,該值的含義就是判斷當(dāng)前日志中記錄的行數(shù)和lruEntries集合容量的差值,即日志中多出來(lái)的"冗余"記錄。
讀取的時(shí)候又調(diào)用了readJournalLine()方法,咱們來(lái)研究下這個(gè)方法
private void readJournalLine(String line) throws IOException {
獲取空串的position,表示頭
int firstSpace = line.indexOf(' ');
//空串的校驗(yàn)
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
//第一個(gè)字符的位置
int keyBegin = firstSpace + 1;
// 方法返回第一個(gè)空字符在此字符串中第一次出現(xiàn),在指定的索引即keyBegin開始搜索,所以secondSpace是愛這個(gè)字符串中的空字符(不包括這一行最左側(cè)的那個(gè)空字符)
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
//如果沒(méi)有中間的空字符
if (secondSpace == -1) {
//截取剩下的全部字符串構(gòu)成key
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
//如果解析的是REMOVE信息,則在lruEntries里面刪除這個(gè)key
lruEntries.remove(key);
return;
}
} else {
//如果含有中間間隔的空字符,則截取這個(gè)中間間隔到左側(cè)空字符之間的字符串,構(gòu)成key
key = line.substring(keyBegin, secondSpace);
}
//獲取key后,根據(jù)key取出Entry對(duì)象
Entry entry = lruEntries.get(key);
//如果Entry為null,則表明內(nèi)存中沒(méi)有,則new一個(gè),并把它放到內(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)容,然后按照空字符分割,設(shè)置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開頭,則設(shè)置一個(gè)新的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)建一個(gè)entry,如果是判斷信息是CLEAN則設(shè)置ENTRY為可讀,并設(shè)置entry.currentEditor表明當(dāng)前Entry不可編輯,調(diào)用entry.setLengths(String[]),設(shè)置該entry.lengths的初始值。如果判斷是Dirty則設(shè)置enry.currentEdtor=new Editor(entry);表明當(dāng)前Entry處于被編輯狀態(tài)。
通過(guò)上面我得到了如下的結(jié)論:
- 1、如果是CLEAN的話,對(duì)這個(gè)entry的文件長(zhǎng)度進(jìn)行更新
- 2、如果是DIRTY,說(shuō)明這個(gè)值正在被操作,還沒(méi)有commit,于是給entry分配一個(gè)Editor。
- 3、如果是READ,說(shuō)明這個(gè)值被讀過(guò)了,什么也不做。
看下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()方法,那我們來(lái)看下:
/**
* 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ù)時(shí)臟的DIRTY,所以不能讀,進(jìn)而刪除Entry下的緩存文件,并且將Entry從lruEntries中移除。如果Entry的Editor==null,則證明該Entry下的緩存文件可用,記錄它所有緩存文件的緩存數(shù)量,結(jié)果賦值給size。
readJournal()方法里面調(diào)用了rebuildJournal(),initialize()方法同樣會(huì)readJourna,但是這里說(shuō)明下:readJournal里面調(diào)用的rebuildJournal()是有條件限制的,initialize()是一定會(huì)調(diào)用的。那我們來(lái)研究下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) {
//關(guān)閉寫入流
journalWriter.close();
}
//通過(guò)okio獲取一個(gè)寫入BufferedSinke
BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp));
try {
//寫入相關(guān)信息和讀取向?qū)?yīng),這時(shí)候大家想下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é)尾接上兩個(gè)十進(jìn)制的數(shù)字,表示長(zhǎng)度
entry.writeLengths(writer);
//換行
writer.writeByte('\n');
}
}
} finally {
//最后關(guān)閉寫入流
writer.close();
}
//如果存在journalFile
if (fileSystem.exists(journalFile)) {
//把journalFile文件重命名為journalFileBackup
fileSystem.rename(journalFile, journalFileBackup);
}
然后又把臨時(shí)文件,重命名為journalFile
fileSystem.rename(journalFileTmp, journalFile);
//刪除備份文件
fileSystem.delete(journalFileBackup);
//拼接一個(gè)新的寫入流
journalWriter = newJournalWriter();
//設(shè)置沒(méi)有error標(biāo)志
hasJournalErrors = false;
//設(shè)置最近重新創(chuàng)建journal文件成功
mostRecentRebuildFailed = false;
}
總結(jié)下:
獲取一個(gè)寫入流,將lruEntries集合中的Entry對(duì)象寫入tmp文件中,根據(jù)Entry的currentEditor的值判斷是CLEAN還是DIRTY,寫入該Entry的key,如果是CLEAN還要寫入文件的大小bytes。然后就是把journalFileTmp更名為journalFile,然后將journalWriter跟文件綁定,通過(guò)它來(lái)向journalWrite寫入數(shù)據(jù),最后設(shè)置一些屬性。
我們可以砍到,rebuild操作是以lruEntries為準(zhǔn),把DIRTY和CLEAN的操作都寫回到j(luò)ournal中。但發(fā)現(xiàn)沒(méi)有,其實(shí)沒(méi)有改動(dòng)真正的value,只不過(guò)重寫了一些事務(wù)的記錄。事實(shí)上,lruEntries和journal文件共同確定了cache數(shù)據(jù)的有效性。lruEntries是索引,journal是歸檔。至此序列化部分就已經(jīng)結(jié)束了
(四)、關(guān)于Cache類調(diào)用的幾個(gè)方法
上回書說(shuō)道Cache調(diào)用DiskCache的幾個(gè)方法,如下:
- 1.DiskLruCache.get(String)獲取DiskLruCache.Snapshot
- 2.DiskLruCache.remove(String)移除請(qǐng)求
- 3.DiskLruCache.edit(String);獲得一個(gè)DiskLruCache.Editor對(duì)象,
- 4.DiskLruCache.Editor.newSink(int);獲得一個(gè)sink流 (具體看Editor類)
- 5.DiskLruCache.Snapshot.getSource(int);獲取一個(gè)Source對(duì)象。 (具體看Editor類)
- 6.DiskLruCache.Snapshot.edit();獲得一個(gè)DiskLruCache.Editor對(duì)象,
1、DiskLruCache.Snapshot get(String)方法
public synchronized Snapshot get(String key) throws IOException {
//初始化
initialize();
//檢查緩存是否已經(jīng)關(guān)閉
checkNotClosed();
//檢驗(yàn)key
validateKey(key);
//如果以上都通過(guò),先獲取內(nèi)存中的數(shù)據(jù),即根據(jù)key在linkedList查找
Entry entry = lruEntries.get(key);
//如果沒(méi)有值,或者有值,但是值不可讀
if (entry == null || !entry.readable) return null;
//獲取entry里面的snapshot的值
Snapshot snapshot = entry.snapshot();
//如果有snapshot為null,則直接返回null
if (snapshot == null) return null;
//如果snapshot不為null
//計(jì)數(shù)器自加1
redundantOpCount++;
//把這個(gè)內(nèi)容寫入文檔中
journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
//如果超過(guò)上限
if (journalRebuildRequired()) {
//開始清理
executor.execute(cleanupRunnable);
}
//返回?cái)?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() {
//最大計(jì)數(shù)單位
final int redundantOpCompactThreshold = 2000;
//清理的條件
return redundantOpCount >= redundantOpCompactThreshold
&& redundantOpCount >= lruEntries.size();
}
主要就是先去拿snapshot,然后會(huì)用journalWriter向journal寫入一條read記錄,最后判斷是否需要清理。
清理的條件是當(dāng)前redundantOpCount大于2000,并且redundantOpCount的值大于linkedList里面的size。咱們接著看下清理任務(wù)
private final Runnable cleanupRunnable = new Runnable() {
public void run() {
synchronized (DiskLruCache.this) {
//如果沒(méi)有初始化或者已經(jīng)關(guān)閉了,則不需要清理
if (!initialized | closed) {
return; // Nothing to do
}
try {
trimToSize();
} catch (IOException ignored) {
//如果拋異常了,設(shè)置最近的一次清理失敗
mostRecentTrimFailed = true;
}
try {
//如果需要清理了
if (journalRebuildRequired()) {
//重新創(chuàng)建journal文件
rebuildJournal();
//計(jì)數(shù)器歸于0
redundantOpCount = 0;
}
} catch (IOException e) {
//如果拋異常了,設(shè)置最近的一次構(gòu)建失敗
mostRecentRebuildFailed = true;
journalWriter = Okio.buffer(Okio.blackhole());
}
}
}
};
void trimToSize() throws IOException {
//如果超過(guò)上限
while (size > maxSize) {
//取出一個(gè)Entry
Entry toEvict = lruEntries.values().iterator().next();
//刪除這個(gè)Entry
removeEntry(toEvict);
}
mostRecentTrimFailed = false;
}
boolean removeEntry(Entry entry) throws IOException {
if (entry.currentEditor != null) {
//讓這個(gè)editor正常的結(jié)束
entry.currentEditor.detach(); // Prevent the edit from completing normally.
}
for (int i = 0; i < valueCount; i++) {
//刪除entry對(duì)應(yīng)的clean文件
fileSystem.delete(entry.cleanFiles[i]);
//緩存大小減去entry的小小
size -= entry.lengths[i];
//設(shè)置entry的緩存為0
entry.lengths[i] = 0;
}
//計(jì)數(shù)器自加1
redundantOpCount++;
//在journalWriter添加一條刪除記錄
journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');
//linkedList刪除這個(gè)entry
lruEntries.remove(entry.key);
//如果需要重新構(gòu)建
if (journalRebuildRequired()) {
//開啟清理任務(wù)
executor.execute(cleanupRunnable);
}
return true;
}
看下cleanupRunnable對(duì)象,看他的run方法得知,主要是調(diào)用了trimToSize()和rebuildJournal()兩個(gè)方法對(duì)緩存數(shù)據(jù)進(jìn)行維護(hù)。rebuildJournal()前面已經(jīng)說(shuō)過(guò)了,這里主要關(guān)注下trimToSize()方法,trimToSize()方法主要是遍歷lruEntries(注意:這個(gè)遍歷科室通過(guò)accessOrder來(lái)的,也就是隱含了LRU這個(gè)算法),來(lái)一個(gè)一個(gè)移除entry直到size小于maxSize,而removeEntry操作就是講editor里的diryFile以及cleanFiles進(jìn)行刪除就是,并且要向journal文件里寫入REMOVE操作,以及刪除lruEntrie里面的對(duì)象。
cleanup主要是用來(lái)調(diào)整整個(gè)cache的大小,以防止它過(guò)大,同時(shí)也能用來(lái)rebuildJournal,如果trim或者rebuild不成功,那之前edit里面也是沒(méi)有辦法獲取Editor來(lái)進(jìn)行數(shù)據(jù)修改操作的。
下面來(lái)看下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來(lái)刪除對(duì)應(yīng)的entry,如果entry存在則將會(huì)被刪除,如果這個(gè)entry正在被編輯,編輯將被正常結(jié)束,但是編輯的內(nèi)容不會(huì)保存
* @return true if an entry was removed.
*/
public synchronized boolean remove(String key) throws IOException {
//初始化
initialize();
//檢查是否被關(guān)閉
checkNotClosed();
//key是否符合要求
validateKey(key);
//根據(jù)key來(lái)獲取Entry
Entry entry = lruEntries.get(key);
//如果entry,返回false表示刪除失敗
if (entry == null) return false;
//然后刪除這個(gè)entry
boolean removed = removeEntry(entry);
//如果刪除成功且緩存大小小于最大值,則設(shè)置最近清理標(biāo)志位
if (removed && size <= maxSize) mostRecentTrimFailed = false;
return removed;
}
這這部分很簡(jiǎn)單,就是先做判斷,然后通過(guò)key獲取Entry,然后刪除entry
那我們繼續(xù),來(lái)看下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();
//流關(guān)閉檢測(cè)
checkNotClosed();
//檢測(cè)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)建失敗,我們需要開始清理任務(wù)
//我大概翻譯下注釋:操作系統(tǒng)已經(jīng)成為我們的敵人,如果清理任務(wù)失敗,它意味著我們存儲(chǔ)了過(guò)多的數(shù)據(jù),因此我們?cè)试S超過(guò)這個(gè)限制,所以不建議編輯。如果構(gòu)建日志失敗,writer這個(gè)寫入流就會(huì)無(wú)效,所以文件無(wú)法及時(shí)更新,導(dǎo)致我們無(wú)法繼續(xù)編輯,會(huì)引起文件泄露。如果滿足以上兩種情況,我們必須進(jìn)行清理,擺脫這種不好的狀態(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!
//開啟清理任務(wù)
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有錯(cuò)誤,表示不能編輯,返回null
if (hasJournalErrors) {
return null; // Don't edit; the journal can't be written.
}
//如果entry==null,則new一個(gè),并放入lruEntries
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
//根據(jù)entry 構(gòu)造一個(gè)Editor
Editor editor = new Editor(entry);
entry.currentEditor = editor;
return editor;
}
上面代碼注釋說(shuō)的很清楚,這里就提幾個(gè)注意事項(xiàng)
注意事項(xiàng):
(1)如果已經(jīng)有個(gè)別的editor在操作這個(gè)entry了,那就返回null
(2)無(wú)時(shí)無(wú)刻不在進(jìn)行cleanup判斷進(jìn)行cleanup操作
(3)會(huì)把當(dāng)前的key在journal文件標(biāo)記為dirty狀態(tài),表示這條記錄正在被編輯
(4)如果沒(méi)有entry,會(huì)new一個(gè)出來(lái)
這個(gè)方法已經(jīng)結(jié)束了,那我們來(lái)看下 在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不可讀表明 是第一次寫回,必須保證每個(gè)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);
}
}
//計(jì)數(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);
}
}
這樣下來(lái),數(shù)據(jù)都寫入cleanFile了,currentEditor也重新設(shè)為null,表明commit徹底結(jié)束了。
總結(jié)起來(lái)DiskLruCache主要的特點(diǎn):
- 1、通過(guò)LinkedHashMap實(shí)現(xiàn)LRU替換
- 2、通過(guò)本地維護(hù)Cache操作日志保證Cache原子性與可用性,同時(shí)為防止日志過(guò)分膨脹定時(shí)執(zhí)行日志精簡(jiǎn)。
- 3、 每一個(gè)Cache項(xiàng)對(duì)應(yīng)兩個(gè)狀態(tài)副本:DIRTY,CLEAN。CLEAN表示當(dāng)前可用的Cache。外部訪問(wèn)到cache快照均為CLEAN狀態(tài);DIRTY為編輯狀態(tài)的cache。由于更新和創(chuàng)新都只操作DIRTY狀態(tài)的副本,實(shí)現(xiàn)了讀和寫的分離。
- 4、每一個(gè)url請(qǐng)求cache有四個(gè)文件,兩個(gè)狀態(tài)(DIRY,CLEAN),每個(gè)狀態(tài)對(duì)應(yīng)兩個(gè)文件:一個(gè)0文件對(duì)應(yīng)存儲(chǔ)meta數(shù)據(jù),一個(gè)文件存儲(chǔ)body數(shù)據(jù)。
至此所有的關(guān)于緩存的相關(guān)類都介紹完畢,為了幫助大家更好的理解緩存,咱們?cè)谥匦驴聪翪acheInterceptor里面執(zhí)行的流程
三.OKHTTP的緩存的實(shí)現(xiàn)---CacheInterceptor的具體執(zhí)行流程
(一)原理和注意事項(xiàng):
1、原理
(1)、okhttp的網(wǎng)絡(luò)緩存是基于http協(xié)議,不清楚請(qǐng)仔細(xì)看上一篇文章
(2)、使用DiskLruCache的緩存策略,具體請(qǐng)看本片文章的第一章節(jié)
2、注意事項(xiàng):
1、目前只支持GET,其他請(qǐng)求方式需要自己實(shí)現(xiàn)。
2、需要服務(wù)器配合,通過(guò)head設(shè)置相關(guān)頭來(lái)控制緩存
3、創(chuàng)建OkHttpClient時(shí)候需要配置Cache
(二)流程:
1、如果配置了緩存,則從緩存中取出(可能為null)
2、獲取緩存的策略.
3、監(jiān)測(cè)緩存
4、如果禁止使用網(wǎng)絡(luò)(比如飛行模式),且緩存無(wú)效,直接返回
5、如果緩存有效,使用網(wǎng)絡(luò),不使用網(wǎng)絡(luò)
6、如果緩存無(wú)效,執(zhí)行下一個(gè)攔截器
7、本地有緩存、根據(jù)條件判斷是使用緩存還是使用網(wǎng)絡(luò)的response
8、把response緩存到本地
(三)源碼對(duì)比:
@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)測(cè)緩存
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)絡(luò)(比如飛行模式),且緩存無(wú)效,直接返回
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)絡(luò),不使用網(wǎng)絡(luò)
// 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、如果緩存無(wú)效,執(zhí)行下一個(gè)攔截器
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)絡(luò)的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());
}
}
//這個(gè)response是用來(lái)返回的
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、什么是“倒序具體分析”?
這里的倒序具體分析是指先分析緩存,在分析使用緩存,因?yàn)榈谝淮问褂玫臅r(shí)候,肯定沒(méi)有緩存,所以肯定先發(fā)起請(qǐng)求request,然后收到響應(yīng)response的時(shí)候,緩存起來(lái),等下次調(diào)用的時(shí)候,才具體獲取緩存策略。
PS:由于涉及到的類全部講過(guò)了一遍了,下面涉及的代碼就不全部粘貼了,只贊貼核心代碼了。
2、先分析獲取響應(yīng)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就是咱們?cè)O(shè)置的Cache對(duì)象,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ù)來(lái)構(gòu)造Cache.Entry對(duì)象,這里強(qiáng)烈提示下,是Cache.Entry對(duì)象,不是DiskLruCache.Entry對(duì)象。 然后 調(diào)用的是DiskLruCache類的edit(String key)方法,而DiskLruCache類的edit(String key)方法調(diào)用的是DiskLruCache類的edit(String key, long expectedSequenceNumber)方法,在DiskLruCache類的edit(String key, long expectedSequenceNumber)方法里面其實(shí)是通過(guò)lruEntries的 lruEntries.get(key)方法獲取的DiskLruCache.Entry對(duì)象,然后通過(guò)這個(gè)DiskLruCache.Entry獲取對(duì)應(yīng)的編輯器,獲取到編輯器后, 再次這個(gè)編輯器(editor)通過(guò)okio把Cache.Entry寫入這個(gè)編輯器(editor)對(duì)應(yīng)的文件上。注意,這里是寫入的是http中的header的內(nèi)容 ,最后 返回一個(gè)CacheRequestImpl對(duì)象
緊接著又調(diào)用了 CacheInterceptor.cacheWritingResponse(CacheRequest, Response)方法
主要就是通過(guò)配置好的cache寫入緩存,都是通過(guò)Cache和DiskLruCache來(lái)具體實(shí)現(xiàn)
總結(jié):緩存實(shí)際上是一個(gè)比較復(fù)雜的邏輯,單獨(dú)的功能塊,實(shí)際上不屬于OKhttp上的功能,實(shí)際上是通過(guò)是http協(xié)議和DiskLruCache做了處理。
LinkedHashMap可以實(shí)現(xiàn)LRU算法,并且在這個(gè)case里,它被用作對(duì)DiskCache的內(nèi)存索引
告訴你們一個(gè)秘密,Universal-Imager-Loader里面的DiskLruCache的實(shí)現(xiàn)跟這里的一模一樣,除了io使用inputstream/outputstream
使用LinkedHashMap和journal文件同時(shí)記錄做過(guò)的操作,其實(shí)也就是有索引了,這樣就相當(dāng)于有兩個(gè)備份,可以互相恢復(fù)狀態(tài)
通過(guò)dirtyFiles和cleanFiles,可以實(shí)現(xiàn)更新和讀取同時(shí)操作,在commit的時(shí)候?qū)leanFiles的內(nèi)容進(jìn)行更新就好了