目前比較常用的緩存策略是LruCache(Android3.1提供)和DiskLruCache(是官方文檔推薦,但不屬于Android SDK,需要自行下載源碼編譯)。
下載地址: https://android.googlesource.com/platform/libcore/+/android-4.1.1-r1/luni/src/main/java/libcore/io/DiskLruCache.java。
LruCache常被用作內存緩存,而DiskLruCache常被用作磁盤緩存。Lru是Least Recently Used的縮寫,
LurCache內部其實是用了一個LinkedHashMap來存儲數據的,它的構造如下:
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap(0, 0.75f, true);
}
構造了一個初始容量為0,負載因子為0.75,accessOrder為true的LinkedHashMap。accessOrder為true意味著鏈表中元素的順序為訪問順序,即調用get方法后,會將這次訪問的元素移至鏈表尾部,這樣最前面的一個元素就是最近最少使用的了。當元素數量達到指定的最大數量之后是怎么刪除的呢?主要是LinkedHashMap中的removeEldestEntry方法。例如可以這樣重寫這個方法:
final int MAX_ENTRIES = 50;
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
這樣當put新元素的時候,如果removeEldestEntry返回了true,就會刪除最老的那個元素。而返回true 的條件就是LinkedHashMap的size大于我們指定的值。這就是LruCache的實現原理。DiskLruCache類似。
下面開始將如何高效的加載Bitmap。
Bitmap 的加載主要是通過BItmapFactory,BitmapFactory有4中方法加載Bitmap:decodeFile,decodeResource,decodeStream和decodeByteArray。其中decodeFile,decodeResource間接調用了decodeStream方法。
很多時候我們圖片的大小是大于ImageView的大小的,這個時候我們就需要對Bitmap進行縮放,如何縮放呢?
主要是通過采樣率來進行縮放。設置采樣率的方式為 BitmapFactory.Options的inSampleSize參數。當inSampleSize為1時,表示是圖片的原始大小不縮放,當inSampleSize大于1,比如為2時,那么采樣后的圖片的寬帶都為原來的1/2,而像素數為原圖的1/4,其占有的內存大小也為1/4。而且采樣率必須為大于1的整數才有效果,當小于1時,其作用相當于1,無縮放效果。另外最新的官方文檔中指出,inSampleSize的取值應該總是2的指數,比如,1,2,4,8,16等等。如果外界傳遞給系統的inSampleSize不為2的指數,那么系統會向下取整并選擇一個最接近2的指數來代替。比如傳3,系統會取2來代替。但是通過驗證,這個結論并非在所有的Android版本都適用,建議開發的時候按2的指數取。
通過采樣率加載可以按照以下步驟:
1.將BitmapFactory.Options的inJustDecodeBounds參數設置為true并加載圖片。(設置為true之后并不會真正的去加載圖片,只會解析圖片的原始寬高。這個操作是輕量級的,但是得注意這個操作獲取的圖片的寬高信息跟圖片的位置以及程序運行的設備有關,比如放在不同的Drawable下面或運行在不同分辨率的機器上)。
2.從BitmapFactory.Options中取出圖片的原始寬高信息,它們對應于outWidth和outHeight參數。
3.根據采樣率的規則并結合目標View的所需大小計算出采樣率inSampleSize。
4.將BitmapFactory.Options的inJustDecodeBounds參數設為false,然后重新加載圖片。
下面給出一個比較通用的計算采樣率的算法:
public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight){
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize=1;
if(height > reqHeight || width > reqWidth){
final int halfHeight = height /2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) > = reqWidth){
inSampleSize *=2;
}
}
return inSampleSize;
}
傳入的options為解析后的options,其中這個算法的關鍵部分可以仔細體會一下。
LruCache的典型初始化代碼:
int maxMemory = (int) ((Runtime.getRuntime().maxMemory()) / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
sizeOf方法是計算緩存對象大小的。這里大小需要和總容量的單位一致,上面的為KB。
DiskLruCache提供了open方法來創建自身,如下所示:
public static DiskLruCache open(File dir, int appVersion, int valueCount, long maxSize);
dir表示緩存的文件的目錄。
appVersion表示應用的版本號,一般設為1,版本號發生改變時,DiskLruCache會清空之前所有的緩存文件。
valueCount 表示耽擱節點所對應的數據的個數,一般設為1即可。
maxSize表示緩存的最大值,比如50MB,但是要轉換成byte。
DiskLruCache緩存的添加:
DiskLruCache的緩存的添加時通過Editor完成的,Editor表示一個緩存對象的編輯對象。
例如:
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if(editor != null){
OutputStream outputStream = editor.newOutputSream(index);
}
因為前面設置了valueCount為1,這里index可以直接傳0;
拿到這個outputSream之后,就可以把從網絡上下載下路的文件流寫入里面,注意最后寫完了,要調一下editor的commit方法,即editor.commit();
DiskLruCache緩存的查找:
Bitmap bitmap = null;
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if(snapShot != null){
FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(index);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
}
之所以通過fileDescriptor是因為,通過采樣率來縮放圖片之后,會對FileInputStream的縮放存在問題,因為FileInputStream是一種有序的文件流,而兩次decodeStream調用影響了文件流的位置屬性,導致了第二次decodeStream時得到的是null;所以這里用文件描述符來解決。
mDiskLruCache.remove(key):刪除某個文件。
mDiskLruCache.delete():刪除所有緩存文件。
其他方法可自行查閱。