您的位置:首页 > 移动开发 > Android开发

关于universal-image-loader中的几种内存缓存策略

2015-11-11 17:03 381 查看

说到图片缓存,做android开发的人想必都很了解universal-image-loader这个开源框架,最近有时间把cache包里面各种缓存实现看了下,在这里对内存方面的缓存进行一个简单的总结。

首先了解一下里面主要的类:LimitedMemoryCache,基本上所有的缓存都是基于这个类进行的。它会根据你传进来的值进行缓存空间大小的设制,超过这个值之后将进行数据的清理,把内容删除到小于设定的值为止。核心代码如下:

@Override
public boolean put(String key, Bitmap value) {
boolean putSuccessfully = false;
// Try to add value to hard cache
int valueSize = getSize(value);
int sizeLimit = getSizeLimit();
int curCacheSize = cacheSize.get();
if (valueSize < sizeLimit) {
while (curCacheSize + valueSize > sizeLimit) {
Bitmap removedValue = removeNext();//抽象方法,由子类实现
if (hardCache.remove(removedValue)) {
curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
}
}
hardCache.add(value);
cacheSize.addAndGet(valueSize);

putSuccessfully = true;
}
// Add value to soft cache
super.put(key, value);
return putSuccessfully;
}


存放一张图片到内存里面的时候,会对当前已经缓存的图片大小总和即将要进行缓存的图片进行求和,通过removeNext()方法进行删除图片,每删除一次进行一次检查,直到总大小小于设定值的大小才进行缓存加载。对于这个removeNext抽象方法由子类来进行实现,删除的方式多种多样,可以按照各种算法随便发挥,了解到这个核思想之后,我们开始来介绍几种实现了这个类的缓存方案。

1.LRUMemoryCache: LRU(Least Recently Use),最近最少使用的缓存方式,就是缓存的大小超过一定的值之后,将会删除最近使用最少的那张图片。要使用这个缓存方式,我们先要了解一下LinkedHashMap,链式的哈西表,数据保存时使用的简和值都使用他们对应的哈西编码,然后使用一个双循环链表结构进行顺序维护,HashMap是一个散列存储,都是无序的,读取效率很高。LinkedHashMap在其基础上添加了一个双链表进行顺序的维护。然后我们的LRU算法也基本上都是这个存储结构实现。

private final Map<String, Bitmap> lruCache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(INITIAL_CAPACITY, LOAD_FACTOR, true));


要实现这个使用顺序维护,就要使用这个构造方法了,第一个参数是初始化容器大小,但是实际申请的大小空间会是这么一个关系:2的n次方>=INITIAL_CAPACITY , 在满足这个关系的前提下,取n的最小值。在INITIAL_CAPACITY==0的情况下比较特殊,这时候容器大小是2。LOAD_FACTOR是在容器中存放数量超过长度的这个比例时,容器将自动扩容为之前的两倍,第三个参数设置为true就是这个的关键了,把这个设置为true之后,每次在读取某一个容器的持有对象之后,这个持有的对象将会被放到尾部。

@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)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}

int hash = Collections.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;
}

private void makeTail(LinkedEntry<K, V> e) {//将访问的参数移动至链表末端。
// Unlink e
e.prv.nxt = e.nxt;
e.nxt.prv = e.prv;

// Relink e as tail
LinkedEntry<K, V> header = this.header;
LinkedEntry<K, V> oldTail = header.prv;
e.nxt = header;
e.prv = oldTail;
oldTail.nxt = header.prv = e;
modCount++;
}


方法中的accessorder就是你在构造函数里面传递进去的,只要这个是true,执行了get方法之后,就会把这个函数移动到链表的表尾,有了以上的基础,LRU似乎变得更加简单了。我们需要做的就是在每次缓存判断之后,如果需要删除图片引用,将链表中的第一个元素删除就可以了,因为最近使用的都会被移动到后面。

