本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/26810303)
我們一般去加載大量的圖片的時候,都會做緩存策略,緩存又分爲內存緩存和硬盤緩存,我之前也寫了幾篇異步加載大量圖片的文章,使用的內存緩存是LruCache這個類,LRU是Least Recently Used 近期最少使用算法,我們可以給LruCache設定一個緩存圖片的最大值,它會自動幫我們管理好緩存的圖片總大小是否超過我們設定的值, 超過就刪除近期最少使用的圖片,而作爲一個強大的圖片加載框架,Universal-Image-Loader自然也提供了多種圖片的緩存策略,下面就來詳細的介紹下
內存緩存
首先我們來了解下什麼是強引用和什麼是弱引用?
強引用是指創建一個對象並把這個對象賦給一個引用變量, 強引用有引用變量指向時永遠不會被垃圾回收。即使內存不足的時候寧願報OOM也不被垃圾回收器回收,我們new的對象都是強引用
弱引用通過weakReference類來實現,它具有很強的不確定性,如果垃圾回收器掃描到有着WeakReference的對象,就會將其回收釋放內存
現在我們來看Universal-Image-Loader有哪些內存緩存策略
1. 只使用的是強引用緩存
- LruMemoryCache(這個類就是這個開源框架默認的內存緩存類,緩存的是bitmap的強引用,下面我會從源碼上面分析這個類)
2.使用強引用和弱引用相結合的緩存有
- UsingFreqLimitedMemoryCache(如果緩存的圖片總量超過限定值,先刪除使用頻率最小的bitmap)
- LRULimitedMemoryCache(這個也是使用的lru算法,和LruMemoryCache不同的是,他緩存的是bitmap的弱引用)
- FIFOLimitedMemoryCache(先進先出的緩存策略,當超過設定值,先刪除最先加入緩存的bitmap)
- LargestLimitedMemoryCache(當超過緩存限定值,先刪除最大的bitmap對象)
- LimitedAgeMemoryCache(當 bitmap加入緩存中的時間超過我們設定的值,將其刪除)
3.只使用弱引用緩存
- WeakMemoryCache(這個類緩存bitmap的總大小沒有限制,唯一不足的地方就是不穩定,緩存的圖片容易被回收掉)
上面介紹了Universal-Image-Loader所提供的所有的內存緩存的類,當然我們也可以使用我們自己寫的內存緩存類,我們還要看看要怎麼將這些內存緩存加入到我們的項目中,我們只需要配置ImageLoaderConfiguration.memoryCache(...),如下
-
ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(this)
-
.memoryCache(new WeakMemoryCache())
-
.build();
下面我們來分析LruMemoryCache這個類的源代碼
-
package com.nostra13.universalimageloader.cache.memory.impl;
-
-
import android.graphics.Bitmap;
-
import com.nostra13.universalimageloader.cache.memory.MemoryCacheAware;
-
-
import java.util.Collection;
-
import java.util.HashSet;
-
import java.util.LinkedHashMap;
-
import java.util.Map;
-
-
-
-
-
-
-
-
-
-
-
-
public class LruMemoryCache implements MemoryCacheAware<String, Bitmap> {
-
-
private final LinkedHashMap<String, Bitmap> map;
-
-
private final int maxSize;
-
-
private int size;
-
-
-
public LruMemoryCache(int maxSize) {
-
if (maxSize <= 0) {
-
throw new IllegalArgumentException("maxSize <= 0");
-
}
-
this.maxSize = maxSize;
-
this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
-
}
-
-
-
-
-
-
@Override
-
public final Bitmap get(String key) {
-
if (key == null) {
-
throw new NullPointerException("key == null");
-
}
-
-
synchronized (this) {
-
return map.get(key);
-
}
-
}
-
-
-
@Override
-
public final boolean put(String key, Bitmap value) {
-
if (key == null || value == null) {
-
throw new NullPointerException("key == null || value == null");
-
}
-
-
synchronized (this) {
-
size += sizeOf(key, value);
-
Bitmap previous = map.put(key, value);
-
if (previous != null) {
-
size -= sizeOf(key, previous);
-
}
-
}
-
-
trimToSize(maxSize);
-
return true;
-
}
-
-
-
-
-
-
-
private void trimToSize(int maxSize) {
-
while (true) {
-
String key;
-
Bitmap value;
-
synchronized (this) {
-
if (size < 0 || (map.isEmpty() && size != 0)) {
-
throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
-
}
-
-
if (size <= maxSize || map.isEmpty()) {
-
break;
-
}
-
-
Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
-
if (toEvict == null) {
-
break;
-
}
-
key = toEvict.getKey();
-
value = toEvict.getValue();
-
map.remove(key);
-
size -= sizeOf(key, value);
-
}
-
}
-
}
-
-
-
@Override
-
public final void remove(String key) {
-
if (key == null) {
-
throw new NullPointerException("key == null");
-
}
-
-
synchronized (this) {
-
Bitmap previous = map.remove(key);
-
if (previous != null) {
-
size -= sizeOf(key, previous);
-
}
-
}
-
}
-
-
@Override
-
public Collection<String> keys() {
-
synchronized (this) {
-
return new HashSet<String>(map.keySet());
-
}
-
}
-
-
@Override
-
public void clear() {
-
trimToSize(-1);
-
}
-
-
-
-
-
-
-
private int sizeOf(String key, Bitmap value) {
-
return value.getRowBytes() * value.getHeight();
-
}
-
-
@Override
-
public synchronized final String toString() {
-
return String.format("LruCache[maxSize=%d]", maxSize);
-
}
-
}
我們可以看到這個類中維護的是一個LinkedHashMap,在LruMemoryCache構造函數中我們可以看到,我們爲其設置了一個緩存圖片的最大值maxSize,並實例化LinkedHashMap, 而從LinkedHashMap構造函數的第三個參數爲ture,表示它是按照訪問順序進行排序的,
我們來看將bitmap加入到LruMemoryCache的方法put(String key, Bitmap value), 第61行,sizeOf()是計算每張圖片所佔的byte數,size是記錄當前緩存bitmap的總大小,如果該key之前就緩存了bitmap,我們需要將之前的bitmap減掉去,接下來看trimToSize()方法,我們直接看86行,如果當前緩存的bitmap總數小於設定值maxSize,不做任何處理,如果當前緩存的bitmap總數大於maxSize,刪除LinkedHashMap中的第一個元素,size中減去該bitmap對應的byte數
我們可以看到該緩存類比較簡單,邏輯也比較清晰,如果大家想知道其他內存緩存的邏輯,可以去分析分析其源碼,在這裏我簡單說下FIFOLimitedMemoryCache的實現邏輯,該類使用的HashMap來緩存bitmap的弱引用,然後使用LinkedList來保存成功加入到FIFOLimitedMemoryCache的bitmap的強引用,如果加入的FIFOLimitedMemoryCache的bitmap總數超過限定值,直接刪除LinkedList的第一個元素,所以就實現了先進先出的緩存策略,其他的緩存都類似,有興趣的可以去看看。
硬盤緩存
接下來就給大家分析分析硬盤緩存的策略,這個框架也提供了幾種常見的緩存策略,當然如果你覺得都不符合你的要求,你也可以自己去擴展
- FileCountLimitedDiscCache(可以設定緩存圖片的個數,當超過設定值,刪除掉最先加入到硬盤的文件)
- LimitedAgeDiscCache(設定文件存活的最長時間,當超過這個值,就刪除該文件)
- TotalSizeLimitedDiscCache(設定緩存bitmap的最大值,當超過這個值,刪除最先加入到硬盤的文件)
- UnlimitedDiscCache(這個緩存類沒有任何的限制)
下面我們就來分析分析TotalSizeLimitedDiscCache的源碼實現
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
package com.nostra13.universalimageloader.cache.disc.impl;
-
-
import com.nostra13.universalimageloader.cache.disc.LimitedDiscCache;
-
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
-
import com.nostra13.universalimageloader.core.DefaultConfigurationFactory;
-
import com.nostra13.universalimageloader.utils.L;
-
-
import java.io.File;
-
-
-
-
-
-
-
-
-
-
public class TotalSizeLimitedDiscCache extends LimitedDiscCache {
-
-
private static final int MIN_NORMAL_CACHE_SIZE_IN_MB = 2;
-
private static final int MIN_NORMAL_CACHE_SIZE = MIN_NORMAL_CACHE_SIZE_IN_MB * 1024 * 1024;
-
-
-
-
-
-
-
-
public TotalSizeLimitedDiscCache(File cacheDir, int maxCacheSize) {
-
this(cacheDir, DefaultConfigurationFactory.createFileNameGenerator(), maxCacheSize);
-
}
-
-
-
-
-
-
-
-
-
public TotalSizeLimitedDiscCache(File cacheDir, FileNameGenerator fileNameGenerator, int maxCacheSize) {
-
super(cacheDir, fileNameGenerator, maxCacheSize);
-
if (maxCacheSize < MIN_NORMAL_CACHE_SIZE) {
-
L.w("You set too small disc cache size (less than %1$d Mb)", MIN_NORMAL_CACHE_SIZE_IN_MB);
-
}
-
}
-
-
@Override
-
protected int getSize(File file) {
-
return (int) file.length();
-
}
-
}
這個類是繼承LimitedDiscCache,除了兩個構造函數之外,還重寫了getSize()方法,返回文件的大小,接下來我們就來看看LimitedDiscCache
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
package com.nostra13.universalimageloader.cache.disc;
-
-
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
-
import com.nostra13.universalimageloader.core.DefaultConfigurationFactory;
-
-
import java.io.File;
-
import java.util.Collections;
-
import java.util.HashMap;
-
import java.util.Map;
-
import java.util.Map.Entry;
-
import java.util.Set;
-
import java.util.concurrent.atomic.AtomicInteger;
-
-
-
-
-
-
-
-
-
-
-
public abstract class LimitedDiscCache extends BaseDiscCache {
-
-
private static final int INVALID_SIZE = -1;
-
-
-
private final AtomicInteger cacheSize;
-
-
private final int sizeLimit;
-
private final Map<File, Long> lastUsageDates = Collections.synchronizedMap(new HashMap<File, Long>());
-
-
-
-
-
-
-
-
public LimitedDiscCache(File cacheDir, int sizeLimit) {
-
this(cacheDir, DefaultConfigurationFactory.createFileNameGenerator(), sizeLimit);
-
}
-
-
-
-
-
-
-
-
-
public LimitedDiscCache(File cacheDir, FileNameGenerator fileNameGenerator, int sizeLimit) {
-
super(cacheDir, fileNameGenerator);
-
this.sizeLimit = sizeLimit;
-
cacheSize = new AtomicInteger();
-
calculateCacheSizeAndFillUsageMap();
-
}
-
-
-
-
-
private void calculateCacheSizeAndFillUsageMap() {
-
new Thread(new Runnable() {
-
@Override
-
public void run() {
-
int size = 0;
-
File[] cachedFiles = cacheDir.listFiles();
-
if (cachedFiles != null) {
-
for (File cachedFile : cachedFiles) {
-
-
size += getSize(cachedFile);
-
-
lastUsageDates.put(cachedFile, cachedFile.lastModified());
-
}
-
cacheSize.set(size);
-
}
-
}
-
}).start();
-
}
-
-
-
-
-
-
@Override
-
public void put(String key, File file) {
-
-
int valueSize = getSize(file);
-
-
-
int curCacheSize = cacheSize.get();
-
-
while (curCacheSize + valueSize > sizeLimit) {
-
int freedSize = removeNext();
-
if (freedSize == INVALID_SIZE) break;
-
curCacheSize = cacheSize.addAndGet(-freedSize);
-
}
-
cacheSize.addAndGet(valueSize);
-
-
Long currentTime = System.currentTimeMillis();
-
file.setLastModified(currentTime);
-
lastUsageDates.put(file, currentTime);
-
}
-
-
-
-
-
@Override
-
public File get(String key) {
-
File file = super.get(key);
-
-
Long currentTime = System.currentTimeMillis();
-
file.setLastModified(currentTime);
-
lastUsageDates.put(file, currentTime);
-
-
return file;
-
}
-
-
-
-
-
@Override
-
public void clear() {
-
lastUsageDates.clear();
-
cacheSize.set(0);
-
super.clear();
-
}
-
-
-
-
-
-
private int removeNext() {
-
if (lastUsageDates.isEmpty()) {
-
return INVALID_SIZE;
-
}
-
Long oldestUsage = null;
-
File mostLongUsedFile = null;
-
-
Set<Entry<File, Long>> entries = lastUsageDates.entrySet();
-
synchronized (lastUsageDates) {
-
for (Entry<File, Long> entry : entries) {
-
if (mostLongUsedFile == null) {
-
mostLongUsedFile = entry.getKey();
-
oldestUsage = entry.getValue();
-
} else {
-
Long lastValueUsage = entry.getValue();
-
if (lastValueUsage < oldestUsage) {
-
oldestUsage = lastValueUsage;
-
mostLongUsedFile = entry.getKey();
-
}
-
}
-
}
-
}
-
-
int fileSize = 0;
-
if (mostLongUsedFile != null) {
-
if (mostLongUsedFile.exists()) {
-
fileSize = getSize(mostLongUsedFile);
-
if (mostLongUsedFile.delete()) {
-
lastUsageDates.remove(mostLongUsedFile);
-
}
-
} else {
-
lastUsageDates.remove(mostLongUsedFile);
-
}
-
}
-
return fileSize;
-
}
-
-
-
-
-
-
-
protected abstract int getSize(File file);
-
}
在構造方法中,第69行有一個方法calculateCacheSizeAndFillUsageMap(),該方法是計算cacheDir的文件大小,並將文件和文件的最後修改時間加入到Map中
然後是將文件加入硬盤緩存的方法put(),在106行判斷當前文件的緩存總數加上即將要加入緩存的文件大小是否超過緩存設定值,如果超過了執行removeNext()方法,接下來就來看看這個方法的具體實現,150-167中找出最先加入硬盤的文件,169-180中將其從文件硬盤中刪除,並返回該文件的大小,刪除成功之後成員變量cacheSize需要減掉改文件大小。
FileCountLimitedDiscCache這個類實現邏輯跟TotalSizeLimitedDiscCache是一樣的,區別在於getSize()方法,前者返回1,表示爲文件數是1,後者返回文件的大小。
等我寫完了這篇文章,我才發現FileCountLimitedDiscCache和TotalSizeLimitedDiscCache在最新的源碼中已經刪除了,加入了LruDiscCache,由於我的是之前的源碼,所以我也不改了,大家如果想要了解LruDiscCache可以去看最新的源碼,我這裏就不介紹了,還好內存緩存的沒變化,下面分析的是最新的源碼中的部分,我們在使用中可以不自行配置硬盤緩存策略,直接用DefaultConfigurationFactory中的就行了
我們看DefaultConfigurationFactory這個類的createDiskCache()方法
-
-
-
-
public static DiskCache createDiskCache(Context context, FileNameGenerator diskCacheFileNameGenerator,
-
long diskCacheSize, int diskCacheFileCount) {
-
File reserveCacheDir = createReserveDiskCacheDir(context);
-
if (diskCacheSize > 0 || diskCacheFileCount > 0) {
-
File individualCacheDir = StorageUtils.getIndividualCacheDirectory(context);
-
LruDiscCache diskCache = new LruDiscCache(individualCacheDir, diskCacheFileNameGenerator, diskCacheSize,
-
diskCacheFileCount);
-
diskCache.setReserveCacheDir(reserveCacheDir);
-
return diskCache;
-
} else {
-
File cacheDir = StorageUtils.getCacheDirectory(context);
-
return new UnlimitedDiscCache(cacheDir, reserveCacheDir, diskCacheFileNameGenerator);
-
}
-
}
如果我們在ImageLoaderConfiguration中配置了diskCacheSize和diskCacheFileCount,他就使用的是LruDiscCache,否則使用的是UnlimitedDiscCache,在最新的源碼中還有一個硬盤緩存類可以配置,那就是LimitedAgeDiscCache,可以在ImageLoaderConfiguration.diskCache(...)配置