天天看點

Android中的緩存政策

緩存政策的主要流程:

當程式第一次從網絡加載圖檔後,将其緩存到儲存裝置上,下一次就不用再次從網絡上擷取了。為了提高應用的使用者體驗,往往還會再記憶體中再緩存一份,這樣當應用打算從網絡請求一張圖檔時,首先從記憶體中讀取,如果沒有那就從儲存裝置中擷取,如果儲存裝置也沒有,那就從網絡上下載下傳這張圖檔。因為從記憶體中加載圖檔比儲存裝置加載要快,是以這樣既提高程式的效率又為使用者節約了不必要的流量開銷。而這種緩存政策不僅僅适用于圖檔,也适用于其他檔案類型。

緩存算法

目前常用的一種緩存算法是LRU(Least Recently Used),LRU是近期最少使用的算法,核心思想:當緩存滿時,會優先淘汰那些近期最少使用的緩存對象。采用LRU算法的緩存有兩種:LruCache和DiskLruCache,LruCache用于實作記憶體緩存,而DiskLruCaChe則充當了儲存設備緩存。

LruCache

LruCache是一個泛型類,内部采用一個LinkedHashMap以強引用的方式存儲外界的緩存對象。并且提供了get和put方法來完成緩存的擷取和添加操作。

  • 強引用:直接的對象引用
  • 軟引用:當一個對象隻有軟引用存在時,系統記憶體不足時此對象會被gc回收
  • 弱引用:當一個對象隻有弱引用存在時,此對象會被随時gc回收

初始化LruCaChe

int MaxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize){
    @override
    protected int sizeof(String key,Bitmap bitmap){
        return bitmap.getRowBytes()*bitmap.getHeight() / 1024;
    }
}
           

在上面代碼中,隻需要提供緩存的總容量大小并重寫sizeof方法即可。另外一些特殊情況下,還需要重寫LruCache的entryRemoved方法,LruCache移除舊緩存時會調用entryRemoved方法,是以可以在entryRemoved中完成一些資源回收工作。

從LruCache中擷取一個緩存對象

向LruCache中添加一個緩存對象

LruCache還支援删除操作,通過remove方法即可删除一個指定的緩存對象。

DiskLruCache

1.DiskLruCache的建立

DiskLruCache提供了open方法建立

open方法有四個參數,其中第一個參數為磁盤緩存在檔案系統中的儲存路徑。緩存可以選擇SD卡上的緩存目錄,具體為:***/sdcard/Android/data/package_name/cache***目錄,當應用被解除安裝後,包名的目錄會一并被删除。當然還可以選擇sd卡上的其他指定路徑,還可以選擇data下的目前應用的目錄。這裡一般有個原則:如果應用解除安裝後就希望删除緩存檔案,那麼就選擇SD卡上的緩存目錄,如果希望保留緩存資料那就應該選擇sd卡上的其他路徑

第二個參數為版本号,一般設為1.當版本号改變時會清空之前的是以緩存檔案。

第三個參數表示單個節點所對應的資料的個數,一般設為1即可。第四個參數為緩存的總大小。下面是典型的DiskLruCache的建立過程:

private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50 ;//50Mb
File diskCacheFile = getDiskCacheDir(mContext,"bitmap");
if(!diskCacheFile.exists()){
    dislCacheFile.mkdirs();
}
mDiskLruCache = DiskLruCache.open(diskCaCheFile,1,1,DISK_CACHE_SIZE);
           

2.DiskLruCache的緩存添加

DiskLruCache的緩存添加的操作是通過Editor完成的,Editor表示一個緩存對象的編輯對象。這裡以照片緩存為例子,首先擷取圖檔的URL所對應的key,然後根據key就可以通過edit()來擷取Editor對象,如果這個緩存正在被編輯,那麼edit()會傳回null,即DiskLruCache不允許同時編輯一個對象。之是以要把url轉化成key,是因為圖檔的url很可能含有特殊的字元,這會影響url在安卓中直接使用,一般采用url的md5值作為key;

private String hashKeyFormUrl(String url){
    String cacheKey;
    try{
        final MessageDigest mDigest = MessageDigest.getInstance("MD5");
        mDigest.update(url.getBytes());
        cacheKey = bytesToHexString(mDigest.digest());
    } catch (NoSuchAlgorithmException e) {
        cacheKey = String.valueof(url.hashcode());
    }
    return cacheKey;
}

private String bytesToHexString(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for(int i = 0; i < bytes.length; i++) {
        String hex = Integer.toHexString(OxFF & bytes[i]);
        if(hex.length() == 1) {
            sb.append('0');
        }
        sb.append(hex);
    }
    return sb.toString();
}
           

将圖檔的url轉成key後,就可以獲得Editor對象了。對于key來說,如果目前不存在其他的Editor對象,那麼edit()就會傳回一個新的Editor對象,通過他可以獲得檔案輸出流。需注意前面的diskLruCache的open方法中設定了一個節點隻有一個資料,是以下面的DISK_CACHE_INDEX常量直接設定成0即可。

String key = hashkeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if(editor != null) {
    OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}
           

有了檔案輸出流,那麼當從網絡下載下傳圖檔時,圖檔就可以通過這個檔案輸出流寫入到檔案系統上,具體實作過程如下:

public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
    HttpURLConnection urlConnection = null;
    BufferedOutputStream out = null;
    BufferedInputSream in = null;
    
    try {
        final URL url = new URL(urlString);
        urlConnection = (HttpURLConnection) url.openConnection();
        in = new BufferedInputStream(urlConnection.getInputSream(),IO_BUFFER_SIZE);
        out = new BufferedOutputStream(outputStream,IO_BUFFER_SIZE);
        
        int b;
        while ((b = in.read())!= -1) {
            out.write(b);
        }
        return ture;
    } catch (IOException e) {
        e.printStack();;
    }finally {
        if(urlConnection != null) {
            urlConnection.disConnect();
        }
        MyUtils.close(in);
        MyUtils.close(out);
    }
    return false;
}
           

另外,必須通過Editor的commit()來送出寫入操作,如果照片下載下傳過程發生了異常,還可以通過Editor的abort()來回退整個操作。

OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if(downloadUrlTOSream(url,outputStream)) {
    editor.commmit();
} else {
    editor.abort();
}
mDiskLruCache.flush();
           

3.DiskLruCache的緩存查找

緩存查找的過程首先需要将url轉化成key,然後通過DiskLruCache的get方法得到一個Snapshot對象,再接着通過這個對象即可獲得緩存檔案的輸入流。進而得到Bitmap對象。為了避免加載圖檔導緻的OOM,一般不建議直接加載原圖。上文提到使用BitmapFactory.Options來加載縮放後的圖檔,但是這種方法之前也提到對FileInputSream存在問題,原因是FileOutput是一種有序的檔案流,兩次的decodeStream調用會影響檔案流的位置屬性,導緻第二次調用decodeStream時傳回了null.故為了解決這種問題,首先得獲得檔案描述符,再通過BitmapFactory.decodeFileDecriptor方法加載一張縮放後的照片。

Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DislLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if(snapShot != null) {
    FileInputStream fileInputStream = (FileInputStream) snapShot.getInputSream(DISK_CACHE_INDEXT);
    FileDecriptor fileDecriptor = fileInputStream.getFD();
    bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDecriptor,reqWidth,reqHeight);
    if(bitmap != null) {
        addBitmapToMemoryCache(key,bitmap);
    }
}
           

注:本筆記來源于《Android開發藝術探索》相關知識