/** 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);
}
}
}


在每次图片放进去之后,都会使用trimToSize方法进行容器调整,调整到缓存里面的大小小于穿进来的参数,在这里穿进去的自然就是我们设置的缓存大小。

2.FIFOLimitedMemoryCache: 先进先出的一个策略。跟LRU的区别是,他每次都是简单的删除最先加入的元素,就是一个队列的形式,这个就没有上面的那个复杂了,这只需要一个List记录下保存的图片信息,每次删除队列中的第一个就行了:

@Override
protected Bitmap removeNext() {
return queue.remove(0);
}


这个子类在这个方法的实现上就是这么简单的一个操作,删除第一个元素

3.FuzzyKeyMemoryCache:这个是一个key等价删除策略,在构造器中穿一个Comparator参数进来,重写public int compare(T lhs, T rhs);这个方法,这种缓存会把已经缓存的key和将要缓存的key进行自己实现定义的方式进行对比,如果出现了这种等价就删除掉第一个对应的entry。如果没有找到就不用删除。核心代码 :

@Override
public boolean put(String key, Bitmap value) {
// Search equal key and remove this entry
synchronized (cache) {
String keyToRemove = null;
for (String cacheKey : cache.keys()) {
if (keyComparator.compare(key, cacheKey) == 0) {
keyToRemove = cacheKey;
break;
}
}
if (keyToRemove != null) {
cache.remove(keyToRemove);
}
}
return cache.put(key, value);
}


4.LargestLimitedMemoryCache:这个缓存策略是在放入缓存前检测已经缓存的总容量,如果超过了这个值就删除掉占用内存最大的元素,这个就像是以前做的java基础体,在一个数组里面找出最大值一样:

@Override
protected Bitmap removeNext() {
Integer maxSize = null;
Bitmap largestValue = null;
Set<Entry<Bitmap, Integer>> entries = valueSizes.entrySet();
synchronized (valueSizes) {
for (Entry<Bitmap, Integer> entry : entries) {
if (largestValue == null) {
largestValue = entry.getKey();
maxSize = entry.getValue();
} else {
Integer size = entry.getValue();
if (size > maxSize) {
maxSize = size;
largestValue = entry.getKey();
}
}
}
}
valueSizes.remove(largestValue);
return largestValue;
}


首先默认把第一个当作最小的,然后循环跟后面的进行比较,出现更大的就进行赋值,这样循环结束之后就可以得到占用内存最大的那个引用了。

5.LimitedAgeMemoryCache : 这个是一个时间限制的缓存策略,每次在引用一个对象到内存的时候,使用一个HashMap记录下来key和它存进去的时间,然后在访问缓存中的某张图片的时候,跟当前时间进行一个比较,如果这个两个时间的差值大于设置的缓存时间,就会把图片引用从缓存中删掉。

@Override
public boolean put(String key, Bitmap value) {
boolean putSuccesfully = cache.put(key, value);
if (putSuccesfully) {
loadingDates.put(key, System.currentTimeMillis());
}
return putSuccesfully;
}

@Override
public Bitmap get(String key) {
Long loadingDate = loadingDates.get(key);
if (loadingDate != null && System.currentTimeMillis() - loadingDate > maxAge) {
cache.remove(key);
loadingDates.remove(key);
}

return cache.get(key);
}


第一个方法在保存的时候,同时保存当前时间。第二个方法在取出的时候,进行时间的检测。

6. UsingFreqLimitedMemoryCache : 在缓存超过限制之后,根据使用频率最少的引用进行优先的删除,这个是在保存引用的时候保存的是key和使用次数,放进去的时候记录次数为0,每次get调用次数加1,然后在调用removeNext的时候,找出使用次数最少的引用,跟上面找出占用内存最大的引用的方式基本相同。

@Override
protected Bitmap removeNext() {
Integer minUsageCount = null;
Bitmap leastUsedValue = null;
Set<Entry<Bitmap, Integer>> entries = usingCounts.entrySet();
synchronized (usingCounts) {
for (Entry<Bitmap, Integer> entry : entries) {
if (leastUsedValue == null) {
leastUsedValue = entry.getKey();
minUsageCount = entry.getValue();
} else {
Integer lastValueUsage = entry.getValue();
if (lastValueUsage < minUsageCount) {
minUsageCount = lastValueUsage;
leastUsedValue = entry.getKey();
}
}
}
}
usingCounts.remove(leastUsedValue);
return leastUsedValue;
}


以上是对几种内存缓存方式的总结,之后将会推出磁盘缓存策略,算法核心是类似的,就是多了一个I/O操作。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息