目錄
- 一、DiskLruCache 的使用
- 1、DiskLruCache 的創建
- 2、DiskLruCache 的添加
- 3、DiskLruCache 的查找
- 4、DiskLruCache 的移除
- 二、部分原始碼決議
- 1、快取日志 journal
- 2、DiskLruCache 的 open()
- 3、DiskLruCache 的 edit()
- 4、DiskLruCache 的 get()
- 5、DiskLruCache 的 remove()
- 6、DiskLruCache 的 close()
- 7、DiskLruCache 的 delete()
- 8、DiskLruCache 的 size()
- 9、DiskLruCache 的 flush()
- 三、DiskLruCache 完整原始碼
- 四、參考資料
DiskLruCache 用于實作存盤設備快取,即磁盤快取,它通過將快取物件寫入檔案系統從而實作快取的效果,DiskLruCache 得到了 Android 官方檔案的推薦,但它不屬于 Android SDK 的一部分,它的 原始碼及網址文末會貼出來,下面分別從 DiskLruCache 的創建、快取查找和快取添加這三個方面來介紹 DiskLruCache 的使用方式,
一、DiskLruCache 的使用
如前已知,DiskLruCache 不屬于 Android SDK 的一部分,且需要存盤權限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
需要在 build.gradle 中配置
implementation 'com.jakewharton:disklrucache:2.0.2'
然后可以開始使用 DiskLruCache 了,
1、DiskLruCache 的創建
DiskLruCache 并不能通過構造方法來創建,它提供了 open() 方法用于創建自身
public static DiskLruCache open(File directory, int appVersion, invalueCount, long maxSize)
open() 方法有四個引數,第一個引數表示磁盤快取在檔案系統中的存盤路徑,快取路徑可以選擇 SD 卡上的快取目錄,具體是指 /sdcard/Android/data/<package_name>/cache 目錄,其中 <package_name> 表示當前應用的包名,當應用被卸載后,此目錄會一并被洗掉,當然也可以選擇 SD 卡上的其他指定目錄,還可以選擇 data 下的當前應用的目錄,具體可根據需要靈活設定,這里給出一個建議:如果應用卸載后就希望洗掉快取檔案,那么就選擇 SD 卡上的快取目錄,如果希望保留快取資料那就應該選擇 SD 卡上的其他特定目錄,
第二個引數表示應用的版本號,一般設為 1 即可,當版本號發生改變時 DiskLruCache 會清空之前所有的快取檔案,而這個特性在實際開發中作用并不大,很多情況下即使應用的版本號發生了改變快取檔案卻仍然是有效的,因此這個引數設為 1 比較好,
第三個引數表示同一個 key 可以對應多少個快取檔案,一般設為 1 即可,
第四個引數表示快取的總大小,比如 50MB,當快取大小超出這個設定值后,DiskLruCache 會清除一些快取從而保證總大小不大于這個設定值,
下面是一個典型的DiskLruCache的創建程序
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50; //50MB
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (! diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
其中,getDiskCacheDir() 方法如下
public File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
當 SD 卡存在或者 SD 卡不可被移除的時候,就呼叫getExternalCacheDir() 方法來獲取快取路徑,否則就呼叫getCacheDir() 方法來獲取快取路徑,前者獲取到的就是 /sdcard/Android/data/<application package>/cache 這個路徑,而后者獲取到的是 /data/data/<application package>/cache 這個路徑,
最后將獲取到的路徑和一個 uniqueName 進行拼接,作為最終的快取路徑回傳, uniqueName 是對不同型別的資料進行區分而設定的一個唯一值,比如 bitmap、file 等檔案夾,
2、DiskLruCache 的添加
DiskLruCache 的快取添加的操作是通過 Editor 完成的,Editor 表示一個快取物件的編輯物件,這里仍然以圖片快取舉例,首先需要獲取圖片url 所對應的 key,然后根據 key 就可以通過 edit() 來獲取 Editor 物件,如果這個快取正在被編輯,那么 edit() 會回傳 null,即 DiskLruCache 不允許同時編輯一個快取物件,之所以要把 url 轉換成 key,是因為圖片的 url 中很可能有特殊字符,這將影響 url 在 Android 中直接使用,一般采用 url 的 md5 值作為 key,如下所示
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
將圖片的 url 轉成 key 后,就可以獲取 Editor 物件了,對于這個 key 來說,如果當前不存在其他 Editor 物件,那么 edit() 就會回傳一個新的 Editor 物件,通過它就可以得到一個檔案輸出流,需要注意的是,由于前面在 DiskLruCache 的 open 方法中設定了一個節點只能有一個資料,因此下面的 DISK_CACHE_INDEX 常量直接設為 0 即可,如下所示
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor ! = null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}
當從網路下載圖片時,圖片就可以通過檔案輸出流寫入到檔案系統上,這個程序的實作如下所示
public boolean downloadUrlToStream(String urlString, utputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection)url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) ! = -1) {
out.write(b);
}
return true;
} catch (IOException e) {
Log.e(TAG, "Download bitmap failed. " + e);
} finally {
if (urlConnection ! = null) {
urlConnection.disconnect();
}
MyUtils.close(out);
MyUtils.close(in);
}
return false;
}
其中 MyUtils 原始碼在這里,之后,還必須通過 Editor 的 commit() 來提交寫入操作,真正地將圖片寫入檔案系統,如果圖片下載程序發生了例外,那么還可以通過 Editor 的 abort() 來回退整個操作,這個程序如下所示
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
經過上面的幾個步驟,圖片已經被正確地寫入到檔案系統了,接下來圖片獲取的操作就不需要請求網路了,
3、DiskLruCache 的查找
和快取的添加程序類似,快取查找程序也需要將 url 轉換為 key,然后通過 DiskLruCache 的 get() 方法得到一個 Snapshot 物件,接著再通過 Snapshot 物件即可得到快取的檔案輸入流,有了檔案輸入流,自然就可以得到 Bitmap 物件了,為了避免加載圖片程序中導致的 OOM 問題,一般不建議直接加載原始圖片,可以通過 BitmapFactory.Options 物件來加載一張縮放后的圖片,但是那種方法對 FileInputStream 的縮放存在問題,原因是 FileInputStream 是一種有序的檔案流,而兩次 decodeStream 呼叫影響了檔案流的位置屬性,導致了第二次 decodeStream 時得到的是 null,為了解決這個問題,可以通過檔案流來得到它所對應的檔案描述符,然后再通過 BitmapFactory.decodeFileDescriptor() 方法來加載一張縮放后的圖片,這個程序的實作如下所示
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot ! = null) {
FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
if (bitmap ! = null) {
addBitmapToMemoryCache(key, bitmap);
}
}
每個物體都是檔案,你可以利用 fileInputStream 讀取出里面的內容,然后做其他操作,上面介紹了 DiskLruCache 的創建、添加和查找程序,除此之外,DiskLruCache 還提供了 remove() 、delete() 等方法用于磁盤快取的洗掉操作,
4、DiskLruCache 的移除
移除快取主要是借助 DiskLruCache 的 remove() 方法實作的,原始碼如下所示
/**
* Drops the entry for {@code key} if it exists and can be removed. Entries
* actively being edited cannot be removed.
*
* @return true if an entry was removed.
*/
public synchronized boolean remove(String key) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null || entry.currentEditor != null) {
return false;
}
for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
if (!file.delete()) {
throw new IOException("failed to delete " + file);
}
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
redundantOpCount++;
journalWriter.append(REMOVE + ' ' + key + '\n');
lruEntries.remove(key);
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return true;
}
remove() 方法中要求傳入一個 key,然后會洗掉這個 key 對應的快取,
示例代碼如下
try {
String imageUrl = "https://chittyo/img.jpghttps://img-blog.csdnimg.cn/2020110614184215.jpg alt="在這里插入圖片描述" />
可以看到,一個名稱很長的檔案和一個 journal 檔案,其中,名稱很長的檔案是被快取的檔案,它的名稱是 MD5 編碼之后的,我們來看看 journal 檔案中的內容是什么樣的吧,如下所示
libcore.io.DiskLruCache
1
1
1
DIRTY 27c7e00adbacc71dc793e5e7bf02f861
CLEAN 27c7e00adbacc71dc793e5e7bf02f861 1208
READ 27c7e00adbacc71dc793e5e7bf02f861
DIRTY b80f9eec4b616dc6682c7fa8bas2061f
CLEAN b80f9eec4b616dc6682c7fa8bas2061f 1208
READ b80f9eec4b616dc6682c7fa8bas2061f
DIRTY be3fgac81c12a08e89088555d85dfd2b
CLEAN be3fgac81c12a08e89088555d85dfd2b 99
READ be3fgac81c12a08e89088555d85dfd2b
DIRTY 536990f4dbddfghcfbb8f350a941wsxd
REMOVE 536990f4dbddfghcfbb8f350a941wsxd
來看一下原始碼注釋
/*
* This cache uses a journal file named "journal". A typical journal file
* looks like this:
* libcore.io.DiskLruCache
* 1
* 100
* 2
*
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
* DIRTY 335c4c6028171cfddfbaae1a9c313c52
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
* REMOVE 335c4c6028171cfddfbaae1a9c313c52
* DIRTY 1ab96a171faeeee38496d8b330771a7a
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
* READ 335c4c6028171cfddfbaae1a9c313c52
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
*
* The first five lines of the journal form its header. They are the
* constant string "libcore.io.DiskLruCache", the disk cache's version,
* the application's version, the value count, and a blank line.
*
* Each of the subsequent lines in the file is a record of the state of a
* cache entry. Each line contains space-separated values: a state, a key,
* and optional state-specific values.
* o DIRTY lines track that an entry is actively being created or updated.
* Every successful DIRTY action should be followed by a CLEAN or REMOVE
* action. DIRTY lines without a matching CLEAN or REMOVE indicate that
* temporary files may need to be deleted.
* o CLEAN lines track a cache entry that has been successfully published
* and may be read. A publish line is followed by the lengths of each of
* its values.
* o READ lines track accesses for LRU.
* o REMOVE lines track entries that have been deleted.
*
* The journal file is appended to as cache operations occur. The journal may
* occasionally be compacted by dropping redundant lines. A temporary file named
* "journal.tmp" will be used during compaction; that file should be deleted if
* it exists when the cache is opened.
*/
來看一下前五行:
libcore.io.DiskLruCache 是固定字串,表明使用的是 DiskLruCache 技術;- DiskLruCache 的版本號,原始碼中為常量 1;
- APP 的版本號,即我們在 open() 方法里傳入的版本號;
- valueCount,這個值也是在 open() 方法中傳入的,指每個 key 對應幾個檔案,通常情況下都為 1;
- 空行
前五行是該檔案的檔案頭,DiskLruCache 初始化的時候,如果該檔案存在,就需要校驗該檔案頭,
接下來看下操作記錄:以 DIRTY 為前綴開始的行,后面是快取檔案的 key,DIRTY 英文是“臟的” 的意思,此處譯為臟資料,每當我們呼叫一次 DiskLruCache 的 edit() 方法時,都會向 journal 檔案中寫入一條 DIRTY 記錄,表示我們正準備寫入一條快取資料,但不知結果如何,然后呼叫 commit() 方法表示寫入快取成功,這時會向 journal 中寫入一條 CLEAN 記錄,意味著這條 “臟” 資料被 “洗干凈了” ,呼叫 abort() 方法表示寫入快取失敗,這時會向 journal 中寫入一條 REMOVE 記錄,也就是說,每一行 DIRTY 的 key,后面都應該有一行對應的 CLEAN 或者 REMOVE 的記錄,否則這條資料就是 “臟” 的,會被自動洗掉掉,
REMOVE 除了上述的情況,當你自己手動呼叫 remove(key) 方法的時候也會寫入一條 REMOVE 記錄,
如果你足夠細心的話應該還會注意到,第七行的那條記錄,除了 CLEAN 前綴和 key 之外,后面還有一個1208,這是什么意思呢?其實,DiskLruCache 會在每一行 CLEAN 記錄的最后加上該條快取資料的大小,以位元組為單位,1208 也就是我們快取檔案的位元組數了,原始碼中的 size() 方法可以獲取到當前快取路徑下所有快取資料的總位元組數,其實它的作業原理就是把 journal 檔案中所有 CLEAN 記錄的位元組數相加,回傳求出的總和,
前綴是 READ 的記錄,每當我們呼叫 get() 方法去讀取一條快取資料時,就會向 journal 檔案中寫入一條 READ 記錄,因此,圖片和資料量都非常大的 APP 的 journal 檔案中就可能會有大量的 READ 記錄,那如果我不停地頻繁操作的話,就會不斷地向 journal 檔案中寫入資料,那這樣 journal 檔案豈不是會越來越大?這倒不必擔心,DiskLruCache 中使用了一個 redundantOpCount 變數來記錄用戶操作的次數,每執行一次寫入、讀取或移除快取的操作,這個變數值都會加 1,當變數值達到 2000 的時候就會觸發重構 journal 的事件,這時會自動把 journal 中一些多余的、不必要的記錄全部清除掉,保證 journal 檔案的大小始終保持在一個合理的范圍內,
2、DiskLruCache 的 open()
/**
* Opens the cache in {@code directory}, creating a cache if none exists
* there.
*
* @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
* @throws IOException if reading or writing the cache directory fails
*/
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
// If a bkp file exists, use it instead.
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
if (backupFile.exists()) {
File journalFile = new File(directory, JOURNAL_FILE);
// If journal file also exists just delete backup file.
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}
// Prefer to pick up where we left off.
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
return cache;
} catch (IOException journalIsCorrupt) {
System.out
.println("DiskLruCache "
+ directory
+ " is corrupt: "
+ journalIsCorrupt.getMessage()
+ ", removing");
cache.delete();
}
}
// Create a new empty cache.
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
首先檢查 journal 的備份檔案是否存在( journal.bkp),如果備份存在,然后檢查 journal 檔案是否存在,如果 journal 檔案存在,備份檔案就可以洗掉了;如果 journal 檔案不存在,將備份檔案檔案重命名為 journal 檔案,
然后檢查 journal 檔案是否存在:
- 如果不存在,創建 directory,重新構造 DiskLruCache,再呼叫
rebuildJournal() 建立 journal 檔案,
/**
* Creates a new journal that omits redundant information. This replaces the
* current journal if it exists.
*/
private synchronized void rebuildJournal() throws IOException {
if (journalWriter != null) {
journalWriter.close();
}
Writer writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
try {
writer.write(MAGIC);
writer.write("\n");
writer.write(VERSION_1);
writer.write("\n");
writer.write(Integer.toString(appVersion));
writer.write("\n");
writer.write(Integer.toString(valueCount));
writer.write("\n");
writer.write("\n");
for (Entry entry : lruEntries.values()) {
if (entry.currentEditor != null) {
writer.write(DIRTY + ' ' + entry.key + '\n');
} else {
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
}
}
} finally {
writer.close();
}
if (journalFile.exists()) {
renameTo(journalFile, journalFileBackup, true);
}
renameTo(journalFileTmp, journalFile, false);
journalFileBackup.delete();
journalWriter = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
}
可以看到首先構建一個 journal.tmp 檔案,然后寫入檔案頭(5行),然后遍歷 lruEntries( LinkedHashMap<String, Entry> lruEntries =
new LinkedHashMap<String, Entry>(0, 0.75f, true); ),此時 lruEntries 里沒有任何資料,接下來將 tmp 檔案重命名為 journal 檔案,這樣一個 journal 檔案便生成了,
- 如果存在,那么呼叫
readJournal()
private void readJournal() throws IOException {
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
try {
String magic = reader.readLine();
String version = reader.readLine();
String appVersionString = reader.readLine();
String valueCountString = reader.readLine();
String blank = reader.readLine();
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(reader.readLine());
lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
redundantOpCount = lineCount - lruEntries.size();
// If we ended on a truncated line, rebuild the journal before appending to it.
if (reader.hasUnterminatedLine()) {
rebuildJournal();
} else {
journalWriter = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(journalFile, true), Util.US_ASCII));
}
} finally {
Util.closeQuietly(reader);
}
}
首先校驗檔案頭,接下來呼叫 readJournalLine 按行讀取內容,
private void readJournalLine(String line) throws IOException {
int firstSpace = line.indexOf(' ');
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
int keyBegin = firstSpace + 1;
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
if (secondSpace == -1) {
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
lruEntries.remove(key);
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}
Entry entry = lruEntries.get(key);
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
String[] parts = line.substring(secondSpace + 1).split(" ");
entry.readable = true;
entry.currentEditor = null;
entry.setLengths(parts);
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
entry.currentEditor = new Editor(entry);
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
// This work was already done by calling lruEntries.get().
} else {
throw new IOException("unexpected journal line: " + line);
}
}
journal 日志每一行中的各個部分都是用 ' ' 空格來分割的,所以先用 空格來截取一下,拿到 key,然后判斷 firstSpace 是 REMOVE 就會呼叫 lruEntries.remove(key); ,若不是 REMOVE ,如果該 key 沒有加入到 lruEntries ,則創建并且加入,然后繼續判斷 firstSpace ,若是 CLEAN ,則初始化 entry ,設定 readable=true , currentEditor 為 null ,初始化長度等,若是 DIRTY ,則設定 currentEditor 物件,若是 READ,無操作,
一般正常操作下 DIRTY 不會單獨出現,會和 REMOVE or CLEAN 成對出現;經過上面這個流程,基本上加入到 lruEntries 里面的只有 CLEAN 且沒有被 REMOVE 的 key,
然后我們回到 readJournal 方法,在我們按行讀取的時候,會記錄一下 lineCount ,然后賦值給 redundantOpCount ,該變數記錄的是多余的記錄條數( redundantOpCount = lineCount - lruEntries.size(); 檔案的行數-真正可以的 key 的行數),最后,如果讀取程序中發現 journal 檔案有問題,則重建 journal 檔案,沒有問題的話,初始化下 journalWriter,關閉 reader,
我們再回到 open() 方法中,readJournal 完成了,會繼續呼叫 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 {
deleteIfExists(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++) {
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));
}
i.remove();
}
}
}
計算快取初始大小,賦值給 size,并收集垃圾作為打開快取的一部分,對于所有非法 DIRTY 狀態(就是 DIRTY 單獨出現的)的 entry,如果存在 檔案則洗掉,并且從 lruEntries 中移除,此時,剩下的就只有 CLEAN 狀態的 key 記錄了,
到此 DiskLruCache 就初始化完畢了,為了方便記憶,我們來捋一下流程:
根據我們傳入的 directory,去找 journal 檔案,如果沒找到,則創建一個,只寫入檔案頭 (5行) ,如果找到,則遍歷該檔案,將里面所有的 CLEAN 記錄的 key,存到 lruEntries 中,
經過 open 以后,journal 檔案肯定存在了,lruEntries 里面肯定有值了,size 為當前所有物體占據的容量,
3、DiskLruCache 的 edit()
/**
* Returns an editor for the entry named {@code key}, or null if another
* edit is in progress.
*/
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Snapshot is stale.
}
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
return null; // Another edit is in progress.
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
// Flush the journal before creating files to prevent file leaks.
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
}
private void validateKey(String key) {
Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
if (!matcher.matches()) {
throw new IllegalArgumentException("keys must match regex "
+ STRING_KEY_PATTERN + ": \"" + key + "\"");
}
}
private void checkNotClosed() {
if (journalWriter == null) {
throw new IllegalStateException("cache is closed");
}
}
首先驗證 key,必須是由字母、數字、下劃線、橫線(-) 組成,且長度在 1-120 之間,然后通過 key 獲取物體 Entry ,如果 Entry 不存在,則創建一個 Entry 加入到 lruEntries 中;如果存在且不是正在編輯的物體,則直接使用,然后為 entry.currentEditor 進行賦值為 new Editor(entry); ,最后在 journal 檔案中寫入一條 DIRTY 記錄,代表這個檔案正在被操作,拿到 editor 物件以后,就是去呼叫 newOutputStream 去獲得一個檔案輸入流了,
/**
* 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.
*/
public OutputStream newOutputStream(int index) throws IOException {
if (index < 0 || index >= valueCount) {
throw new IllegalArgumentException("Expected index " + index + " to "
+ "be greater than 0 and less than the maximum value count "
+ "of " + valueCount);
}
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
if (!entry.readable) {
written[index] = true;
}
File dirtyFile = entry.getDirtyFile(index);
FileOutputStream outputStream;
try {
outputStream = new FileOutputStream(dirtyFile);
} catch (FileNotFoundException e) {
// Attempt to recreate the cache directory.
directory.mkdirs();
try {
outputStream = new FileOutputStream(dirtyFile);
} catch (FileNotFoundException e2) {
// We are unable to recover. Silently eat the writes.
return NULL_OUTPUT_STREAM;
}
}
return new FaultHidingOutputStream(outputStream);
}
}
首先校驗 index 是否在 (0, valueCount] 范圍內,一般我們使用都是一個 key 對應一個檔案,所以傳入的基本都是 0,接下來就是通過 entry.getDirtyFile(index); 拿到一個 dirtyFile 物件,其實就是個中轉檔案,檔案格式為 key.index.tmp ,將這個檔案的 FileOutputStream 通過 FaultHidingOutputStream 封裝下傳給我們,
最后,我們通過 outputStream 寫入資料以后,需要呼叫 commit 方法
/**
* Commits this edit so it is visible to readers. This releases the
* edit lock so another edit may be started on the same key.
*/
public void commit() throws IOException {
if (hasErrors) {
completeEdit(this, false);
remove(entry.key); // The previous entry is stale.
} else {
completeEdit(this, true);
}
committed = true;
}
首先通過 hasErrors 判斷,是否有錯誤發生,如果有就呼叫 completeEdit(this, false); 和 remove(entry.key); ;如果沒有就呼叫 completeEdit(this, true); ,
那么這里這個 hasErrors 哪來的呢?還記得上面 newOutputStream 的時候,回傳了一個 outputStream,這個 outputStream 是 FileOutputStream ,但是經過了 FaultHidingOutputStream 的封裝,這個類實際上就是重寫了 FilterOutputStream 的 write 相關方法,將所有的 IOException 給屏蔽了,如果發生 IOException 就將 hasErrors 賦值為 true,這樣的設計的好處是不直接將 OutputStream 回傳給用戶,如果出錯可以檢測到,不需要用戶手動去呼叫一些操作,
下面看看 completeEdit 方法
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
if (entry.currentEditor != editor) {
throw new IllegalStateException();
}
// If this edit is creating the entry for the first time, every index must have a value.
if (success && !entry.readable) {
for (int i = 0; i < valueCount; i++) {
if (!editor.written[i]) {
editor.abort();
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
}
if (!entry.getDirtyFile(i).exists()) {
editor.abort();
return;
}
}
}
for (int i = 0; i < valueCount; i++) {
File dirty = entry.getDirtyFile(i);
if (success) {
if (dirty.exists()) {
File clean = entry.getCleanFile(i);
dirty.renameTo(clean);
long oldLength = entry.lengths[i];
long newLength = clean.length();
entry.lengths[i] = newLength;
size = size - oldLength + newLength;
}
} else {
deleteIfExists(dirty);
}
}
redundantOpCount++;
entry.currentEditor = null;
if (entry.readable | success) {
entry.readable = true;
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
lruEntries.remove(entry.key);
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
}
journalWriter.flush();
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
}
首先判斷 if (success && !entry.readable) 是否成功,且是第一次寫入(如果以前這個記錄有值,則 readable = true ),內部的判斷,我們都不會走,因為 written[i] 在 newOutputStream 的時候被寫入 true 了,而且正常情況下,getDirtyFile 是存在的,
然后如果成功,將 dirtyFile 進行重命名為 cleanFile,檔案名為:key.index,然后重繪 size 的長度,如果失敗,則洗掉 dirtyFile .
然后,如果成功或者 readable 為 true ,將 readable 設定為 true ,寫入一條 CLEAN 記錄,如果第一次提交且失敗,那么就會從 lruEntries.remove(key) ,寫入一條 REMOVE 記錄,
寫入快取,要判斷是否超過了最大 size ,或者需要重建 journal 檔案
/**
* We only rebuild the journal when it will halve the size of the journal
* and eliminate at least 2000 ops.
*/
private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold //
&& redundantOpCount >= lruEntries.size();
}
如果 redundantOpCount 達到 2000,且超過了 lruEntries.size() 就會重建,這里就可以看到 redundantOpCount 的作用了,防止 journal 檔案過大,到此我們的存入快取就分析完成了,
總結:首先呼叫 editor,拿到指定的 dirtyFile 的 OutputStream ,然后開始寫操作,寫完后,記得呼叫 commit .
commit 中會檢測你是否發生 IOException ,若無,則將 dirtyFile -> cleanFile ,將 readable = true ,寫入 CLEAN 記錄,如若發生錯誤,則洗掉 dirtyFile ,從 lruEntries 中移除,然后寫入一條 REMOVE 記錄,
4、DiskLruCache 的 get()
/**
* Returns a snapshot of the entry named {@code key}, or null if it doesn't
* exist is not currently readable. If a value is returned, it is moved to
* the head of the LRU queue.
*/
public synchronized Snapshot get(String key) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}
if (!entry.readable) {
return null;
}
// Open 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.
InputStream[] ins = new InputStream[valueCount];
try {
for (int i = 0; i < valueCount; i++) {
ins[i] = new FileInputStream(entry.getCleanFile(i));
}
} catch (FileNotFoundException e) {
// A file must have been deleted manually!
for (int i = 0; i < valueCount; i++) {
if (ins[i] != null) {
Util.closeQuietly(ins[i]);
} else {
break;
}
}
return null;
}
redundantOpCount++;
journalWriter.append(READ + ' ' + key + '\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
}
如果根據 key 取到的 Entry 為 null ,或者 readable = false ,則回傳 null,否則將 cleanFile 的 FileInputStream 進行封裝回傳 Snapshot,且寫入一條 READ 陳述句,
然后 getInputStream 就是回傳該 FileInputStream 了,
5、DiskLruCache 的 remove()
/**
* Drops the entry for {@code key} if it exists and can be removed. Entries
* actively being edited cannot be removed.
*
* @return true if an entry was removed.
*/
public synchronized boolean remove(String key) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null || entry.currentEditor != null) {
return false;
}
for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
if (file.exists() && !file.delete()) {
throw new IOException("failed to delete " + file);
}
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
redundantOpCount++;
journalWriter.append(REMOVE + ' ' + key + '\n');
lruEntries.remove(key);
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return true;
}
如果物體存在且不是正在被編輯,就可以直接進行洗掉,然后寫入一條 REMOVE 記錄,
remove() 與 open() 對應,在使用完成 cache 后可以手動關閉,
6、DiskLruCache 的 close()
/**
* Closes this cache. Stored values will remain on the filesystem.
*/
public synchronized void close() throws IOException {
if (journalWriter == null) {
return; // already closed
}
for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
if (entry.currentEditor != null) {
entry.currentEditor.abort();
}
}
trimToSize();
journalWriter.close();
journalWriter = null;
}
這個方法用于將 DiskLruCache 關閉掉,關閉前,會判斷所有正在編輯的物體,呼叫 abort() 方法,最后關閉 journalWriter ,close() 是和 open() 方法對應的一個方法,關閉之后就不能再呼叫 DiskLruCache 中任何操作快取資料的方法,通常只應該在 Activity 的 onDestroy() 方法中去呼叫 close() 方法,
其中 abort() 方法是存盤失敗時的邏輯,中止此編輯,這將釋放編輯鎖,以便其他操作可以獲取編輯鎖,操作同一個 key,
/**
* Aborts this edit. This releases the edit lock so another edit may be
* started on the same key.
*/
public void abort() throws IOException {
completeEdit(this, false);
}
7、DiskLruCache 的 delete()
/**
* Closes the cache and deletes all of its stored values. This will delete
* all files in the cache directory including files that weren't created by the cache.
*/
public void delete() throws IOException {
close();
IoUtils.deleteContents(directory);
}
這個方法用于將所有的快取資料全部洗掉,比如 APP 中手動清理快取功能,只需要呼叫一下 DiskLruCache 的 delete() 方法就可以實作了,
8、DiskLruCache 的 size()
/**
* Returns the number of bytes currently being used to store the values in
* this cache. This may be greater than the max size if a background
* deletion is pending.
*/
public synchronized long size() {
return size;
}
這個方法會回傳當前快取路徑下所有快取資料的總位元組數,以 byte 為單位,如果應用程式中需要在界面上顯示當前快取資料的總大小,就可以通過呼叫這個方法計算出來,如果后臺洗掉掛起,則此值可能大于最大大小,
9、DiskLruCache 的 flush()
/**
* Force buffered operations to the filesystem.
*/
public synchronized void flush() throws IOException {
checkNotClosed();
trimToSize();
journalWriter.flush();
}
這個方法用于將記憶體中的操作記錄同步到日志檔案(也就是 journal 檔案)當中,這個方法非常重要,因為 DiskLruCache 能夠正常作業是依賴于 journal 檔案中的內容,其實并不是每次寫入快取都要呼叫一次 flush() 方法,頻繁地呼叫并不會帶來任何好處,只會額外增加同步 journal 檔案的時間,比較標準的做法是在 Activity 的 onPause() 方法中呼叫一次 flush() 方法就可以了,
至此,我們的原始碼分析就結束了,可以看到 DiskLruCache ,利用一個 journal 檔案,保證了 cache 物體的可用性(只有 CLEAN 的可用),且獲取檔案的長度的時候可以通過在該檔案的記錄中讀取,利用 FaultHidingOutputStream 對 FileOutPutStream 在寫入檔案程序中是否發生錯誤進行捕獲,而不是讓用戶手動去呼叫出錯后的處理方法,其內部的很多細節都很值得我們推敲和學習,不過也可以看到,存取的操作不是特別的方便易用,需要我們自己去操作檔案流,
三、DiskLruCache 完整原始碼
DiskLruCache 的完整原始碼
Github - JakeWharton/DiskLruCache
本站免費下載鏈接
四、參考資料
任玉剛的《Android 開發藝術探索》一書,
郭霖 CSDN
鴻洋 CSDN
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/213647.html
標籤:其他
下一篇:Android WebView
