Android 开源框架Universal-Image-Loader全然解析(二)--- 图片缓存策略具体解释

时间:2024-10-30 13:07:44

转载请注明本文出自xiaanming的博客(http://blog.****.net/xiaanming/article/details/26810303),请尊重他人的辛勤劳动成果,谢谢!

本篇文章继续为大家介绍Universal-Image-Loader这个开源的图片载入框架,介绍的是图片缓存策略方面的。假设大家对这个开源框架的使用还不了解。大家能够看看我之前写的一篇文章Android 开源框架Universal-Image-Loader全然解析(一)--- 基本介绍及使用,我们一般去载入大量的图片的时候,都会做缓存策略,缓存又分为内存缓存和硬盘缓存。我之前也写了几篇异步载入大量图片的文章,使用的内存缓存是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的总大小没有限制,唯一不足的地方就是不稳定,缓存的图片easy被回收掉)

上面介绍了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; /**
* A cache that holds strong references to a limited number of Bitmaps. Each time a Bitmap is accessed, it is moved to
* the head of a queue. When a Bitmap is added to a full cache, the Bitmap at the end of that queue is evicted and may
* become eligible for garbage collection.<br />
* <br />
* <b>NOTE:</b> This cache uses only strong references for stored Bitmaps.
*
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
* @since 1.8.1
*/
public class LruMemoryCache implements MemoryCacheAware<String, Bitmap> { private final LinkedHashMap<String, Bitmap> map; private final int maxSize;
/** Size of this cache in bytes */
private int size; /** @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;
this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
} /**
* 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) {
if (key == null) {
throw new NullPointerException("key == null");
} synchronized (this) {
return map.get(key);
}
} /** 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);
Bitmap previous = map.put(key, value);
if (previous != null) {
size -= sizeOf(key, previous);
}
} trimToSize(maxSize);
return true;
} /**
* 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);
}
}
} /** Removes the entry for {@code key} if it exists. */
@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); // -1 will evict 0-sized elements
} /**
* Returns the size {@code Bitmap} in bytes.
* <p/>
* An entry's size must not change while it is in the cache.
*/
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的源代码实现

/*******************************************************************************
* Copyright 2011-2013 Sergey Tarasevich
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
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; /**
* Disc cache limited by total cache size. If cache size exceeds specified limit then file with the most oldest last
* usage date will be deleted.
*
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
* @see LimitedDiscCache
* @since 1.0.0
*/
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; /**
* @param cacheDir Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's
* needed for right cache limit work.
* @param maxCacheSize Maximum cache directory size (in bytes). If cache size exceeds this limit then file with the
* most oldest last usage date will be deleted.
*/
public TotalSizeLimitedDiscCache(File cacheDir, int maxCacheSize) {
this(cacheDir, DefaultConfigurationFactory.createFileNameGenerator(), maxCacheSize);
} /**
* @param cacheDir Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's
* needed for right cache limit work.
* @param fileNameGenerator Name generator for cached files
* @param maxCacheSize Maximum cache directory size (in bytes). If cache size exceeds this limit then file with the
* most oldest last usage date will be deleted.
*/
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

/*******************************************************************************
* Copyright 2011-2013 Sergey Tarasevich
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
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; /**
* Abstract disc cache limited by some parameter. If cache exceeds specified limit then file with the most oldest last
* usage date will be deleted.
*
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
* @see BaseDiscCache
* @see FileNameGenerator
* @since 1.0.0
*/
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>()); /**
* @param cacheDir Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's
* needed for right cache limit work.
* @param sizeLimit Cache limit value. If cache exceeds this limit then file with the most oldest last usage date
* will be deleted.
*/
public LimitedDiscCache(File cacheDir, int sizeLimit) {
this(cacheDir, DefaultConfigurationFactory.createFileNameGenerator(), sizeLimit);
} /**
* @param cacheDir Directory for file caching. <b>Important:</b> Specify separate folder for cached files. It's
* needed for right cache limit work.
* @param fileNameGenerator Name generator for cached files
* @param sizeLimit Cache limit value. If cache exceeds this limit then file with the most oldest last usage date
* will be deleted.
*/
public LimitedDiscCache(File cacheDir, FileNameGenerator fileNameGenerator, int sizeLimit) {
super(cacheDir, fileNameGenerator);
this.sizeLimit = sizeLimit;
cacheSize = new AtomicInteger();
calculateCacheSizeAndFillUsageMap();
} /**
* 另开线程计算cacheDir里面文件的大小,并将文件和最后改动的毫秒数增加到Map中
*/
private void calculateCacheSizeAndFillUsageMap() {
new Thread(new Runnable() {
@Override
public void run() {
int size = 0;
File[] cachedFiles = cacheDir.listFiles();
if (cachedFiles != null) { // rarely but it can happen, don't know why
for (File cachedFile : cachedFiles) {
//getSize()是一个抽象方法。子类自行实现getSize()的逻辑
size += getSize(cachedFile);
//将文件的最后改动时间增加到map中
lastUsageDates.put(cachedFile, cachedFile.lastModified());
}
cacheSize.set(size);
}
}
}).start();
} /**
* 将文件增加到Map中。并计算缓存文件的大小是否超过了我们设置的最大缓存数
* 超过了就删除最先增加的那个文件
*/
@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; // cache is empty (have nothing to delete)
curCacheSize = cacheSize.addAndGet(-freedSize);
}
cacheSize.addAndGet(valueSize); Long currentTime = System.currentTimeMillis();
file.setLastModified(currentTime);
lastUsageDates.put(file, currentTime);
} /**
* 依据key生成文件
*/
@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;
} /**
* 抽象方法,获取文件大小
* @param file
* @return
*/
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()方法

	/**
* Creates default implementation of {@link DiskCache} depends on incoming parameters
*/
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(...)配置

今天就给大家分享到这里。有不明确的地方在以下留言。我会尽量为大家解答的,下一篇文章我将继续更深入的分析这个框架,希望大家继续关注!