Android中的快取策略
快取策略的主要流程:
當程式第一次從網路載入圖片後,將其快取到儲存裝置上,下一次就不用再次從網路上獲取了。為了提高應用的使用者體驗,往往還會再記憶體中再快取一份,這樣當應用打算從網路請求一張圖片時,首先從記憶體中讀取,如果沒有那就從儲存裝置中獲取,如果儲存裝置也沒有,那就從網路上下載這張圖片。因為從記憶體中載入圖片比儲存裝置載入要快,所以這樣既提高程式的效率又為使用者節約了不必要的流量開銷。而這種快取策略不僅僅適用於圖片,也適用於其他檔案型別。
快取演算法
目前常用的一種快取演算法是LRU(Least Recently Used),LRU是近期最少使用的演算法,核心思想:當快取滿時,會優先淘汰那些近期最少使用的快取物件
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);
}
}