缓存
提高用户体验,同时也使得应用更加流畅,也就是缓存图片至内存时,可以更加高效的工作。
配置
在应用中配置ImageLoaderConfiguration参数(注意:只配置一次就好了,如多次配置,则默认第一次的配置参数)
默认设置(即框架已配置好了参数)
ImageLoaderConfiguration configuration = ImageLoaderConfiguration.createDefault(this);
自定义设置
File cacheDir = StorageUtils.getCacheDirectory(context); //缓存文件夹路径
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.memoryCacheExtraOptions(480, 800) // default = device screen dimensions 内存缓存文件的最大长宽
// .diskCacheExtraOptions(480, 800, null) // 本地缓存的详细信息(缓存的最大长宽),最好不要设置这个
.taskExecutor(...)
.taskExecutorForCachedImages(...)
.threadPoolSize(5) // default是3个线程池 线程池内加载的数量
.threadPriority(Thread.NORM_PRIORITY - 2) // default 设置当前线程的优先级
.tasksProcessingOrder(QueueProcessingType.FIFO) // default
.denyCacheImageMultipleSizesInMemory()
.memoryCache(new LruMemoryCache(2 * 1024 * 1024)) //可以通过自己的内存缓存实现
.memoryCacheSize(2 * 1024 * 1024) // 内存缓存的最大值
.memoryCacheSizePercentage(13) // default
.diskCache(new UnlimitedDiscCache(cacheDir)) // default ,可以自定义缓存路径
.diskCacheSize(50 * 1024 * 1024) // 50 Mb sd卡(本地)缓存的最大值
.diskCacheFileCount(100) // 可以缓存的文件数量
// default为使用HASHCODE对UIL进行加密命名, 还可以用MD5(new Md5FileNameGenerator())加密
.diskCacheFileNameGenerator(new HashCodeFileNameGenerator())
.imageDownloader(new BaseImageDownloader(context)) // default
.imageDecoder(new BaseImageDecoder()) // default
.defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default
.writeDebugLogs() // 打印debug log
ImageLoader.getInstance().init(config.build()); //初始化配置,开始构建
主体有三个,分别是UI,缓存模块和数据源(网络)。
UI: 请求数据,使用唯一的Key值索引Memory Cache中的Bitmap。
缓存模块:分两种,一是内存缓存,通过缓存搜索,如果能找到Key值对应的Bitmap,则返回数据。二是硬盘存储,使用唯一Key值对应的文件名,检索SDCard上的文件。
默认的内存缓存实现是LruMemoryCache,磁盘缓存是UnlimitedDiscCache。
- 先说说 MemoryCache 接口实现,底下有很多实现类
- 它负责定义通用规则,具体的实现工作由不同缓存算法的子类去实现即可。
- 这是一个继承和多态的体现。
- 当前分析的就是 LruMemoryCache 类,这也是默认的缓存策略
- 内存缓存策略设计与实现
只使用的是强引用缓存
LruMemoryCache(这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用,下面我会从源码上面分析这个类)使用强引用和弱引用相结合的缓存有
UsingFreqLimitedMemoryCache(如果缓存的图片总量超过限定值,先删除使用频率最小的bitmap)
LRULimitedMemoryCache(这个也是使用的lru算法,和LruMemoryCache不同的是,他缓存的是bitmap的弱引用)
FIFOLimitedMemoryCache(先进先出的缓存策略,当超过设定值,先删除最先加入缓存的bitmap)
LargestLimitedMemoryCache(当超过缓存限定值,先删除最大的bitmap对象)
LimitedAgeMemoryCache(当 bitmap加入缓存中的时间超过我们设定的值,将其删除)只使用弱引用缓存
WeakMemoryCache(这个类缓存bitmap的总大小没有限制,唯一不足的地方就是不稳定,缓存的图片容易被回收掉)
LruMemoryCach
LruMemoryCach这也是默认的缓存策略
1、最近最少使用算法。
2、当图片缓存到磁盘之后,然后就需要缓存到内存中。
3、需要定义的缓存空间是多大呢?(默认大小为 app 可用空间的 1/8 大小。trimToSize 方法可以确保当前空间还有剩余。)
4、缓存的数据结构是什么呢?(LinkedHashMap<String,Map>)
LruMemoryCache:一种使用强引用来保存有数量限制的Bitmap的cache(在空间有限的情况,保留最近使用过的Bitmap)。每次Bitmap被访问时,它就被移动到一个队列的头部。当Bitmap被添加到一个空间已满的cache时,在队列末尾的Bitmap会被挤出去并变成适合被GC回收的状态。
注意:这个cache只使用强引用来保存Bitmap。
/** @param maxSize Maximum sum of the sizes of the Bitmaps in this cache */
public LruMemoryCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
//使用LinkedHashMap来缓存数据
this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
}
思考:为什么不用HashMap来缓存数据?好了,我们继续看源码
LruMemoryCache.get(…);
/**
* Returns the Bitmap for {@code key} if it exists in the cache. If a Bitmap was returned, it is moved to the head
* of the queue. This returns null if a Bitmap is not cached.
*/
@Override
public final Bitmap get(String key) {
//代码中除了异常判断,就是利用synchronized进行同步控制。
if (key == null) {
throw new NullPointerException("key == null");
}
synchronized (this) {
return map.get(key);
}
}
public V get(Object key) {
LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
LruMemoryCache保留在空间有限的情况下保留最近使用过的Bitmap。
LinkedHashMap中的get()方法不仅返回所匹配的值,并且在返回前还会将所匹配的key对应的entry调整在列表中的顺序(LinkedHashMap使用双链表来保存数据,不懂可以去看看Map集合类了解),让它处于列表的最后。当然,这种情况必须是在LinkedHashMap中accessOrder==true的情况下才生效的,反之就是get()方法不会改变被匹配的key对应的entry在列表中的位置。
@Override
public V get(Object key) {
/*
* This method is overridden to eliminate the need for a polymorphic
* invocation in superclass at the expense of code duplication.
*/
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
if (e == null)
return null;
if (accessOrder)//调整entry在列表中的位置,其实就是双向链表的调整。它判断accessOrder
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
// Replace with Collections.secondaryHash when the VM is fast enough (http://b/8290590).
int hash = secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
}
return null;
}
结论
LruMemoryCache缓存的存储数据结构是LinkedHashMap<String,Map>,在LinkedHashMap.get()方法执行后,LinkedHashMap中entry的顺序会得到调整。
如果图片过多,就会保留最近使用的,其他都要被回收,怎么才不被剔出呢?
需要定义的缓存空间是多大呢?
1、默认大小为 app 可用空间的 1/8 大小。
2、trimToSize 方法可以确保当前空间还有剩余。
LruMemoryCache中的trimToSize(…)这个函数就是用来限定LruMemoryCache的大小不要超过用户限定的大小,cache的大小由用户在LruMemoryCache刚开始初始化的时候限定。那么就看看LruMemoryCache中的put方法。
/** Caches {@code Bitmap} for {@code key}. The Bitmap is moved to the head of the queue. */
@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);
//map.put()的返回值如果不为空,说明存在跟key对应的entry,put操作只是更新原有key对应的entry
Bitmap previous = map.put(key, value);
if (previous != null) {
size -= sizeOf(key, previous);
}
}
trimToSize(maxSize);
return true;
}
当Bitmap缓存的大小超过原来设定的maxSize时应该是在trimToSize(…)这个函数中做到的。遍历map,将多余的项(代码中对应toEvict)剔除掉,直到当前cache的大小等于或小于限定的大小。
/**
* Remove the eldest entries until the total of remaining entries is at or below the requested size.
*
* @param maxSize the maximum size of the cache before returning. May be -1 to evict even 0-sized elements.
*/
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);
}
}
}
磁盘缓存策略设计与实现
自己实现磁盘缓存,要考虑的太多,UIL提供了几种常见的磁盘缓存策略,当然你觉得都不符合你的要求,你也可以自己去扩展的。
- FileCountLimitedDiscCache(可以设定缓存图片的个数,当超过设定值,删除掉最先加入到硬盘的文件)
- LimitedAgeDiscCache(设定文件存活的最长时间,当超过这个值,就删除该文件)
- TotalSizeLimitedDiscCache(设定缓存bitmap的最大值,当超过这个值,删除最先加入到硬盘的文件)
- UnlimitedDiscCache(这个缓存类没有任何的限制)
在UIL中有着比较完整的存储策略,根据预先指定的空间大小,使用频率(生命周期),文件个数的约束条件,都有着对应的实现策略。最基础的接口DiscCacheAware和抽象类BaseDiscCache
UnlimitedDiscCache解析
UnlimitedDiscCache实现DiskCache接口,是ImageLoaderConfiguration中默认的磁盘缓存处理。用它的时候,磁盘缓存的大小是不受限的。
接下来看看实现UnlimitedDiscCache的源代码,通过源代码我们发现他其实就是继承了BaseDiscCache,这个类内部没有实现自己独特的方法,也没有重写什么,那么我们就直接看BaseDiscCache这个类。BaseDiscCache继承了DiscCache。在分析这个类之前,我们先想想自己实现一个磁盘缓存需要做多少麻烦的事情:
- 图片的命名会不会重。你没有办法知道用户下载的图片原始的文件名是怎么样的,因此很可能因为文件重名将有用的图片给覆盖掉了。
- 当应用卡顿或网络延迟的时候,同一张图片反复被下载。
- 处理图片写入磁盘可能遇到的延迟和同步问题。
直接上源码
/**
* @param cacheDir Directory for file caching
* @param reserveCacheDir null-ok; Reserve directory for file caching. It's used when the primary directory isn't available.
* @param fileNameGenerator {@linkplain com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator
* Name generator} for cached files
*/
public BaseDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator) {
if (cacheDir == null) {
throw new IllegalArgumentException("cacheDir" + ERROR_ARG_NULL);
}
if (fileNameGenerator == null) {
throw new IllegalArgumentException("fileNameGenerator" + ERROR_ARG_NULL);
}
this.cacheDir = cacheDir;
this.reserveCacheDir = reserveCacheDir;
this.fileNameGenerator = fileNameGenerator;
}
我们看一下BaseDiscCache的构造函数:
cacheDir: 文件缓存目录
reserveCacheDir: 备用的文件缓存目录,可以为null。它只有当cacheDir不能用的时候才有用。
fileNameGenerator: 文件名生成器。为缓存的文件生成文件名。
当看到fileNameGenerator,有点疑虑,我们并没有设置文件名啊?查阅代码发现是默认生成DefaultConfigurationFactory.createFileNameGenerator())。
代码如下:
/**
* @param cacheDir Directory for file caching
* @param reserveCacheDir null-ok; Reserve directory for file caching. It's used when the primary directory isn't available.
*/
public BaseDiskCache(File cacheDir, File reserveCacheDir) {
this(cacheDir, reserveCacheDir, DefaultConfigurationFactory.createFileNameGenerator());
}
/** Creates {@linkplain HashCodeFileNameGenerator default implementation} of FileNameGenerator */
public static FileNameGenerator createFileNameGenerator() {
return new HashCodeFileNameGenerator();
}
/**
* Names image file as image URI {@linkplain String#hashCode() hashcode}
*
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
* @since 1.3.1
*/
public class HashCodeFileNameGenerator implements FileNameGenerator {
@Override
public String generate(String imageUri) {
return String.valueOf(imageUri.hashCode());
}
}
UIL中有3种文件命名策略,默认的文件命名策略DefaultConfigurationFactory.createFileNameGenerator()。它是一个HashCodeFileNameGenerator。用String.hashCode()进行文件名的生成,这样也不会生成重复的文件名,就那么简单。
接着看看是如何存储数据的
直接看源码
@Override
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
//主要用于生成一个指向缓存目录中的文件,在这个函数里面调用了刚刚介绍过的fileNameGenerator来生成文件名。
File imageFile = getFile(imageUri);
//它是用来写入bitmap的临时文件,然后就把这个文件给删除了
File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
boolean loaded = false;
try {
OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
try {
loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);
} finally {
IoUtils.closeSilently(os);
}
} finally {
if (loaded && !tmpFile.renameTo(imageFile)) {
loaded = false;
}
if (!loaded) {
tmpFile.delete();
}
}
return loaded;
}
UIL加载图片的一般流程是先判断内存中是否有对应的Bitmap,再判断磁盘(disk)中是否有,如果没有就从网络中加载。最后根据原先在UIL中的配置判断是否需要缓存Bitmap到内存或磁盘中。也就是说,当需要调用BaseDiscCache.save(…)之前,其实已经判断过这个文件不在磁盘中。
/** Returns file object (not null) for incoming image URI. File object can reference to non-existing file. */
protected File getFile(String imageUri) {
//利用fileNameGenerator生成一个唯一的文件名
String fileName = fileNameGenerator.generate(imageUri);
File dir = cacheDir;
//当cacheDir不可用的时候,就是用reserveCachedir作为缓存目录了。
if (!cacheDir.exists() && !cacheDir.mkdirs()) {
if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
dir = reserveCacheDir;
}
}
//最后返回一个指向文件的对象,但是要注意当File类型的对象指向的文件不存在时,file会为null,而不是报错。
return new File(dir, fileName);
}
总结
内存缓存其实就是利用Map接口的对象在内存中进行缓存,可能有不同的存储机制。磁盘缓存其实就是将文件写入磁盘。这样就达到缓存目的。