做者:郭霖老師,《第一行代碼》做者,開源框架LitePal做者java
http://blog.csdn.net/guolin_blog/article/details/28863651android
記得在很早以前,我有寫過一篇文章 Android高效加載大圖、多圖解決方案,有效避免程序OOM ,這篇文章是翻譯自Android Doc的,其中防止多圖OOM的核心解決思路就是使用LruCache技術。但LruCache只是管理了內存中圖片的存儲與釋放,若是圖片從內存中被移除的話,那麼又須要從網絡上從新加載一次圖片,這顯然很是耗時。對此,Google又提供了一套硬盤緩存的解決方案:DiskLruCache(非Google官方編寫,但得到官方認證)。只惋惜,Android Doc中並無對DiskLruCache的用法給出詳細的說明,而網上關於DiskLruCache的資料也少之又少,所以今天我準備專門寫一篇博客來詳細講解DiskLruCache的用法,以及分析它的工做原理,這應該也是目前網上關於DiskLruCache最詳細的資料了。緩存
那麼咱們先來看一下有哪些應用程序已經使用了DiskLruCache技術。在我所接觸的應用範圍裏,Dropbox、Twitter、網易新聞等都是使用DiskLruCache來進行硬盤緩存的,其中Dropbox和Twitter大多數人應該都沒用過,那麼咱們就從你們最熟悉的網易新聞開始着手分析,來對DiskLruCache有一個最初的認識吧。網絡
相信全部人都知道,網易新聞中的數據都是從網絡上獲取的,包括了不少的新聞內容和新聞圖片,以下圖所示:app
可是不知道你們有沒有發現,這些內容和圖片在從網絡上獲取到以後都會存入到本地緩存中,所以即便手機在沒有網絡的狀況下依然可以加載出之前瀏覽過的新聞。而使用的緩存技術不用多說,天然是DiskLruCache了,那麼首先第一個問題,這些數據都被緩存在了手機的什麼位置呢?框架
其實DiskLruCache並無限制數據的緩存位置,能夠自由地進行設定,可是一般狀況下多數應用程序都會將緩存的位置選擇爲 /sdcard/Android/data/<application package>/cache 這個路徑。選擇在這個位置有兩點好處:第一,這是存儲在SD卡上的,所以即便緩存再多的數據也不會對手機的內置存儲空間有任何影響,只要SD卡空間足夠就行。第二,這個路徑被Android系統認定爲應用程序的緩存路徑,當程序被卸載的時候,這裏的數據也會一塊兒被清除掉,這樣就不會出現刪除程序以後手機上還有不少殘留數據的問題。ide
那麼這裏仍是以網易新聞爲例,它的客戶端的包名是com.netease.newsreader.activity,所以數據緩存地址就應該是 /sdcard/Android/data/com.netease.newsreader.activity/cache ,咱們進入到這個目錄中看一下,結果以下圖所示:學習
能夠看到有不少個文件夾,由於網易新聞對多種類型的數據都進行了緩存,這裏簡單起見咱們只分析圖片緩存就好,因此進入到bitmap文件夾當中。而後你將會看到一堆文件名很長的文件,這些文件命名沒有任何規則,徹底看不懂是什麼意思,但若是你一直向下滾動,將會看到一個名爲journal的文件,以下圖所示:gradle
那麼這些文件到底都是什麼呢?看到這裏相信有些朋友已是一頭霧水了,這裏我簡單解釋一下。上面那些文件名很長的文件就是一張張緩存的圖片,每一個文件都對應着一張圖片,而journal文件是DiskLruCache的一個日誌文件,程序對每張圖片的操做記錄都存放在這個文件中,基本上看到journal這個文件就標誌着該程序使用DiskLruCache技術了。ui
好了,對DiskLruCache有了最初的認識以後,下面咱們來學習一下DiskLruCache的用法吧。因爲DiskLruCache並非由Google官方編寫的,因此這個類並無被包含在Android API當中,咱們須要將這個類從網上下載下來,而後手動添加到項目當中。DiskLruCache的源碼在Google Source上,地址以下:
若是Google Source打不開的話,也能夠 點擊這裏 下載DiskLruCache的源碼。下載好了源碼以後,只須要在項目中新建一個libcore.io包,而後將DiskLruCache.java文件複製到這個包中便可。
這樣的話咱們就把準備工做作好了,下面看一下DiskLruCache到底該如何使用。首先你要知道,DiskLruCache是不能new出實例的,若是咱們要建立一個DiskLruCache的實例,則須要調用它的open()方法,接口以下所示:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
open()方法接收四個參數,第一個參數指定的是數據的緩存地址,第二個參數指定當前應用程序的版本號,第三個參數指定同一個key能夠對應多少個緩存文件,基本都是傳1,第四個參數指定最多能夠緩存多少字節的數據。
其中緩存地址前面已經說過了,一般都會存放在 /sdcard/Android/data/<application package>/cache 這個路徑下面,但同時咱們又須要考慮若是這個手機沒有SD卡,或者SD正好被移除了的狀況,所以比較優秀的程序都會專門寫一個方法來獲取緩存地址,以下所示:
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、object等文件夾。
接着是應用程序版本號,咱們可使用以下代碼簡單地獲取到當前應用程序的版本號:
public int getAppVersion(Context context) {
try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionCode; } catch (NameNotFoundException e) { e.printStackTrace(); } return 1; }
須要注意的是,每當版本號改變,緩存路徑下存儲的全部數據都會被清除掉,由於DiskLruCache認爲當應用程序有版本更新的時候,全部的數據都應該從網上從新獲取。
後面兩個參數就沒什麼須要解釋的了,第三個參數傳1,第四個參數一般傳入10M的大小就夠了,這個能夠根據自身的狀況進行調節。
所以,一個很是標準的open()方法就能夠這樣寫:
DiskLruCache mDiskLruCache = null;
try {
File cacheDir = getDiskCacheDir(context, "bitmap"); if (!cacheDir.exists()) { cacheDir.mkdirs(); } mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024); } catch (IOException e) { e.printStackTrace(); }
首先調用getDiskCacheDir()方法獲取到緩存地址的路徑,而後判斷一下該路徑是否存在,若是不存在就建立一下。接着調用DiskLruCache的open()方法來建立實例,並把四個參數傳入便可。
有了DiskLruCache的實例以後,咱們就能夠對緩存的數據進行操做了,操做類型主要包括寫入、訪問、移除等,咱們一個個進行學習。
先來看寫入,好比說如今有一張圖片,地址是http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg,那麼爲了將這張圖片下載下來,就能夠這樣寫:
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; }
這段代碼至關基礎,相信你們都看得懂,就是訪問urlString中傳入的網址,並經過outputStream寫入到本地。有了這個方法以後,下面咱們就可使用DiskLruCache來進行寫入了,寫入的操做是藉助DiskLruCache.Editor這個類完成的。相似地,這個類也是不能new的,須要調用DiskLruCache的edit()方法來獲取實例,接口以下所示:
public Editor edit(String key) throws IOException
能夠看到,edit()方法接收一個參數key,這個key將會成爲緩存文件的文件名,而且必需要和圖片的URL是一一對應的。那麼怎樣才能讓key和圖片的URL可以一一對應呢?直接使用URL來做爲key?不太合適,由於圖片URL中可能包含一些特殊字符,這些字符有可能在命名文件時是不合法的。其實最簡單的作法就是將圖片的URL進行MD5編碼,編碼後的字符串確定是惟一的,而且只會包含0-F這樣的字符,徹底符合文件的命名規則。
那麼咱們就寫一個方法用來將字符串進行MD5編碼,代碼以下所示:
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(); }
代碼很簡單,如今咱們只須要調用一下hashKeyForDisk()方法,並把圖片的URL傳入到這個方法中,就能夠獲得對應的key了。
所以,如今就能夠這樣寫來獲得一個DiskLruCache.Editor的實例:
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Editor editor = mDiskLruCache.edit(key);
有了DiskLruCache.Editor的實例以後,咱們能夠調用它的newOutputStream()方法來建立一個輸出流,而後把它傳入到downloadUrlToStream()中就能實現下載並寫入緩存的功能了。注意newOutputStream()方法接收一個index參數,因爲前面在設置valueCount的時候指定的是1,因此這裏index傳0就能夠了。在寫入操做執行完以後,咱們還須要調用一下commit()方法進行提交才能使寫入生效,調用abort()方法的話則表示放棄這次寫入。
所以,一次完整寫入操做的代碼以下所示:
new Thread(new Runnable() { @Override public void run() { try { String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); if (downloadUrlToStream(imageUrl, outputStream)) { editor.commit(); } else { editor.abort(); } } mDiskLruCache.flush(); } catch (IOException e) { e.printStackTrace(); } } }).start();
因爲這裏調用了downloadUrlToStream()方法來從網絡上下載圖片,因此必定要確保這段代碼是在子線程當中執行的。注意在代碼的最後我還調用了一下flush()方法,這個方法並非每次寫入都必需要調用的,但在這裏卻不可缺乏,我會在後面說明它的做用。
如今的話緩存應該是已經成功寫入了,咱們進入到SD卡上的緩存目錄裏看一下,以下圖所示:
能夠看到,這裏有一個文件名很長的文件,和一個journal文件,那個文件名很長的文件天然就是緩存的圖片了,由於是使用了MD5編碼來進行命名的。
緩存已經寫入成功以後,接下來咱們就該學習一下如何讀取了。讀取的方法要比寫入簡單一些,主要是藉助DiskLruCache的get()方法實現的,接口以下所示:
public synchronized Snapshot get(String key) throws IOException
很明顯,get()方法要求傳入一個key來獲取到相應的緩存數據,而這個key毫無疑問就是將圖片URL進行MD5編碼後的值了,所以讀取緩存數據的代碼就能夠這樣寫:
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
很奇怪的是,這裏獲取到的是一個DiskLruCache.Snapshot對象,這個對象咱們該怎麼利用呢?很簡單,只須要調用它的getInputStream()方法就能夠獲得緩存文件的輸入流了。一樣地,getInputStream()方法也須要傳一個index參數,這裏傳入0就好。有了文件的輸入流以後,想要把緩存圖片顯示到界面上就垂手可得了。因此,一段完整的讀取緩存,並將圖片加載到界面上的代碼以下所示:
try {
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { InputStream is = snapShot.getInputStream(0); Bitmap bitmap = BitmapFactory.decodeStream(is); mImage.setImageBitmap(bitmap); } } catch (IOException e) { e.printStackTrace(); }
咱們使用了BitmapFactory的decodeStream()方法將文件流解析成Bitmap對象,而後把它設置到ImageView當中。若是運行一下程序,將會看到以下效果:
OK,圖片已經成功顯示出來了。注意這是咱們從本地緩存中加載的,而不是從網絡上加載的,所以即便在你手機沒有聯網的狀況下,這張圖片仍然能夠顯示出來。
學習完了寫入緩存和讀取緩存的方法以後,最難的兩個操做你就都已經掌握了,那麼接下來要學習的移除緩存對你來講也必定很是輕鬆了。移除緩存主要是藉助DiskLruCache的remove()方法實現的,接口以下所示:
public synchronized boolean remove(String key) throws IOException
相信你已經至關熟悉了,remove()方法中要求傳入一個key,而後會刪除這個key對應的緩存圖片,示例代碼以下:
try {
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); mDiskLruCache.remove(key); } catch (IOException e) { e.printStackTrace(); }
用法雖然簡單,可是你要知道,這個方法咱們並不該該常常去調用它。由於你徹底不須要擔憂緩存的數據過多從而佔用SD卡太多空間的問題,DiskLruCache會根據咱們在調用open()方法時設定的緩存最大值來自動刪除多餘的緩存。只有你肯定某個key對應的緩存內容已通過期,須要從網絡獲取最新數據的時候才應該調用remove()方法來移除緩存。
除了寫入緩存、讀取緩存、移除緩存以外,DiskLruCache還提供了另一些比較經常使用的API,咱們簡單學習一下。
1. size()
這個方法會返回當前緩存路徑下全部緩存數據的總字節數,以byte爲單位,若是應用程序中須要在界面上顯示當前緩存數據的總大小,就能夠經過調用這個方法計算出來。好比網易新聞中就有這樣一個功能,以下圖所示:
2.flush()
這個方法用於將內存中的操做記錄同步到日誌文件(也就是journal文件)當中。這個方法很是重要,由於DiskLruCache可以正常工做的前提就是要依賴於journal文件中的內容。前面在講解寫入緩存操做的時候我有調用過一次這個方法,但其實並非每次寫入緩存都要調用一次flush()方法的,頻繁地調用並不會帶來任何好處,只會額外增長同步journal文件的時間。比較標準的作法就是在Activity的onPause()方法中去調用一次flush()方法就能夠了。
3.close()
這個方法用於將DiskLruCache關閉掉,是和open()方法對應的一個方法。關閉掉了以後就不能再調用DiskLruCache中任何操做緩存數據的方法,一般只應該在Activity的onDestroy()方法中去調用close()方法。
4.delete()
這個方法用於將全部的緩存數據所有刪除,好比說網易新聞中的那個手動清理緩存功能,其實只須要調用一下DiskLruCache的delete()方法就能夠實現了。
前面已經提到過,DiskLruCache可以正常工做的前提就是要依賴於journal文件中的內容,所以,可以讀懂journal文件對於咱們理解DiskLruCache的工做原理有着很是重要的做用。那麼journal文件中的內容究竟是什麼樣的呢?咱們來打開瞧一瞧吧,以下圖所示:
因爲如今只緩存了一張圖片,因此journal中並無幾行日誌,咱們一行行進行分析。第一行是個固定的字符串「libcore.io.DiskLruCache」,標誌着咱們使用的是DiskLruCache技術。第二行是DiskLruCache的版本號,這個值是恆爲1的。第三行是應用程序的版本號,咱們在open()方法裏傳入的版本號是什麼這裏就會顯示什麼。第四行是valueCount,這個值也是在open()方法中傳入的,一般狀況下都爲1。第五行是一個空行。前五行也被稱爲journal文件的頭,這部份內容仍是比較好理解的,可是接下來的部分就要稍微動點腦筋了。
第六行是以一個DIRTY前綴開始的,後面緊跟着緩存圖片的key。一般咱們看到DIRTY這個字樣都不表明着什麼好事情,意味着這是一條髒數據。沒錯,每當咱們調用一次DiskLruCache的edit()方法時,都會向journal文件中寫入一條DIRTY記錄,表示咱們正準備寫入一條緩存數據,但不知結果如何。而後調用commit()方法表示寫入緩存成功,這時會向journal中寫入一條CLEAN記錄,意味着這條「髒」數據被「洗乾淨了」,調用abort()方法表示寫入緩存失敗,這時會向journal中寫入一條REMOVE記錄。也就是說,每一行DIRTY的key,後面都應該有一行對應的CLEAN或者REMOVE的記錄,不然這條數據就是「髒」的,會被自動刪除掉。
若是你足夠細心的話應該還會注意到,第七行的那條記錄,除了CLEAN前綴和key以外,後面還有一個152313,這是什麼意思呢?其實,DiskLruCache會在每一行CLEAN記錄的最後加上該條緩存數據的大小,以字節爲單位。152313也就是咱們緩存的那張圖片的字節數了,換算出來大概是148.74K,和緩存圖片剛恰好同樣大,以下圖所示:
前面咱們所學的size()方法能夠獲取到當前緩存路徑下全部緩存數據的總字節數,其實它的工做原理就是把journal文件中全部CLEAN記錄的字節數相加,求出的總合再把它返回而已。
除了DIRTY、CLEAN、REMOVE以外,還有一種前綴是READ的記錄,這個就很是簡單了,每當咱們調用get()方法去讀取一條緩存數據時,就會向journal文件中寫入一條READ記錄。所以,像網易新聞這種圖片和數據量都很是大的程序,journal文件中就可能會有大量的READ記錄。
那麼你可能會擔憂了,若是我不停頻繁操做的話,就會不斷地向journal文件中寫入數據,那這樣journal文件豈不是會愈來愈大?這倒沒必要擔憂,DiskLruCache中使用了一個redundantOpCount變量來記錄用戶操做的次數,每執行一次寫入、讀取或移除緩存的操做,這個變量值都會加1,當變量值達到2000的時候就會觸發重構journal的事件,這時會自動把journal中一些多餘的、沒必要要的記錄所有清除掉,保證journal文件的大小始終保持在一個合理的範圍內。
好了,這樣的話咱們就算是把DiskLruCache的用法以及簡要的工做原理分析完了。至於DiskLruCache的源碼仍是比較簡單的, 限於篇幅緣由就不在這裏展開了,感興趣的朋友能夠本身去摸索。下一篇文章中,我會帶着你們經過一個項目實戰的方式來更加深刻地理解DiskLruCache的用法。