在正式最近最久未使用緩存(LruDiscCache)以前,先介紹一個概念和重要的三個類: java
key:是DiscCacheAware接口中save方法裏面的imageUri參數經過調用FileNameGenerator的generate(imageUri)所生成的字符串,key必須知足[a-z0-9_-]{1,64};對應着Entry,Snapshot以及Editor的key字段。經過以以下方法來檢測key的合法性 算法
Entry:是DiskLruCache裏面的內部類,每個key對應多個緩存圖片文件,保存文件的個數是由Entry中lengths數組的長度來決定的,若是追根就地的話是valueCount來決定Entry中封裝的文件的個數。(不過在實際初始化緩存的過程當中,該每個Entry中只有一個圖片緩存文件與之對應,也就是說一個key對應一個file) 數組
該類只有一個構造函數用來初始化key和lengths數組 緩存 |
||||
屬性名稱 app |
說明 less |
類型 編輯器 |
||
key 函數 |
每個key對應多個file,該能夠是圖片文件 ui |
String this |
||
readable |
噹噹前Entry對象被髮布的時候設置爲true |
boolean |
||
currentEditor |
若是當前entry沒有被編輯那麼該屬性就位null |
Editor |
||
sequenceNumber |
對當前entry最近提交的編輯作的標誌序列號,每成功提交一次,固然entry的該字段就++ |
long |
||
lengths |
當前entry中的每個file的長度,在構造器中初始化,初始化長度爲valueCount,實際上該lengths固定長度爲1,也就是一個Entry表明了一個文件的緩存實體 |
long[] |
||
|
||||
方法名 |
方法說明 |
返回值 |
||
getLengths() |
返回當前entry對象中全部文件的總長度 |
String |
||
setLengths(String[] strs) |
採用十進制的數字來設置每個entry所封裝的file的長度,來這是lengths數組每個元素的值,經過讀取日誌的CLEAN行數據時調用此方法 |
void |
||
getCleanFile(int i) |
獲取當前entry中某一個乾淨(有效)的緩存文件(圖片),文件名稱的格式爲key+」.」+i |
File |
||
getDirtyFile(int i) |
獲取當前entry中每個髒的(無效)的緩存文件(圖片),名稱的格式爲key+」.」+i+」.tmp」 |
File |
||
Editor(編輯器):每個entry對象又都包含一個Editor,Editor類也是DiskLruCache的一個final類型的內部類;用來負責保存Entry中每個圖片File的文件輸出流和輸入流,以便於圖片緩存文件的輸出和讀取,在調用緩存的save方法的時候就是獲取方法參數imageUri所生成的key對應Entry中的一個Editor對象,從而獲取imageUri圖片的輸出流來把圖片寫入到緩存中(directory目錄中)。
該類提供一個構造器用來初始化,用來所要編輯的entry和written數組 |
|||
屬性 |
說明 |
類型 |
|
entry |
final,表明當前Editor對象的所要編輯的entry對象 |
Entry |
|
written |
final,在構造器中初始化,若是entry已經發布的話就設置爲null,不然就初始化長度爲valueCount的數組,它的每個元素用來標誌entry中對應索引文件是否可寫。 |
boolean[] |
|
hasErrors |
編輯是否出錯,當把entry中的一個File輸出時發生IO異常時設置爲true,具體在Editor的內部類FaultHidingOutputStream中設置 |
boolean |
|
committed |
編輯是否已經提交 |
boolean |
|
|
|||
方法名 |
方法說明 |
返回類型 |
|
newInputStream(int index) |
該方法返回一個無緩衝的輸入流用來讀取entry中第index條數據(也就是第index個圖片文件,實際應用因爲Entry中值對應一個文件因此index固定位0)上次提交的值,若是該條數據沒有被提交的值就返回null。 1) |
InputStream |
|
getString(index) |
獲取entry中第index文件上次提交的值 |
String |
|
newOutputStream(int index) |
Entry中對應文件的輸出流,向緩存寫入數據時調用,目的是把圖片文件保存到緩存。 調用save方法時調用,獲取entry中第index文件(也就是第index個圖片文件,實際應用因爲Entry中值對應一個文件因此index固定位0)上的無緩衝的輸出流用來把文件輸出到緩存, 若是輸出的過程當中發成異常就設置Editor的hasErrors爲true,即爲編輯失敗 |
OutputStream |
|
set(index,String value) |
向固然Editor中entry的第index個文件寫入數據value |
value |
|
commit() |
當編輯完成後調用這個方法使得該File對reader可見,同時釋放線程鎖以便於讓其餘editor對象對同一個key上的entry進行編輯操做。執行步驟以下: 1) 判斷hasError是否爲true,若是爲true,則撤銷這次提交 2)設置commited爲true |
void |
|
abort() |
終止對當前entry的第index文件的編輯操做。其實是調用completeEdit(this,false)來撤銷這次編輯,並釋放鎖 |
void |
|
abortUnlessCommitted() |
在沒有提交的狀況下,也就是commited=false的狀況下終止本次編輯 |
|
|
Snapshot: 每個Entry又有一個Snapshot(快照),當從緩存中調用DisCacheAware方法中的get(String iamgeUri)獲取緩存圖片時實際上獲取的不是Entry,而是imageUri生成key對應Entry的一個快照Snapshot對象所封裝的File對象
該類實現了Closeable,可使用Java7的新特性 用try-with-resource來自動關閉流,該類包含的字段都在構造函數中進行初始化 |
||
屬性名 |
說明 |
類型 |
key |
entry的key |
string |
sequenceNumber |
entry的sequenceNumber |
long |
files[] |
entry中全部的file(實際上該files的長度只有一) |
File[] |
ins |
entry中全部file的輸入流 |
InputStream[] |
lengths[] |
entry全部file的總大小 |
long[] |
edit() |
返回該快照所對應的entry的Editor對象 |
|
getFile(int index) |
獲取快照中的第index個文件,從緩存中取出數據時調用 |
File |
getInputStream(int index) |
獲取ins數組中第index個文件的輸入流 |
InputStream |
getString(int index) |
把第index文件中的內容做爲字符串返回 |
String |
close() |
循環遍歷ins,關閉每個輸入流 |
void |
因此Entry,Editor,Snapshot之間的關係經過key串聯了起來:
日誌文件:該緩存提供了一個名叫journal的日誌文件,典型的日誌文件看清來以下格式
每一個日誌文件的開頭前面五行數據分別爲
行號 |
該行的數據 |
1 |
libcore.io.DiskLruCache |
2 |
該緩存的版本號 例如1 |
3 |
app的版本號 例如100 |
4 |
每個Entry中所對應的File的數目例如2 |
5 |
空白行 |
第五行事後就是日誌的正文,日誌正文的格式以下
CLEAN行所表明的數據格式以下
CLEAN |
|
entry所表明的key |
|
f1.length |
|
f2.length |
|
……………. |
\n |
特別說明:f1.length 是key所對應的entry中第一個文件的大小,和f2.length之間用一個空格隔開,具體fn是多少由日誌中第四行的數據所決定。若是追根究底的話,是由Entry對象中的lengths數組的長度來決定或者是DiskLruCache的valueCount字段來決定(由於lengths數組的長度初始化的時候就是valueCount)。
REMOVE行READ行以及DIRTY行顯示的數據格式較爲簡單:
REMOVE |
|
entry對象的key |
\n |
READ |
|
同上 |
\n |
DIRTY |
|
同上 |
\n |
下面是源代碼中給出的日誌樣本文件,如圖
每行字段的被寫入日誌文件的時機以下表:
DIRTY |
寫入該行數據的時機有兩處: 1) 調用rebuildJournal從新新的日誌文件時會把lruEntries中處於編輯狀態的entry(entry.currentEditor!=null的狀態)寫入日誌文件中去(日誌格式件見上文) 2) 調動edit方法對entry進行編輯或者說調用save方法時會把當前的entry寫入到日誌文件 |
CLEAN |
寫入該行數據的時機有兩處: 1) 調用rebuildJournal從新新的日誌文件時會把lruEntries中處於非編輯狀態的entry(entry.currentEditor==null的狀態)寫入日誌文件中去(日誌格式件見上文) 2) completeEdit方法中當前entry處於發佈狀態(readable=true)或者編輯成功的時候(success=true的時候)寫入 |
READ |
當調用get(key)方法獲取entry的快照時會寫入 |
REMOVE |
寫入該條數據的世紀有兩處 1) 在completeEdit方法中若是固然Entry既不處於發佈狀態並且方法參數success爲false的時候寫入,而且把對應的緩存文件也從緩存中刪除。 2) 調用remove(String key)方法刪除key對應的緩存文件時寫入,而且把對應的緩存文件也從緩存中刪除。 |
既然有寫入日誌文件的時機,那麼確定也會提供一個讀取日誌文件的時機,具體的時機下面講解open方法初始化緩存的時候會講到。
當緩存被操做的時候日誌文件就會被追加進來。日誌文件偶爾會經過丟掉多餘行的數據來實現對日誌的簡化;在對日誌進行簡化操做的過程當中會用到一個名爲journal.tmp的臨時文件,當緩存被打開的狀況下若是journal.tmp文件存在的話就會被刪除。
在DiskLruCache類中與日誌相關的字段以下所示:
注意:其中的journalWriter,該對象用來向日志文件中寫入數據,同時該對象是否爲null是做爲緩存是否關閉的決定條件:下面三個方法能夠說明這個結論
---------------------------------------------------------------------------------------------------
其實,這個類中大部分的方法都是再操做這些日誌文件,固然日誌文件的大小也有限制,而這個限制就是有redundantOpCount字段決定的,若是以下方法返回true的話就從新創建一個新的日誌文件,並把原來的日誌文件刪除掉
redundantOpCount的值有在四處進行了設定:
get(String key),remove(Stringkey) completeEdit沒調用一次這個方法就會對redundantOpCount進行++操做,而讀取日誌的方法readJournal則對該字段賦值爲redundantOpCount = lineCount - lruEntries.size();事實上這個readJournal是在調用open方法初始化緩存的時候調用的,也就至關於對redundantOpCount進行了初始化操做。同時當journalRebuildRequired的時候redundantOpCount進行清零操做
-------------------------------------------------------------------------------
介紹了上面的一些基本概念下面說說具體怎麼使用這個緩存(介紹流程爲:初始化緩存,向緩存中存取數據,從緩存中刪除數據以及關閉緩存來進行說明)以及LRU算法實現是怎麼體現的。
因爲DiskLruCache的構造函數是私有的,因此不能在外部進行該對象的初始化;DiskLruCache提供了一個靜態的open()方法來進行緩存的初始化:
該方法進行以下操做
1) 建立日誌文件:主要是對日誌備份文件journal.bkp進行處理,若是journal.bkp文件和journal都存在的話就刪除journal.bkp文件,若是journal.bkp文件存在而journal文件不存在就把journal.bkp重命名爲journal文件。
2) 調用構造器進行緩存對象cache的初始化,初始化的的數據包括日誌的三個文件:journal, journal.tmp,journal.bkp;同時還初始化了每個Entry鎖能存儲的文件的個數valueCount,緩存的最大內存maxSize和最多緩存多少個文件的maxFileCount;
3) 若是cache.journalFile.exists()==true而且讀取日記操做沒有IO錯誤的話,就直接返回上面的cache,否者就從新初始化緩存對象。
也便是說打開緩存的時候有可能初始化兩次DiskLruCache的對象,第一次初始化cache1的時候會判斷日誌文件journalFile是否存在,不存在的話就進行第二次初始化cache2;若是存在的話就進行對journalFile進行IO操做,若是沒有出現異常的狀況下直接返回cache1,不然返回cache2.邏輯代碼的處理以下:
上文剛說過初始化的時候須要讀日誌進行讀取操做,下面重點說說初始化緩存的時候對日誌文件進行了哪些操做。
1) 讀取日誌的方法是由readJournal()來對日式文件journal一行一行的讀取,對每一行日誌文件的處理是由readJournalLine(String line)方法來決定的,對每一行日式數據的處理實際上式對每行日誌的key在lruEnries中對應Entry對象的處理。對每一行文件的處理以下表:
DIRTY |
當讀取該條數據的時候,就實例化該key對應entry對象的currentEditor使之處於編輯狀態 |
CLEAN |
設置該條數據key對應的Entry的爲發佈狀態,而且設置currentEditro=null |
READ |
對該條數據不做處理 |
REMOVE |
當調用open方法讀取日誌文件的時候,改行數據中key鎖對應的那個實體會從lruEntries中刪除。注意:該key對應的Entry所表明的那個file文件在緩存已經刪除 |
注意:除了讀取到REMOVE行直接在lruEntries中刪除對應的Entry以外,其他的每一行數據的須要進行以下判斷而後在進行處理,主要是向lruEntries中添加Entry對象,(這是第一次添加):
附帶具體方法實現:
2) 調用processJournal()對日誌文件進一步處理:遍歷lruEntries(lruEntires中的數據在步驟1中的readJournalLine方法中添加的)中的每個Entry對象,對同編輯狀態的Entry進行不一樣的處理;對於處於非編輯狀態的entry。也就是entry.currentEditor==null的entry,計算他們的總的文件數目fileCount以及總的文件的大小size;而entry.currentEditor!=null的Entry(注意是由日誌文件中的DIRTY對應的Entry),這些,刪除這些entry對應的每個file,也便是說直接從緩存中刪除了這些緩存文件。
3) 初始化journalWriter
到此完成了對緩存的初始化操做,注意這是在讀取日誌文件時沒有拋出異常的時候完成的初始化,若是拋出異常的話,就再次進行以下初始化
向緩存寫入數據時經過DiscCacheWare的兩個save方法來實現的:核心思想是從lruEntries中獲取指定key(假設該key爲123456)對應的Entry對象(若是沒有就往lruEntries中添加),而後獲取該Entry對象的Editor對象editor,並返回editor。由於這個editor包含了文件的輸出流,用該輸出流來想緩存中寫入數據,從而達到緩存圖片的目的。(注意此時向journal文件中寫入了DIRTY記錄)>
此時lruEntries中和journal文件中的包含的數據以下:
若是失敗的話調用editor.abort()來撤掉這次的編輯,同時從lruEntries中刪除此entry.(注意此時向journal文件中寫入了REMOVE記錄)
從緩存中取數據是經過調用DiscCacheWare的get(String imageUri)方法來實現的。前面說過,從緩存中取數據的時候獲取的其實是一個Entry的Snapshot,具體的方法以下:
此時向日志中加READ 記錄
經過代碼能夠發現,當向緩存中取數據的時候須要檢測是否重建日誌,具體怎麼重建,見下文。在此暫不作描述。
調用DiscCacheAware接口的remove(String imageUri方法)來實現,具體的刪除的主要邏輯:
刪除緩存中對應的文件,向日志中追加REMOVE行,從lruEntires中刪除對應的entry
經過close方法來實現,具體的邏輯爲:
對正在編輯的entry進行撤銷操做;
調用trimToSize使得已經使用緩存的大小不超過maxSize
調用trimToFileCount()方法使得緩存中的文件方法小於maxfileCount
關閉日誌journalWriter
清空緩存的clear除了以上的邏輯外還對directory進行了刪除操做。
--------------------------------------------------------------------------------------------------------------
LruDiscCache,看到這個類的名字就是到該類用到了最近最久未使用(LRU)算法來處理文件緩存。該算法的在該類核心思想的體現就是選擇在最近一段時間裏最久沒有使用過的緩存文件刪除。
該類也提供了跟BasicDiscCache同樣的默認屬性,好比默認緩存的大小爲32k,默認壓縮後的圖片格式爲png等等。另外也提供了後備緩存reserveCacheDir,不過跟BasicDiscCache不一樣的是BasicDiscCache中表明緩存目錄的cacheDir在LruDiscCache中用DiskLruCache對象的引用cache來代替。
LruDiscCache提供了一個主要構造函數,該構造函數裏面的參數主要用來初始化fileNameGenerator和cache對象。在此構造器中能夠設置最大緩存的大小,緩存最多能夠保存多少條數據的參數,這些參數都是初始化cache對象所須要的數據。
DiscCache接口提供的方法在LruDiscCache中的核心實現都轉移到了cache對象中(或者說是DiskLruCache中)。下面就說說DiskLruCache,而後在掉過頭來講LruDiscCache。
該類也定義了一下變量:
另外該類還封裝了一個重要類型爲LinkedHashMap的屬性lruEntries,用它來實現LRU算法(最近最久未使用算法)
用LinkedHashMap能實現LRU算法的緣由是在迭代map遍歷列表中的元素時最近訪問的元素會排在LinkedHashMap的尾部 。在這裏簡單介紹一個例子做爲說明:若是一個LinkedHashMap中經過一個for循環加入了a b c d e 五個元素,而後調用get方法獲取元素a,那麼當再次遍歷該map的時候iterator.next().getValue()會依次輸入b c d e a而不是a b c d e,這樣經過最近常用的元素(好比get方法獲取的元素a)就放在後面,最近最少使用的就排在了鏈表的前面,從而實現了LRU算法。
最後還有一個long 類型的nextSequenceNumber:爲了對新的和舊的快照作區分,每個entry對象在每一次編輯被提交的時候會獲取一個序列號(nextSequenceNumber),若是這個序列號不等於entry的序列號的話,就說明該快照是舊的快照。(最近最久未使用的快照)
在這個緩存中實現對最近最久未使用文件的刪除的目的和時機以下:
目的:
1) 在緩存文件總大小超出最大緩存大小maxSize時對最近最久未使用的圖片緩存進行刪除,核心方法爲:trimToSize
2) 在緩存中的文件總數目超過緩存要求的最大文件數目fileCount時對最近最久未使用的的圖片緩存進行刪除,核心方法爲:trimToFileCount
這兩個方法調用的時機從整體來講分爲三個,因爲這些操做涉及到IO操做,費時,因此在代碼中交給了一個Callable去處理,具體的核心代碼以下
執行Callable的三種時機:
1) 調用setMax方法設置緩存的最大值的時候
2) 成功緩存一個文件的時候,也就是說調用complete第二個參數爲true的時候、
3) 對緩存中的文件進行刪除的時候