我們先來看一下有哪些應用程序已經使用了DiskLruCache技術。在我所接觸的應用範圍裏,Dropbox、Twitter、網易新聞等都是使用DiskLruCache來進行硬盤緩存的,其中Dropbox和Twitter大多數人應該都沒用過,那麼我們就從大家最熟悉的網易新聞開始着手分析,來對DiskLruCache有一個最初的認識吧。
初探
相信所有人都知道,網易新聞中的數據都是從網絡上獲取的,包括了很多的新聞內容和新聞圖片,如下圖所示:
但是不知道大家有沒有發現,這些內容和圖片在從網絡上獲取到之後都會存入到本地緩存中,因此即使手機在沒有網絡的情況下依然能夠加載出以前瀏覽過的新聞。而使用的緩存技術不用多說,自然是DiskLruCache了,那麼首先第一個問題,這些數據都被緩存在了手機的什麼位置呢?
其實DiskLruCache並沒有限制數據的緩存位置,可以自由地進行設定,但是通常情況下多數應用程序都會將緩存的位置選擇爲 /sdcard/Android/data/<application package>/cache 這個路徑。選擇在這個位置有兩點好處:第一,這是存儲在SD卡上的,因此即使緩存再多的數據也不會對手機的內置存儲空間有任何影響,只要SD卡空間足夠就行。第二,這個路徑被Android系統認定爲應用程序的緩存路徑,當程序被卸載的時候,這裏的數據也會一起被清除掉,這樣就不會出現刪除程序之後手機上還有很多殘留數據的問題。
那麼這裏還是以網易新聞爲例,它的客戶端的包名是com.netease.newsreader.activity,因此數據緩存地址就應該是 /sdcard/Android/data/com.netease.newsreader.activity/cache ,我們進入到這個目錄中看一下,結果如下圖所示:
可以看到有很多個文件夾,因爲網易新聞對多種類型的數據都進行了緩存,這裏簡單起見我們只分析圖片緩存就好,所以進入到bitmap文件夾當中。然後你將會看到一堆文件名很長的文件,這些文件命名沒有任何規則,完全看不懂是什麼意思,但如果你一直向下滾動,將會看到一個名爲journal的文件,如下圖所示:
那麼這些文件到底都是什麼呢?看到這裏相信有些朋友已經是一頭霧水了,這裏我簡單解釋一下。上面那些文件名很長的文件就是一張張緩存的圖片,每個文件都對應着一張圖片,而journal文件是DiskLruCache的一個日誌文件,程序對每張圖片的操作記錄都存放在這個文件中,基本上看到journal這個文件就標誌着該程序使用DiskLruCache技術了。
下載
好了,對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)
其中緩存地址前面已經說過了,通常都會存放在 /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);
- }
接着又將獲取到的路徑和一個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;
- }
後面兩個參數就沒什麼需要解釋的了,第三個參數傳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();
- }
有了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;
- }
- public Editor edit(String key) throws IOException
那麼我們就寫一個方法用來將字符串進行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();
- }
因此,現在就可以這樣寫來得到一個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);
因此,一次完整寫入操作的代碼如下所示:
- 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();
現在的話緩存應該是已經成功寫入了,我們進入到SD卡上的緩存目錄裏看一下,如下圖所示:
可以看到,這裏有一個文件名很長的文件,和一個journal文件,那個文件名很長的文件自然就是緩存的圖片了,因爲是使用了MD5編碼來進行命名的。
讀取緩存
緩存已經寫入成功之後,接下來我們就該學習一下如何讀取了。讀取的方法要比寫入簡單一些,主要是藉助DiskLruCache的get()方法實現的,接口如下所示:
- public synchronized Snapshot get(String key) throws IOException
- String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
- String key = hashKeyForDisk(imageUrl);
- DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
- 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
- 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();
- }
其它API
除了寫入緩存、讀取緩存、移除緩存之外,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()方法就可以實現了。
解讀journal
前面已經提到過,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的用法。感興趣的朋友請繼續閱讀 Android照片牆完整版,完美結合LruCache和DiskLruCache 。