- 1.OkHttp源碼解析(一):OKHttp初階
- 2 OkHttp源碼解析(二):OkHttp連接的"前戲"——HTTP的那些事
- 3 OkHttp源碼解析(三):OKHttp中階之線程池和消息隊列
- 4 OkHttp源碼解析(四):OKHttp中階之攔截器及調用鏈
- 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,由于篇幅限制,今天內容看似不多,大概分為兩個部分
1.DiskLruCache內部類詳解
2.DiskLruCache類詳解
3.OKHTTP的緩存的實現---CacheInterceptor的具體執行流程
一、DiskLruCache
在看DiskLruCache前先看下他的幾個內部類
1、Entry.class(DiskLruCache的內部類)
Entry內部類是實際用于存儲的緩存數據的實體類,每一個url對應一個Entry實體
private final class Entry {
final String key;
/** 實體對應的緩存文件 */
/** Lengths of this entry's files. */
final long[] lengths; //文件比特數
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;
//構造器 就一個入參 key,而key又是url,所以,一個url對應一個Entry
Entry(String key) {
this.key = key;
//valueCount在構造DiskLruCache時傳入的參數默認大小為2
//具體請看Cache類的構造函數,里面通過DiskLruCache.create()方法創建了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,所以循環了2次,一共創建了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的內部類)
/** 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; //可以讀入數據的流 這么多的流主要是從cleanFile中讀取數據
private final long[] lengths; //與上面的流一一對應
//構造器就是對上面這些屬性進行賦值
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方法主要就是調用DiskLruCache的edit方法了,入參是該Snapshot對象的兩個屬性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類型數組,容量為2
Source[] sources = new Source[valueCount];
//clone一個long類型的數組,容量為2
long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
//獲取cleanFile的Source,用于讀取cleanFile中的數據,并用得到的souce、Entry.key、Entry.length、sequenceNumber數據構造一個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的內部類)
Editro類的屬性和構造器貌似看不到什么東西,不過通過構造器,我們知道,在構造一個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正在被調用的時候而被清
*除的,為了防止編輯器可以正常的完成。我們需要刪除編輯器創建的文件,并防止創建新的文件。如果編
*輯器被分離,其他的編輯器可以編輯這個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) {
//如果已經commit了,不能讀取了
if (done) {
throw new IllegalStateException();
}
//如果entry不可讀,并且已經有編輯器了(其實就是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文件的輸出流,如果在寫入數據的時候出現錯誤,會立即停止。返回的輸出流不會拋IO異常
*/
public Sink newSink(int index) {
synchronized (DiskLruCache.this) {
//已經提交,不能操作
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.
* 寫好數據,一定不要忘記commit操作對數據進行提交,我們要把dirtyFiles里面的內容移動到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) {
}
}
}
}
}
哎,看到這個了類的注釋,發現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文件寫入數據 |
commit() | 這里執行的工作是提交數據,并釋放鎖,最后通知DiskLruCache刷新相關數據 |
abort() | 終止編輯,并釋放鎖 |
abortUnlessCommitted() | 除非正在編輯,否則終止 |
abort()和abortUnlessCommitted()最后都會執行completeEdit(Editor, boolean) 這個方法這里簡單說下:
success情況提交:dirty文件會被更名為clean文件,entry.lengths[i]值會被更新,DiskLruCache,size會更新(DiskLruCache,size代表的是所有整個緩存文件加起來的總大?。瑀edundantOpCount++,在日志中寫入一條Clean信息
failed情況:dirty文件被刪除,redundantOpCount++,日志中寫入一條REMOVE信息
至此DiskLruCache的內部類就全部介紹結束了。現在咱們正式關注下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算法的光環屬性,詳情請看LinkedHashMap源碼說明
DiskLruCache也有一個線程池屬性 executor,不過該池最多有一個線程工作,用于清理,維護緩存數據。創建一個DiskLruCache對象的方法是調用該方法,而不是直接調用構造器。
(二)、構造函數和創建對象
DiskLruCache有一個構造函數,但是不是public的所以DiskLruCache只能被包內中類調用,不能在外面直接new。不過DiskLruCache提供了一個靜態方法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;
}
該構造器會在制定的目錄下創建三份文件,這三個文件是DiskLruCache的工作日志文件。在執行DiskLruCache的任何方法之前都會執行initialize()方法來完成DiskLruCache的初始化,有人會想為什么不在DiskLruCache的構造器中完成對該方法的調用,其實是為了延遲初始化,因為初始化會創建一系列的文件和對象,所以做了延遲初始化。
(三)、初始化
那么來看下initialize里面的代碼
public synchronized void initialize() throws IOException {
//斷言,當持有自己鎖的時候。繼續執行,沒有持有鎖,直接拋異常
assert Thread.holdsLock(this);
//如果已經初始化過,則不需要再初始化,直接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文件,則對該文件,則分別調用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;
}
大家發現沒有,如論是否有journal文件,最后都會將initialized設為true,該值不會再被設置為false,除非DiskLruCache對象唄銷毀。這表明initialize()放啊在DiskLruCache對象的整個生命周期中只會執行一次。該動作完成日志的寫入和lruEntries集合的初始化。
這里面分別調用了readJournal()方法和processJournal()方法,那咱們依次分析下這兩個方法,這里面有大量的okio里面的代碼,如果大家對okio不熟悉能讀上一篇文章。
private void readJournal() throws IOException {
//獲取journalFile的source即輸入流
BufferedSource source = Okio.buffer(fileSystem.source(journalFile));
try {
//讀取相關數據
String magic = source.readUtf8LineStrict();
String version = source.readUtf8LineStrict();
String appVersionString = source.readUtf8LineStrict();
String valueCountString = source.readUtf8LineStrict();
String blank = source.readUtf8LineStrict();
//做校驗
if (!MAGIC.equals(magic)
|| !VERSION_1.equals(version)
|| !Integer.toString(appVersion).equals(appVersionString)
|| !Integer.toString(valueCount).equals(valueCountString)
|| !"".equals(blank)) {
throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
+ valueCountString + ", " + blank + "]");
}
int lineCount = 0;
//校驗通過,開始逐行讀取數據
while (true) {
try {
readJournalLine(source.readUtf8LineStrict());
lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
//讀取出來的行數減去lruEntriest的集合的差值,即日志多出的"冗余"記錄
redundantOpCount = lineCount - lruEntries.size();
// If we ended on a truncated line, rebuild the journal before appending to it.
//source.exhausted()表示是否還多余字節,如果沒有多余字節,返回true,有多月字節返回false
if (!source.exhausted()) {
//如果有多余字節,則重新構建下journal文件,主要是寫入頭文件,以便下次讀的時候,根據頭文件進行校驗
rebuildJournal();
} else {
//獲取這個文件的Sink
journalWriter = newJournalWriter();
}
} finally {
Util.closeQuietly(source);
}
}
這里說一下ource.readUtf8LineStrict()方法,這個方法是BufferedSource接口的方法,具體實現是RealBufferedSource,所以大家要去RealBufferedSource里面去找具體實現。我這里簡單說下,就是從source里面按照utf-8編碼取出一行的數據。這里面讀取了magic,version,appVersionString,valueCountString,blank,然后進行校驗,這個數據是在"寫"的時候,寫入的,具體情況看DiskLruCache的rebuildJournal()方法。隨后記錄redundantOpCount的值,該值的含義就是判斷當前日志中記錄的行數和lruEntries集合容量的差值,即日志中多出來的"冗余"記錄。
讀取的時候又調用了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;
// 方法返回第一個空字符在此字符串中第一次出現,在指定的索引即keyBegin開始搜索,所以secondSpace是愛這個字符串中的空字符(不包括這一行最左側的那個空字符)
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
//如果沒有中間的空字符
if (secondSpace == -1) {
//截取剩下的全部字符串構成key
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
//如果解析的是REMOVE信息,則在lruEntries里面刪除這個key
lruEntries.remove(key);
return;
}
} else {
//如果含有中間間隔的空字符,則截取這個中間間隔到左側空字符之間的字符串,構成key
key = line.substring(keyBegin, secondSpace);
}
//獲取key后,根據key取出Entry對象
Entry entry = lruEntries.get(key);
//如果Entry為null,則表明內存中沒有,則new一個,并把它放到內存中。
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) 為獲取中間空格后面的內容,然后按照空字符分割,設置entry的屬性,表明是干凈的數據,不能編輯。
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創建一個entry,如果是判斷信息是CLEAN則設置ENTRY為可讀,并設置entry.currentEditor表明當前Entry不可編輯,調用entry.setLengths(String[]),設置該entry.lengths的初始值。如果判斷是Dirty則設置enry.currentEdtor=new Editor(entry);表明當前Entry處于被編輯狀態。
通過上面我得到了如下的結論:
- 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
然后又調用了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文件
然后調用for循環獲取鏈表中的所有Entry,如果Entry的中Editor!=null,則表明Entry數據時臟的DIRTY,所以不能讀,進而刪除Entry下的緩存文件,并且將Entry從lruEntries中移除。如果Entry的Editor==null,則證明該Entry下的緩存文件可用,記錄它所有緩存文件的緩存數量,結果賦值給size。
readJournal()方法里面調用了rebuildJournal(),initialize()方法同樣會readJourna,但是這里說明下:readJournal里面調用的rebuildJournal()是有條件限制的,initialize()是一定會調用的。那我們來研究下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 {
//寫入相關信息和讀取向對應,這時候大家想下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數據
if (entry.currentEditor != null) {
在開頭寫上 DIRTY,然后寫上 空字符
writer.writeUtf8(DIRTY).writeByte(' ');
//把entry的key寫上
writer.writeUtf8(entry.key);
//換行
writer.writeByte('\n');
} else {
//如果editor為null,則為CLEAN數據, 在開頭寫上 CLEAN,然后寫上 空字符
writer.writeUtf8(CLEAN).writeByte(' ');
//把entry的key寫上
writer.writeUtf8(entry.key);
//結尾接上兩個十進制的數字,表示長度
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;
//設置最近重新創建journal文件成功
mostRecentRebuildFailed = false;
}
總結下:
獲取一個寫入流,將lruEntries集合中的Entry對象寫入tmp文件中,根據Entry的currentEditor的值判斷是CLEAN還是DIRTY,寫入該Entry的key,如果是CLEAN還要寫入文件的大小bytes。然后就是把journalFileTmp更名為journalFile,然后將journalWriter跟文件綁定,通過它來向journalWrite寫入數據,最后設置一些屬性。
我們可以砍到,rebuild操作是以lruEntries為準,把DIRTY和CLEAN的操作都寫回到journal中。但發現沒有,其實沒有改動真正的value,只不過重寫了一些事務的記錄。事實上,lruEntries和journal文件共同確定了cache數據的有效性。lruEntries是索引,journal是歸檔。至此序列化部分就已經結束了
(四)、關于Cache類調用的幾個方法
上回書說道Cache調用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();
//檢查緩存是否已經關閉
checkNotClosed();
//檢驗key
validateKey(key);
//如果以上都通過,先獲取內存中的數據,即根據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
//計數器自加1
redundantOpCount++;
//把這個內容寫入文檔中
journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
//如果超過上限
if (journalRebuildRequired()) {
//開始清理
executor.execute(cleanupRunnable);
}
//返回數據
return snapshot;
}
/**
* We only rebuild the journal when it will halve the size of the journal and eliminate at least
* 2000 ops.
*/
boolean journalRebuildRequired() {
//最大計數單位
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) {
//如果沒有初始化或者已經關閉了,則不需要清理
if (!initialized | closed) {
return; // Nothing to do
}
try {
trimToSize();
} catch (IOException ignored) {
//如果拋異常了,設置最近的一次清理失敗
mostRecentTrimFailed = true;
}
try {
//如果需要清理了
if (journalRebuildRequired()) {
//重新創建journal文件
rebuildJournal();
//計數器歸于0
redundantOpCount = 0;
}
} catch (IOException e) {
//如果拋異常了,設置最近的一次構建失敗
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正常的結束
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;
}
//計數器自加1
redundantOpCount++;
//在journalWriter添加一條刪除記錄
journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');
//linkedList刪除這個entry
lruEntries.remove(entry.key);
//如果需要重新構建
if (journalRebuildRequired()) {
//開啟清理任務
executor.execute(cleanupRunnable);
}
return true;
}
看下cleanupRunnable對象,看他的run方法得知,主要是調用了trimToSize()和rebuildJournal()兩個方法對緩存數據進行維護。rebuildJournal()前面已經說過了,這里主要關注下trimToSize()方法,trimToSize()方法主要是遍歷lruEntries(注意:這個遍歷科室通過accessOrder來的,也就是隱含了LRU這個算法),來一個一個移除entry直到size小于maxSize,而removeEntry操作就是講editor里的diryFile以及cleanFiles進行刪除就是,并且要向journal文件里寫入REMOVE操作,以及刪除lruEntrie里面的對象。
cleanup主要是用來調整整個cache的大小,以防止它過大,同時也能用來rebuildJournal,如果trim或者rebuild不成功,那之前edit里面也是沒有辦法獲取Editor來進行數據修改操作的。
下面來看下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.
*根據key來刪除對應的entry,如果entry存在則將會被刪除,如果這個entry正在被編輯,編輯將被正常結束,但是編輯的內容不會保存
* @return true if an entry was removed.
*/
public synchronized boolean remove(String key) throws IOException {
//初始化
initialize();
//檢查是否被關閉
checkNotClosed();
//key是否符合要求
validateKey(key);
//根據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
那我們繼續,來看下DiskLruCache.edit(String);方法
/**
* Returns an editor for the entry named {@code key}, or null if another edit is in progress.
* 返回一entry的編輯器,如果其他正在編輯,則返回null
* 我的理解是根據key找entry,然后根據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);
//根據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.
}
//如果最近清理失敗,或者最近重新構建失敗,我們需要開始清理任務
//我大概翻譯下注釋:操作系統已經成為我們的敵人,如果清理任務失敗,它意味著我們存儲了過多的數據,因此我們允許超過這個限制,所以不建議編輯。如果構建日志失敗,writer這個寫入流就會無效,所以文件無法及時更新,導致我們無法繼續編輯,會引起文件泄露。如果滿足以上兩種情況,我們必須進行清理,擺脫這種不好的狀態。
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);
}
//根據entry 構造一個Editor
Editor editor = new Editor(entry);
entry.currentEditor = editor;
return editor;
}
上面代碼注釋說的很清楚,這里就提幾個注意事項
注意事項:
(1)如果已經有個別的editor在操作這個entry了,那就返回null
(2)無時無刻不在進行cleanup判斷進行cleanup操作
(3)會把當前的key在journal文件標記為dirty狀態,表示這條記錄正在被編輯
(4)如果沒有entry,會new一個出來
這個方法已經結束了,那我們來看下 在Editor內部類commit()方法里面調用的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里面要有數據,這是為了保證完整性
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,完成數據遷移;
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數據
fileSystem.delete(dirty);
}
}
//計數器加1
redundantOpCount++;
//編輯器指向null
entry.currentEditor = null;
if (entry.readable | success) {
//開始寫入數據
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);
}
}
這樣下來,數據都寫入cleanFile了,currentEditor也重新設為null,表明commit徹底結束了。
總結起來DiskLruCache主要的特點:
- 1、通過LinkedHashMap實現LRU替換
- 2、通過本地維護Cache操作日志保證Cache原子性與可用性,同時為防止日志過分膨脹定時執行日志精簡。
- 3、 每一個Cache項對應兩個狀態副本:DIRTY,CLEAN。CLEAN表示當前可用的Cache。外部訪問到cache快照均為CLEAN狀態;DIRTY為編輯狀態的cache。由于更新和創新都只操作DIRTY狀態的副本,實現了讀和寫的分離。
- 4、每一個url請求cache有四個文件,兩個狀態(DIRY,CLEAN),每個狀態對應兩個文件:一個0文件對應存儲meta數據,一個文件存儲body數據。
至此所有的關于緩存的相關類都介紹完畢,為了幫助大家更好的理解緩存,咱們在重新看下CacheInterceptor里面執行的流程
三.OKHTTP的緩存的實現---CacheInterceptor的具體執行流程
(一)原理和注意事項:
1、原理
(1)、okhttp的網絡緩存是基于http協議,不清楚請仔細看上一篇文章
(2)、使用DiskLruCache的緩存策略,具體請看本片文章的第一章節
2、注意事項:
1、目前只支持GET,其他請求方式需要自己實現。
2、需要服務器配合,通過head設置相關頭來控制緩存
3、創建OkHttpClient時候需要配置Cache
(二)流程:
1、如果配置了緩存,則從緩存中取出(可能為null)
2、獲取緩存的策略.
3、監測緩存
4、如果禁止使用網絡(比如飛行模式),且緩存無效,直接返回
5、如果緩存有效,使用網絡,不使用網絡
6、如果緩存無效,執行下一個攔截器
7、本地有緩存、根據條件判斷是使用緩存還是使用網絡的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、監測緩存
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、如果禁止使用網絡(比如飛行模式),且緩存無效,直接返回
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、如果緩存有效,使用網絡,不使用網絡
// 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、如果緩存無效,執行下一個攔截器
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、本地有緩存、根據條件判斷是使用緩存還是使用網絡的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、什么是“倒序具體分析”?
這里的倒序具體分析是指先分析緩存,在分析使用緩存,因為第一次使用的時候,肯定沒有緩存,所以肯定先發起請求request,然后收到響應response的時候,緩存起來,等下次調用的時候,才具體獲取緩存策略。
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)方法就是調用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作為參數來構造Cache.Entry對象,這里強烈提示下,是Cache.Entry對象,不是DiskLruCache.Entry對象。 然后 調用的是DiskLruCache類的edit(String key)方法,而DiskLruCache類的edit(String key)方法調用的是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的內容 ,最后 返回一個CacheRequestImpl對象
緊接著又調用了 CacheInterceptor.cacheWritingResponse(CacheRequest, Response)方法
主要就是通過配置好的cache寫入緩存,都是通過Cache和DiskLruCache來具體實現
總結:緩存實際上是一個比較復雜的邏輯,單獨的功能塊,實際上不屬于OKhttp上的功能,實際上是通過是http協議和DiskLruCache做了處理。
LinkedHashMap可以實現LRU算法,并且在這個case里,它被用作對DiskCache的內存索引
告訴你們一個秘密,Universal-Imager-Loader里面的DiskLruCache的實現跟這里的一模一樣,除了io使用inputstream/outputstream
使用LinkedHashMap和journal文件同時記錄做過的操作,其實也就是有索引了,這樣就相當于有兩個備份,可以互相恢復狀態
通過dirtyFiles和cleanFiles,可以實現更新和讀取同時操作,在commit的時候將cleanFiles的內容進行更新就好了