Android的緩存策略:LruCache和DiskLruCache

當用戶要瀏覽微信的某張圖片或視頻時,第一次肯定需要從網絡上下載下來才能看,但如果第二次去瀏覽,還要從網絡上下載就不合適了,用戶體驗差,更重要的是浪費了用戶的流量。當圖片首次下載下來的時候,我們需要對圖片做一下緩存,這樣再次讀它的時候直接從緩存中取就可以了。圖片緩存對於目前的主流圖片加載框架(比如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() / 1024);
        int cacheSize = maxMemory / 4;
        mLruCache = new LruCache<String, Bitmap>(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };

        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(), 0);
        return packageInfo.versionCode;
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }
    return 1;
}

valueCount表示一個key可以對應幾個緩存文件,通常設爲1。
maxSize表示緩存容量的最大值。

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

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

DiskLruCache.Editor editor = mDiskLruCache.edit(cacheKey);

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

OutputStream os = editor.newOutputStream(0);

這裏參數傳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(), 4 * 1024);
        BufferedOutputStream bos = new BufferedOutputStream(os, 4 * 1024);
        int len;
        while((len = bis.read()) != -1){
            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對象:

DiskLruCache.Snapshot snapshot = mDiskLruCache.get(cacheKey);

2.通過snapshot獲取輸入流:

InputStream is = snapshot.getInputStream(0);

這裏參數傳0也是因爲創建DiskLruCache時valueCount我們傳了1。

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

Bitmap bitmap = BitmapFactory.decodeStream(is);

完整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, 1, 10 * 1024 * 1024);

                    String cacheKey = getCacheKey(downloadUrl);
                    DiskLruCache.Editor editor = mDiskLruCache.edit(cacheKey);
                    if(editor != null){
                        OutputStream os = editor.newOutputStream(0);
                        //從網絡上下載一張圖片
                        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(0);
                        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 = 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 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(), 0);
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

    private boolean downloadBitmap(String urlString, OutputStream os) {
        try {
            URL url = new URL(urlString);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            BufferedInputStream bis = new BufferedInputStream(conn.getInputStream(), 4 * 1024);
            BufferedOutputStream bos = new BufferedOutputStream(os, 4 * 1024);
            int len;
            while((len = bis.read()) != -1){
                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文件簡單介紹

這裏寫圖片描述
由於現在只緩存了一張圖片,所以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文件的大小始終保持在一個合理的範圍內。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章