本文出自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(...)配置