Android DiskLruCache 完全解析,硬盤緩存的最佳方案


轉載請注明出處:http://blog.csdn.net/guolin_blog/article/details/28863651

一、概述

記得在很早之前,我有寫過一篇文章Android高效加載大圖、多圖解決方案,有效避免程序OOM,這篇文章是翻譯自 Android Doc 的,其中防止多圖 OOM 的核心解決思路就是使用 LruCache 技術。但 LruCache 只是管理了內存中圖片的存儲與釋放,如果圖片從內存中被移除的話,那么又需要從網絡上重新加載一次圖片,這顯然非常耗時。對此,Google 又提供了一套硬盤緩存的解決方案:DiskLruCache (非Google官方編寫,但獲得官方認證)。只可惜,Android Doc 中並沒有對 DiskLruCache 的用法給出詳細的說明,而網上關於DiskLruCache 的資料也少之又少,因此今天我准備專門寫一篇博客來詳細講解 DiskLruCache 的用法,以及分析它的工作原理,這應該也是目前網上關於 DiskLruCache 最詳細的資料了。

那么我們先來看一下有哪些應用程序已經使用了 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 上,地址如下:
android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java

如果 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() 方法就可以這樣寫:

    /**
     * 圖片硬盤緩存核心類。
     */
    private DiskLruCache mDiskLruCache;

        try {
            
            // 獲取圖片緩存路徑
            File cacheDir = getDiskCacheDir(context, "thumb");
            
            if (!cacheDir.exists()) {
                
                cacheDir.mkdirs();
            }
            
            // 創建DiskLruCache實例,初始化緩存數據
            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,那么為了將這張圖片下載下來,就可以這樣寫:

        /**
         * 建立HTTP請求,並獲取 Bitmap 對象。
         * 
         * @param imageUrl
         *            圖片的URL地址
         * @return 解析后的Bitmap對象
         */
        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 編碼,代碼如下所示:
    /**
     * 使用 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();
    }

代碼很簡單,現在我們只需要調用一下 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() 方法來移除緩存。

八、其它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.74 K,和緩存圖片剛剛好一樣大,如下圖所示:

 

 


前面我們所學的 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 。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM