天天看點

Android的緩存政策:LruCache和DiskLruCache緩存算法LruCacheDiskLruCache

當使用者要浏覽微信的某張圖檔或視訊時,第一次肯定需要從網絡上下載下傳下來才能看,但如果第二次去浏覽,還要從網絡上下載下傳就不合适了,使用者體驗差,更重要的是浪費了使用者的流量。當圖檔首次下載下傳下來的時候,我們需要對圖檔做一下緩存,這樣再次讀它的時候直接從緩存中取就可以了。圖檔緩存對于目前的主流圖檔加載架構(比如UniversalImageLoader)是最最基礎的功能了。

緩存通常分為兩種:記憶體緩存和硬碟緩存。當應用打算從網絡上請求一張圖檔時,先嘗試從記憶體中擷取,如果沒有再嘗試從硬碟中擷取,還是沒有再從網絡上下載下傳。因為記憶體速度>硬碟速度>下載下傳速度,而且還能節省流量。上述的緩存政策不隻适用于圖檔,還适用于其他檔案類型。

緩存算法

記憶體緩存和硬碟緩存的存儲空間都是有限的,而且使用緩存時都需要制定一個最大的使用容量。如果超過這個容量,但程式還需要添加緩存,就需要删除一些舊的緩存。目前最常用的一種緩存算法是LRU(Least Recently Used),最近最少使用算法,當緩存滿時會優先淘汰那些近期最少使用的緩存對象。采用LRU算法的緩存有兩種:LruCache和DiskLruCache,其中LruCache用于實作記憶體緩存,DiskLruCache用于實作硬碟緩存。

LruCache

從Android 3.1開始提供這個類,之前的android版本想使用的話可以用support-v4下面的。

LruCache是一個泛型類,内部通過LinkedHashMap以強引用的方式存儲緩存對象,它本身也提供了get和put方法供外界調用。另外它是線程安全的:

public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;

    public LruCache(int maxSize) {
           

new一個LruCache對象時,構造函數的參數需指定緩存的總容量大小。下面通過一個小demo來看一下它的使用:

public class MainActivity extends ActionBarActivity {

    private LruCache<String, Bitmap> mLruCache;
    private ImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mImageView = (ImageView) findViewById(R.id.iv);

        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / );
        int cacheSize = maxMemory / ;
        mLruCache = new LruCache<String, Bitmap>(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / ;
            }
        };

        Bitmap bitmap;
        String path = new File(Environment.getExternalStorageDirectory(), "a.jpg").getAbsolutePath();
        if((bitmap = BitmapFactory.decodeFile(path)) != null){
            //先緩存後再取出
            mLruCache.put(path, bitmap);
            mImageView.setImageBitmap(mLruCache.get(path));
        }else{
            Toast.makeText(MainActivity.this, "error path", Toast.LENGTH_SHORT).show();
        }
    }
}
           

這個demo中,設定了緩存容量的總大小為目前程序可用記憶體的1/4,機關是KB。另外重寫了sizeOf方法,它的作用是計算緩存對象的大小,注意它的機關應該跟總容量的機關保持一緻,這裡都是KB。

一些特殊情況下,還需要重寫entryRemoved方法,LruCache移除舊緩存時會調用該方法,是以可以在其中完成一些資源回收工作。

DiskLruCache

這個類并不在android源碼中,使用時需要手動把這個檔案加入到項目中,它的源碼可以從google source中擷取:

android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java

如果網址打不開可以點選這裡下載下傳源碼:點我下載下傳

複制到工程中後注意改一下包名。

這個類的構造方法是私有的,隻能通過下面方法去構造對象:

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
           

首先來講解一下參數的含義:

directory表示要緩存的路徑,我們選擇路徑的時候最好存在/sdcard/Android/data//cache裡,因為系統可以識别出這是應用的緩存路徑,當程式被解除安裝時這裡的資料會被一起清掉;另外cache下面可以再加一級路徑,比如/bitmap/,用來區分不同的緩存對象類型。擷取路徑可以參考下面的代碼:

