1. 程式人生 > >Android中的快取策略

Android中的快取策略

快取策略的主要流程:

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

快取演算法

目前常用的一種快取演算法是LRU(Least Recently Used),LRU是近期最少使用的演算法,核心思想:當快取滿時,會優先淘汰那些近期最少使用的快取物件

。採用LRU演算法的快取有兩種:LruCacheDiskLruCache,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中獲取一個快取物件

mMemoryCache.get(key);

向LruCache中新增一個快取物件

mMemoryCache.put(key,bitmap)

LruCache還支援刪除操作,通過remove方法即可刪除一個指定的快取物件。

DiskLruCache

1.DiskLruCache的建立

DiskLruCache提供了open方法建立

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

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開發藝術探索》相關知識