上一篇記錄了Bitmap的(高效)加載,那麼這一篇就記錄Cache。
對於網絡上的圖片,第一次使用就需要從網絡上去下載下來,但如果每次都去從網絡上下載,那就非常浪費流量了,所以需要做緩存。另外的添加了緩存也要做好刪除緩存,畢竟有些過久地圖片或是很少會再用到的圖片,就需要刪掉了,釋放空間。
這裏用到的緩存算法是LRU(Least Recently Used),最近最少使用算法。在該算法的基礎上有衍生出兩種緩存,LruCache和DiskLruCache,前者用於實現內存緩存,後者用於實現存儲設備的緩存。所以這裏就是將這兩者結合,實現了一個ImageLoader(圖片加載器),這裏用到了三級緩存(網絡緩存,磁盤緩存和內存緩存)。
1. LruCache
引用原文的話:
LruCache是一個泛型類,它內部採用一個LinkedHashMap以強引用的方式存儲外界的緩存對象。
另外LruCache是線程安全的。
短短兩句話涉及了不少概念。
LinkedHashMap如果去查找Lru算法的話,基本都是在它的基礎上實現的;
從構造方法裏可以看出它是個泛型類:
然後關於強引用:
之所以說線程安全,因爲在LruCache裏的添加,刪除,獲取都是有同步鎖機制的。
1.1 LruCache的使用
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//單位KB
//設定緩存的容量爲總容量的1/8
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
//返回bitmap大小的計算
return value.getRowBytes() * value.getHeight() / 1024;
}
};
總的來說就是提供緩存的總容量大小然後重寫sizeof方法。這裏緩存的總容量大小爲當前進程的可用內存的1/8,sizeof裏就返回的是bitmap對象的大小計算。這兩個的單位應該一致,所以這裏都除以1024。
然後是獲取的方法:
mMemoryCache.get(key);
添加的方法:
mMemoryCache.put(key, bitmap);
2. DiskLruCache
2.1 DiskLruCache的使用
2.1.1 引用
用DiskLruCache來做磁盤緩存,可以通過依賴來獲取:
compile 'com.jakewharton:disklrucache:2.0.2'
2.1.2 創建
DiskLruCache需要通過open方法來創建,而不是普通的構造方法:
//利用open方法來創建,第一個參數是存儲路徑,第二個參數是版本號,
//第三個參數是單個節點對應的數據的個數,第四個參數是緩存的總大小
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
2.1.3 添加
與前面的LruCache一樣,DiskLruCache也是用到了LinkedHashMap,那麼在LruCache裏的操作都用到了“key”這個東西,這裏也同樣用到了key。由於在這個ImageLoader裏他們都是操作同一個東西,所以當然是一樣的。作者在這裏是用圖片的url來作key,但需要作一些轉換,用url的md5值來作爲key,主要是防止url裏可能有些特殊字符導致出錯:
/**
* 將圖片的url轉換成key,這裏採用url的md5的值作爲key
* @param url
* @return
*/
private String hashKeyFromUrl(String url) {
String cacheKey;
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(url.getBytes());
cacheKey = bytesToHexString(messageDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
stringBuilder.append('0');
}
stringBuilder.append(hex);
}
return stringBuilder.toString();
}
那麼DiskLruCache的緩存添加是通過Editor來完成的,通過edit()方法和key就可以獲取到這個Editor對象,進而可以獲得文件輸出流。
String key = hashKeyFromUrl(url);
//使用Editor進行緩存添加的操作
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}
那麼怎麼操作這個文件輸出流呢?或者說它的數據從哪來呢?
其實它的數據是從它的更上一級,網絡緩存那裏來的,我們通過url去做網絡請求的時候會獲得一個輸入流,然後我們把輸入流寫到這個輸出流裏,那麼它就有數據了。
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
in.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
現在我們通過網絡請求將流寫給了磁盤緩存,但需要通過進一步的確認操作來真正的寫入。即commit()方法,所以把前面的一塊代碼修改下:
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
}
2.1.4 獲取
DiskLruCache對緩存的獲取則是通過它的Snapshot對象,與上面的類似,它是通過get()方法和key得到的,然後可以進一步的得到文件輸入流,那拿到了文件輸入流我們通過上一篇的BitmapFactory提供的解碼方法就可以得到一個Bitmap對象了。
String key = hashKeyFromUrl(url);
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
//獲取該圖片的文件輸入流
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
//獲取該文件輸入流的文件描述
FileDescriptor fileDescriptor = fileInputStream.getFD();
//通過文件描述得到想要的bitmap
bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
}
這裏對文件輸入流的處理是用了FileDescriptor, 也就是對應於decodeFileDescriptor()方法。爲什麼這裏要用這個方法?
作者給的解釋是FileInputStream是一種有序的文件流,兩次decodeStream調用影響文件流的位置屬性,在第二次decodeStream的時候會得到null,那這裏我做了測試,確實在第二次的時候會得到null。