private File getDiskCacheDir(String folderName) {
    String cachePath;
    if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) && !Environment.isExternalStorageRemovable()){
        cachePath = getExternalCacheDir().getPath();
    }else{
        cachePath = getCacheDir().getPath();
    }
    return new File(cachePath, folderName);
}
           

appVersion表示目前應用的版本,如果版本号改變了,那麼緩存會被清空,資料需要從網上重新擷取。擷取版本号可以參考下面的代碼:

private int getAppVersion() {
    try {
        PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), );
        return packageInfo.versionCode;
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }
    return ;
}
           

valueCount表示一個key可以對應幾個緩存檔案,通常設為1。

maxSize表示緩存容量的最大值。

得到DiskLruCache對象後,就可以緩存檔案了,比如我們要緩存網上的一個bitmap,具體步驟是:

1.通過DiskLruCache對象擷取DiskLruCache.Editor對象,要執行緩存操作必須要用到這個editor對象:

2.通過editor擷取輸出流,用來存儲緩存:

這裡參數傳0是因為建立DiskLruCache時valueCount我們傳了1。

3.從網上下載下傳bitmap,将通路url得到的輸入流寫入到第2步得到的輸出流裡:

private boolean downloadBitmap(String urlString, OutputStream os) {
    try {
        URL url = new URL(urlString);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        BufferedInputStream bis = new BufferedInputStream(conn.getInputStream(),  * );
        BufferedOutputStream bos = new BufferedOutputStream(os,  * );
        int len;
        while((len = bis.read()) != -){
            bos.write(len);
        }
        bis.close();
        bos.close();
        conn.disconnect();
        return true;
    } catch (MalformedURLException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return false;
}
           

4.将輸出流存入緩存:

if(downloadBitmap(downloadUrl, os)){
    editor.commit();
}else{
    editor.abort();
}
mDiskLruCache.flush();
           

commit代表送出,即寫入生效;abort代表放棄此次操作。調用flush()表示将操作記錄都同步到journal檔案裡,這個journal檔案是DiskLruCache的操作記錄日志,它的位置也在上面我們指定的緩存目錄下,它是DiskLruCache能夠正常工作的前提,我們不需要頻繁調用,一般隻需在onPause()時調用即可。

接下來是讀取緩存檔案,具體步驟是:

1.通過DiskLruCache對象擷取DiskLruCache.Snapshot對象,要讀取緩存必須要用到這個snapshot對象:

2.通過snapshot擷取輸入流:

這裡參數傳0也是因為建立DiskLruCache時valueCount我們傳了1。

3.得到輸入流以後就可以做業務相關的操作了,比如解析出bitmap:

完整demo如下:

public class MainActivity extends ActionBarActivity {

    private final String downloadUrl = "http://img3.douban.com/view/note/large/public/p28933592.jpg";

    private DiskLruCache mDiskLruCache;
    private ImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mImageView = (ImageView) findViewById(R.id.iv);

        //打開硬碟緩存,最大容量設為10M
        final File cacheDir = getDiskCacheDir("bitmap");
        if(!cacheDir.exists()){
            cacheDir.mkdirs();
        }
        final int appVersion = getAppVersion();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    mDiskLruCache = DiskLruCache.open(cacheDir, appVersion, ,  *  * );

                    String cacheKey = getCacheKey(downloadUrl);
                    DiskLruCache.Editor editor = mDiskLruCache.edit(cacheKey);
                    if(editor != null){
                        OutputStream os = editor.newOutputStream();
                        //從網絡上下載下傳一張圖檔
                        if(downloadBitmap(downloadUrl, os)){
                            //将下載下傳的圖檔存入到緩存中
                            editor.commit();
                        }else{
                            editor.abort();
                        }
                    }
                    mDiskLruCache.flush();

                    //從緩存中讀取該圖檔并顯示在ImageView中
                    DiskLruCache.Snapshot snapshot = mDiskLruCache.get(cacheKey);
                    if(snapshot != null){
                        InputStream is = snapshot.getInputStream();
                        final Bitmap bitmap = BitmapFactory.decodeStream(is);
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                mImageView.setImageBitmap(bitmap);
                            }
                        });
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private String getCacheKey(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 = ; i < bytes.length; i++) {
            String hex = Integer.toHexString( & bytes[i]);
            if (hex.length() == ) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    private File getDiskCacheDir(String folderName) {
        String cachePath;
        if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) && !Environment.isExternalStorageRemovable()){
            cachePath = getExternalCacheDir().getPath();
        }else{
            cachePath = getCacheDir().getPath();
        }
        return new File(cachePath, folderName);
    }

    private int getAppVersion() {
        try {
            PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), );
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return ;
    }

    private boolean downloadBitmap(String urlString, OutputStream os) {
        try {
            URL url = new URL(urlString);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            BufferedInputStream bis = new BufferedInputStream(conn.getInputStream(),  * );
            BufferedOutputStream bos = new BufferedOutputStream(os,  * );
            int len;
            while((len = bis.read()) != -){
                bos.write(len);
            }
            bis.close();
            bos.close();
            conn.disconnect();
            return true;
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
}
           

這個demo中緩存的key沒有直接用url,而是用了url的MD5編碼,因為url中可能包含特殊字元,不能作為緩存檔案的命名,而MD5編碼既唯一又肯定符合命名要求。

另外,除了上面的存取、擷取,還有移除操作,需要調用

mDiskLruCache.remove(key);  
           

這個通常不需要我們操作,因為緩存容量超過maxSize後,DiskLruCache會根據LRU算法自動删除某些緩存,是以除非你很明确某個緩存是沒有必要的了,否則不必手動去調。

close()方法用于将DiskLruCache關閉掉,是和open()方法對應的一個方法。關閉掉以後就不能再調用DiskLruCache中任何操作緩存資料的方法,通常隻應該在Activity的onDestroy()方法中去調用close()方法。

delete()方法用于将所有的緩存資料全部删除,比如某些app設定裡通常都有的手動清理緩存功能,其實隻需要調用一下DiskLruCache的delete()方法就可以實作了。

journal檔案簡單介紹

Android的緩存政策:LruCache和DiskLruCache緩存算法LruCacheDiskLruCache

由于現在隻緩存了一張圖檔,是以journal中并沒有幾行日志,第一行是個固定的字元串“libcore.io.DiskLruCache”,标志着使用了DiskLruCache。第二行是DiskLruCache的版本号,這個值是恒為1的。第三行是應用程式的版本号,我們在open()方法裡傳入的版本号是什麼這裡就會顯示什麼。第四行是valueCount,這個值也是在open()方法中傳入的,通常情況下都為1。第五行是一個空行。前五行也被稱為journal檔案的頭,這部分内容還是比較好了解的,但是接下來的部分就要稍微動點腦筋了。

第六行是以一個DIRTY字首開始的,後面緊跟着緩存圖檔的key。每當我們調用一次DiskLruCache的edit()方法時,都會向journal檔案中寫入一條DIRTY記錄,表示我們正準備寫入一條緩存資料,但不知結果如何。然後調用commit()方法表示寫入緩存成功,這時會向journal中寫入一條CLEAN記錄,調用abort()方法表示寫入緩存失敗,這時會向journal中寫入一條REMOVE記錄。也就是說,每一行DIRTY的key,後面都應該有一行對應的CLEAN或者REMOVE的記錄,否則這條資料就是“髒”的,會被自動删除掉。

除了DIRTY、CLEAN、REMOVE之外,還有一種字首是READ的記錄,這個就非常簡單了,每當我們調用get()方法去讀取一條緩存資料時,就會向journal檔案中寫入一條READ記錄。是以,非常大的程式journal檔案中就可能會有大量的READ記錄,那麼你可能會擔心了,如果我不停頻繁操作的話,就會不斷地向journal檔案中寫入資料,那這樣journal檔案豈不是會越來越大?這倒不必擔心,DiskLruCache中使用了一個redundantOpCount變量來記錄使用者操作的次數,每執行一次寫入、讀取或移除緩存的操作,這個變量值都會加1,當變量值達到2000的時候就會觸發重構journal的事件,這時會自動把journal中一些多餘的、不必要的記錄全部清除掉,保證journal檔案的大小始終保持在一個合理的範圍内。