DiskLruCache是谷歌推薦的用來實現硬盤緩存的類,今天我們開始對於DiskLruCache的學習。DiskLruCache的測試代碼: DiskLruCache的測試代碼下載。關於FidkLruCache的使用,請參見我的博客:android基礎---->LruCache的使用及原理
目錄導航
DiskLruCache緩存的代碼實例
我們通過一個案例來體會DiskLruCache的使用及執行的流程,在我們的項目中包含DiskLruCache文件:http://pan.baidu.com/s/1slR6pg5。項目結構如下:
一、 DiskLruCache的是一個final類,不能繼承。如果我們要創建一個DiskLruCache的實例,則需要調用它的open()方法,如下:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
在AndroidManifest.xml加入網絡權限和sd卡寫入權限:
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
項目中創建並打開緩存:
try { File cacheDir = getDiskCacheDir(this, "bitmap"); if (!cacheDir.exists()) { cacheDir.mkdirs(); } mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(this), 1, 10 * 1024 * 1024); } catch (Exception e) { e.printStackTrace(); }
二、 從網絡得到圖片資源,並寫入緩存:
private void saveCache() { new Thread(new Runnable() { @Override public void run() { try { String key = hashKeyForDisk(imageUrl); DiskLruCache.Editor editor = null; editor = mDiskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); if (downloadUrlToStream(imageUrl, outputStream)) { editor.commit(); } else { editor.abort(); } } //頻繁的flush mDiskLruCache.flush(); handler.post(new Runnable() { @Override public void run() { textView.setText("saveCache done,the bitmap is ready"); } }); } catch (IOException e) { e.printStackTrace(); } } }).start(); }
downloadUrlToStream方法用於從網絡上獲取圖片資源:
private boolean downloadUrlToStream(String urlString, OutputStream 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(), 8 * 1024); out = new BufferedOutputStream(outputStream, 8 * 1024); int b; while ((b = in.read()) != -1) { out.write(b); } return true; } catch (final IOException e) { e.printStackTrace(); } finally { if (urlConnection != null) { urlConnection.disconnect(); } try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (final IOException e) { e.printStackTrace(); } } return false; }
三、 由於涉及到key的因素,我們寫一個MD5對key進行編碼:
public String hashKeyForDisk(String key) { String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(key.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(key.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(); }
四、 從緩存中讀取數據:
private void readCache() { //讀取緩存 try { DiskLruCache.Snapshot snapshot = mDiskLruCache.get(hashKeyForDisk(imageUrl)); if (snapshot != null) { InputStream is = snapshot.getInputStream(0); Bitmap bitmap = BitmapFactory.decodeStream(is); imageView.setImageBitmap(bitmap); } else { imageView.setImageBitmap(null); } } catch (IOException e) { e.printStackTrace(); } }
五、 移除緩存:
private void removeCache() { try { mDiskLruCache.remove(hashKeyForDisk(imageUrl)); } catch (IOException e) { e.printStackTrace(); } }
六、 清空緩存:
private void deleteCache() { try { //delete()方法內部會調用close() mDiskLruCache.delete(); } catch (IOException e) { e.printStackTrace(); } }
七、 得到緩存的大小:
private void getCacheSize() { textView.setText("cache size : " + mDiskLruCache.size() + "B"); }
八、 在onDesctory方法中關閉緩存:
@Override protected void onDestroy() { super.onDestroy(); try { //關閉DiskLruCache,與open對應 mDiskLruCache.close(); } catch (IOException e) { e.printStackTrace(); } }
DiskLruCache的原理分析
經過上述實例的說明,我們對DiskLruCache的使用已經有了一些認識。現在我們開始DiskLruCache的原理分析:
創建打開緩存
一、 當創建打開緩存:mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(this), 1, 10 * 1024 * 1024);
創建日志journal文件,並且初始化journalWriter:
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"); } // 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(); cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true), IO_BUFFER_SIZE); return cache; } catch (IOException journalIsCorrupt) { cache.delete(); } } // create a new empty cache directory.mkdirs(); cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); cache.rebuildJournal(); return cache; }
在DiskLruCache的構造方法中,創建journal和journal.tmp文件。
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { this.directory = directory; this.appVersion = appVersion; this.journalFile = new File(directory, JOURNAL_FILE); this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP); this.valueCount = valueCount; this.maxSize = maxSize; }
在rebuildJournal方法中,在journalFileTmp臨時文件中,寫入一些數據,最后重命名為journalFile:
private synchronized void rebuildJournal() throws IOException { if (journalWriter != null) { journalWriter.close(); } Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE); 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'); } } writer.close(); journalFileTmp.renameTo(journalFile); journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE); }
二、 創建完成之后 ,在sd中存在journal文件:內容如下:
libcore.io.DiskLruCache // MAGIC 1 // VERSION_1 1 // appVersion 1 // valueCount
寫入緩存
二、 然后是這個代碼:DiskLruCache.Editor editor = mDiskLruCache.edit(key);其中的lruEntries是一個LinkedHashMap,用於記錄資源的key與value。
當第一次edit時,lruEntries.get(key)返回的是空。這里會創建一個Entry對象,並在日志文件中寫入DIRTY + ' ' + key + '\n'內容。最后返回包含這個entry的Editor。
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; }
三、 OutputStream outputStream = editor.newOutputStream(0);downloadUrlToStream(imageUrl, outputStream);得到文件輸出流,將從網絡上請求到的資源寫入到文件中。
這里關注下outputStream,它是由newOutputStream方法得到的:
public OutputStream newOutputStream(int index) throws IOException { synchronized (DiskLruCache.this) { if (entry.currentEditor != this) { throw new IllegalStateException(); } return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index))); } }
getDirtyFile文件是一個臨時文件,也就是網絡文件寫入到這個臨時文件當中:
public File getDirtyFile(int i) { return new File(directory, key + "." + i + ".tmp"); }
四、 接着執行到了editor.commit()方法,如果沒有出現錯誤的話:
創建一個key + "." + i的文件,將上述的dirty文件重命名為key + "." + i的文件。更新已經緩存的大小,並且刪除上述的dirty文件。
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 (!entry.getDirtyFile(i).exists()) { editor.abort(); throw new IllegalStateException("edit didn't create file " + i); } } } 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'); } if (size > maxSize || journalRebuildRequired()) { executorService.submit(cleanupCallable); } }
五、 當執行完寫入之后,日志文件如下內容:abb7d9d7add6b9fba5314aec6e60c9e6就是上述MD5生成的key,15417是代表緩存的大小。並生成一個abb7d9d7add6b9fba5314aec6e60c9e6.0文件.
libcore.io.DiskLruCache 1 1 1 DIRTY abb7d9d7add6b9fba5314aec6e60c9e6 CLEAN abb7d9d7add6b9fba5314aec6e60c9e6 15417
從緩存中讀取
一、 mDiskLruCache.get(key,以下是關鍵代碼,中間省略了代碼);
Entry entry = lruEntries.get(key); // 根據key從map中得到相應的value .... InputStream[] ins = new InputStream[valueCount]; try { for (int i = 0; i < valueCount; i++) { ins[i] = new FileInputStream(entry.getCleanFile(i)); // 得到clearn文件輸入流 } } catch (FileNotFoundException e) { // a file must have been deleted manually! return null; } journalWriter.append(READ + ' ' + key + '\n'); // 在日志文件中寫入內容 .... return new Snapshot(key, entry.sequenceNumber, ins); // 返回一個Snapshot對象
二、 snapshot.getInputStream(0)得到clean文件的輸入流,然后通過Bitmap bitmap = BitmapFactory.decodeStream(is);方法得到緩存在硬盤的圖片。