本片博客主要讲解以下两个问题
介绍一下常见的内存缓存算法
怎样实现这些算法
大家应该对ImageLoader这个框架都不陌生吧,一个很强大的图片加载框架,虽然作者去年的时候已经停止维护了,但里面的许多东西还是值得我们去学习的。本篇博客讲解的内存缓存算法正是基于ImageLoader的实现基础之上的
常见的几种缓存算法
(1)LRU即Least RecentlyUsed,近期最少使用算法。
也就是当内存缓存达到设定的最大值时将内存缓存中近期最少使用的对象移除,有效的避免了OOM的出现。
(2)Least Frequently Used(LFU)
对每个缓存对象计算他们被使用的频率。把最不常用的缓存对象换走。
(3)、First in First out(FIFO)
这是一个低负载的算法,并且对缓存对象的管理要求不高。通过一个队列去跟踪所有的缓存对象,最近最常用的缓存对象放在后面,而更早的缓存对象放在前面,当缓存容量满时,排在前面的缓存对象会被踢走,然后把新的缓存对象加进去。
(4)、Simple time-based
通过绝对的时间周期去失效那些缓存对象。对于新增的对象,保存特定的时间。
(5)、LargestLimitedMemoryCache
超过指定缓存的话,每次移除栈最大内存的缓存的对象
下面我们一起来看一下ImageLoader是怎样实现这些算法的
首先我们一起先来看一下类UML图
源码分析
1)首先我们先来看一下BaseMemoryCache做了什么?
public abstract class BaseMemoryCache implements MemoryCache { /** Stores not strong references to objects */ private final Map<String, Reference<Bitmap>> softMap = Collections.synchronizedMap(new HashMap<String, Reference<Bitmap>>()); @Override public Bitmap get(String key) { Bitmap result = null; Reference<Bitmap> reference = softMap.get(key); if (reference != null) { result = reference.get(); } return result; } @Override public boolean put(String key, Bitmap value) { softMap.put(key, createReference(value)); return true; } @Override public Bitmap remove(String key) { Reference<Bitmap> bmpRef = softMap.remove(key); return bmpRef == null ? null : bmpRef.get(); } @Override public Collection<String> keys() { synchronized (softMap) { return new HashSet<String>(softMap.keySet()); } } @Override public void clear() { softMap.clear(); } /** Creates {@linkplain Reference not strong} reference of value */ protected abstract Reference<Bitmap> createReference(Bitmap value); }
其实就是保存着一份弱引用而已,而它的父类Memory只是定义了几个接口方法,统一标准而已
2)接着我们来看LimitedMemoryCache做了什么?
public abstract class LimitedMemoryCache extends BaseMemoryCache { private static final int MAX_NORMAL_CACHE_SIZE_IN_MB = 16; private static final int MAX_NORMAL_CACHE_SIZE = MAX_NORMAL_CACHE_SIZE_IN_MB * 1024 * 1024; private final int sizeLimit; private final AtomicInteger cacheSize; /** * Contains strong references to stored objects. Each next object is added last. If hard cache size will exceed * limit then first object is deleted (but it continue exist at {@link #softMap} and can be collected by GC at any * time) */ private final List<Bitmap> hardCache = Collections.synchronizedList( new LinkedList<Bitmap>()); /** @param sizeLimit Maximum size for cache (in bytes) */ public LimitedMemoryCache(int sizeLimit) { this.sizeLimit = sizeLimit; cacheSize = new AtomicInteger(); if (sizeLimit > MAX_NORMAL_CACHE_SIZE) { L.w("You set too large memory cache size (more than %1$d Mb)", MAX_NORMAL_CACHE_SIZE_IN_MB); } } @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; } @Override public Bitmap remove(String key) { Bitmap value = super.get(key); if (value != null) { if (hardCache.remove(value)) { cacheSize.addAndGet(-getSize(value)); } } return super.remove(key); } @Override public void clear() { hardCache.clear(); cacheSize.set(0); super.clear(); } protected int getSizeLimit() { return sizeLimit; } protected abstract int getSize(Bitmap value); protected abstract Bitmap removeNext(); }
LimitedMemoryCache所做的工作可以分为以下几步
1) 保存着一份强引用
private final List<Bitmap> hardCache = Collections.synchronizedList( new LinkedList<Bitmap>());
2) 其实在我们调用put方法的时候,即我们把bitmap存进内存的时候,他会判断是否超出我们的最大值,超出我们的最大值就会调用removeNext();来获得我们将要移除的bitmap对象,最终再调用hardCache.remove(removedValue)去移除它。
3) 注意到removeNext()方法是抽象方法,交给子类自己去实现自己的算法逻辑。
注意事项
结合BaseMemoryCache和LimitedMemoryCache,我们可以知道LimitedMemoryCache的子类,至少可以访问两份Bitmap 的缓存,一份是BaseMemoryCache所拥有的softMap ,是弱引用;一份是LimitedMemoryCachehar所拥有的hardCache 集合
//父类BaseMemoryCache的成员变量,并且每次在操作的时候都会把bitmap的弱引用存进去 private final Map<String, Reference<Bitmap>> softMap = Collections.synchronizedMap( new HashMap<String, Reference<Bitmap>>());//LimitedMemoryCache的成员变量,缓存的bitmap是强引用private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());
有人可能会有疑问了这些成员变量不是私有的吗?为什么说LimitedMemoryCache的子类,至少可以访问两份引用,这点我们可以从他们的put方法和get方法中知道,只需要调用super.put()即可把我们的bitmap缓存存到父类,调用super.get()即可从父类中 访问我们保存的Bitmap对象 。
@Overridepublic 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; }
同理LimitedMemoryCache的子类put也会调用LimitedMemoryCache的put方法,代码见下面分析。
同时从上面的分析当中我们可以知道主要关心put和removeNext()这两个方法就可以了,put()方法其实就是把bitmap对象存进我们的queue队列中
下面我们在看一下UsingFreqLimitedMemoryCache是怎样实现的?
public class UsingFreqLimitedMemoryCache extends LimitedMemoryCache { /** * Contains strong references to stored objects (keys) and last object usage date (in milliseconds). If hard cache * size will exceed limit then object with the least frequently usage is deleted (but it continue exist at * {@link #softMap} and can be collected by GC at any time) */ private final Map<Bitmap, Integer> usingCounts = Collections.synchronizedMap(new HashMap<Bitmap, Integer>()); public UsingFreqLimitedMemoryCache(int sizeLimit) { super(sizeLimit); } @Override public boolean put(String key, Bitmap value) { if (super.put(key, value)) { usingCounts.put(value, 0); return true; } else { return false; } } @Override public Bitmap get(String key) { Bitmap value = super.get(key); // Increment usage count for value if value is contained in hardCahe if (value != null) { Integer usageCount = usingCounts.get(value); if (usageCount != null) { usingCounts.put(value, usageCount + 1); } } return value; } @Override public Bitmap remove(String key) { Bitmap value = super.get(key); if (value != null) { usingCounts.remove(value); } return super.remove(key); } @Override public void clear() { usingCounts.clear(); super.clear(); } @Override protected int getSize(Bitmap value) { return value.getRowBytes() * value.getHeight(); } @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; } @Override protected Reference<Bitmap> createReference(Bitmap value) { return new WeakReference<Bitmap>(value); } }
思路解析
当我们调用put方法,把bitmap存进内存的时候,他会判断是否超出我们的最大值,超出我们的最大值就会调用removeNext();来获得我们将要移除的bitmap对象,最终再调用hardCache.remove(removedValue)去移除它。
@Overridepublic boolean put(String key, Bitmap value) {boolean putSuccessfully = false;// Try to add value to hard cacheint 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 cachesuper.put(key, value);return putSuccessfully; } ··· * 下面我们来看一下removeNext()是怎样获得将要移除的bitmap对象的? ```javaprivate final Map<Bitmap, Integer> usingCounts = Collections. synchronizedMap(new HashMap<Bitmap, Integer>());@Overrideprotected 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; }
其实就是将usingCounts中出现次数最少的节点移除掉。
那它实在什么时候计算bitmap的使用次数的呢?相信大多数人会想到,既然是使用频率,那肯定是在取图片的过程中计算的,没错,下面让我们一起来看一下是怎样实现的?
@Overridepublic Bitmap get(String key) { Bitmap value = super.get(key);// Increment usage count for value if value is contained in hardCaheif (value != null) { Integer usageCount = usingCounts.get(value); if (usageCount != null) { usingCounts.put(value, usageCount + 1); } }return value; }
其实也很简单,判断是否存在缓存value,存在的话,使用次数加一
好的,到此UsingFreqLimitedMemoryCache的源码分析位置
FIFOLimitedMemoryCache源码分析
public class FIFOLimitedMemoryCache extends LimitedMemoryCache { private final List<Bitmap> queue = Collections.synchronizedList(new LinkedList<Bitmap>()); public FIFOLimitedMemoryCache(int sizeLimit) { super(sizeLimit); } @Override public boolean put(String key, Bitmap value) { if (super.put(key, value)) { queue.add(value); return true; } else { return false; } } @Override public Bitmap remove(String key) { Bitmap value = super.get(key); if (value != null) { queue.remove(value); } return super.remove(key); } @Override public void clear() { queue.clear(); super.clear(); } @Override protected int getSize(Bitmap value) { return value.getRowBytes() * value.getHeight(); } @Override protected Bitmap removeNext() { return queue.remove(0); } @Override protected Reference<Bitmap> createReference(Bitmap value) { return new WeakReference<Bitmap>(value); } }
1)从上面的分析当中我们可以知道主要关心put和removeNext()这两个方法就可以了,put()方法其实就是把bitmap对象存进我们的queue队列中
2)remove方法其实就是一出队列的第一个bitmap对象,将先进先出,符合我们的FIFO原则
@Overridepublic Bitmap get(String key) { Bitmap result = null; Reference<Bitmap> reference = softMap.get(key);if (reference != null) { result = reference.get(); }return result; }
LargestLimitedMemoryCache源码分析
public class LargestLimitedMemoryCache extends LimitedMemoryCache { /** * Contains strong references to stored objects (keys) and sizes of the objects. If hard cache * size will exceed limit then object with the largest size is deleted (but it continue exist at * {@link #softMap} and can be collected by GC at any time) */ private final Map<Bitmap, Integer> valueSizes = Collections.synchronizedMap(new HashMap<Bitmap, Integer>()); public LargestLimitedMemoryCache(int sizeLimit) { super(sizeLimit); } @Override public boolean put(String key, Bitmap value) { if (super.put(key, value)) { valueSizes.put(value, getSize(value)); return true; } else { return false; } } @Override public Bitmap remove(String key) { Bitmap value = super.get(key); if (value != null) { valueSizes.remove(value); } return super.remove(key); } //这里我们省略若干个方法,有兴趣的话讲源码去,下面有提供源码下载地址 @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; } }
同样我们只关心put方法和removeNext()方法
@Overridepublic boolean put(String key, Bitmap value) { if (super.put(key, value)) { valueSizes.put(value, getSize(value)); return true; }else { return false; } }@Overrideprotected 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; }
1)其实就是put方法的时候( valueSizes.put(value, getSize(value));),我们将bitmap做为key,大小作为value,存进valueSizesM集合
2)在超过最大缓存数量的时候,遍历移除掉valueSizes中最大的bitmap。
下面我们来看一下LruMemoryCache是怎样实现的
源码我们就不贴出来了
主要逻辑在put方法中
// 存储bitmap对象,在构造方法里面初始化private final LinkedHashMap<String, Bitmap> map;/** Caches {@code Bitmap} for {@code key}. The Bitmap is moved to the head of the queue. */@Overridepublic 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; }
当我们把bitmap存进内存的时候,他会trimToSize(maxSize)这个方法去判断我们是否超过我们规定内存的最大值,超过的话移除掉最先添加进来的那个
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); } } }
下面我们来看一下LimitedAgeMemoryCache 是怎样实现的
/** * Decorator for {@link MemoryCache}. Provides special feature for cache: if some cached object age exceeds defined * value then this object will be removed from cache. * * 采用装饰着模式,计算对象的最大存活时间 * 在get方法的时候判断大于的移除掉 * */public class LimitedAgeMemoryCache implements MemoryCache { private final MemoryCache cache; private final long maxAge; private final Map<String, Long> loadingDates = Collections.synchronizedMap(new HashMap<String, Long>()); /** * @param cache Wrapped memory cache * @param maxAge Max object age <b>(in seconds)</b>. If object age will exceed this value then it'll be removed from * cache on next treatment (and therefore be reloaded). */ public LimitedAgeMemoryCache(MemoryCache cache, long maxAge) { this.cache = cache; this.maxAge = maxAge * 1000; // to milliseconds } @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); } @Override public Bitmap remove(String key) { loadingDates.remove(key); return cache.remove(key); } @Override public Collection<String> keys() { return cache.keys(); } @Override public void clear() { cache.clear(); loadingDates.clear(); } }
分析
1)这个采用了装饰者模式,包装我们的Memory对象,不了解装饰者模式的,建议先读我的这一篇博客装饰者模式及其应用:http://blog.csdn.net/gdutxiaoxu/article/details/51885105
2)主要逻辑在get方法里面,在我们通过key取bitmap的时候,他会先判断存活时间是否超出我们规定的maxAge(System.currentTimeMillis() - loadingDate > maxAge),超过的话移除掉
3)那我们是怎样保存这些的存活时间的呢,其实很简单?就是用一个loadingDates集合来保存,在我们put的时候,把当前的时间存进去,源码体现如下
//成员变量,保持存活时间的map集合private final Map<String, Long> loadingDates = Collections.synchronizedMap( new HashMap<String, Long>());@Overridepublic boolean put(String key, Bitmap value) { boolean putSuccesfully = cache.put(key, value); if (putSuccesfully) { loadingDates.put(key, System.currentTimeMillis()); } return putSuccesfully; }
感觉ImageLoader在实现FIFOLimitedMemoryCache算法的时候还是有一点缺陷,为什么呢?
1)我们看了FIFOLimitedMemoryCache,LimitedMemoryCache,里面的方法发现并没没有重写get方法,只有BaseMemoryCache类有实现get方法,那这样就会导致其实我们去缓存bitmap的时候,都会从softMap里面去取,并没有从我们的队列里面去取,我们知道,当内存紧张的时候,会优先回收弱引用引用的对象,有可能发生这样的情况,弱引用用已经被回收了,但是我们的queue里面的强引用还没有回收。再者内存中同时保存着弱引用和强引用,相对来说也是比较占内存的(有错误的话欢迎指出)
2)个人感觉没有必要使用双引用了,弱应用和强引用,使用其中的一种就可以了,当然ImageLoader的LruCache实现就内存当中bitmap的缓存只保存着一份引用。
到此ImageLoader内存的缓存算法源码分析为止,下面我稍微改一下实现方式,内存里面不再保存着两份引用了,bitmap的缓存只保存着一份引用。
自己实现的内存缓存算法
类UML图
其实跟ImageLoader实现的也没有多大区别,只是我去除了弱引用,每个实现类里面不像LimitedMemoryCache的实现类一样持有两份引用而已
首先我们来看一下LimitedMemoryCache是怎样实现的
public abstract class LimitedMemoryCache implements MemoryCache { private static final int MAX_NORMAL_CACHE_SIZE_IN_MB = 16; private static final int MAX_NORMAL_CACHE_SIZE = MAX_NORMAL_CACHE_SIZE_IN_MB * 1024 * 1024; private final int sizeLimit; public static final String TAG="tag"; private final AtomicInteger cacheSize; private final Map<String, Bitmap> mMap= Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>()); public LimitedMemoryCache(int sizeLimit) { this.sizeLimit = sizeLimit; cacheSize = new AtomicInteger(); if (sizeLimit > MAX_NORMAL_CACHE_SIZE) { Log.w(TAG,"You set too large memory cache size (more than %1$d Mb)"+ MAX_NORMAL_CACHE_SIZE_IN_MB); } } @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) { String removeKey = removeNext(); if(removeKey==null){ break; } Bitmap bitmap = mMap.remove(key); if(bitmap!=null){ curCacheSize = cacheSize.addAndGet(-getSize(bitmap)); } } mMap.put(key,value); cacheSize.addAndGet(valueSize); putSuccessfully = true; } return putSuccessfully; } @Override public Bitmap remove(String key) { return mMap.remove(key); } @Override public Bitmap get(String key) { return mMap.get(key); } @Override public void clear() { mMap.clear(); cacheSize.set(0); } protected int getSizeLimit() { return sizeLimit; } @Override public Collection<String> keys() { synchronized (mMap) { return new HashSet<String>(mMap.keySet()); } } protected abstract int getSize(Bitmap value); protected abstract String removeNext(); }
LimitedMemoryCache所做的工作可以分为以下几步
1) 保存着一份强引用
//在构造方法里面初始化 private final Map<String, Bitmap> mMap;
2) 在我们调用put方法的时候,即我们把bitmap存进内存的时候,他会判断是否超出我们的最大值,超出我们的最大值就会调用removeNext();来获得我们将要移除的bitmap对象,最终再调用hardCache.remove(removedValue)去移除它。
3) 注意到removeNext()方法是抽象方法,交给子类自己去实现自己的算法逻辑,返回类型是String。
下面我们来看一下
public class UsingFreqLimitedMemoryCache extends LimitedMemoryCache { private final Map<String, Integer> usingCounts = Collections.synchronizedMap(new HashMap<String, Integer>());//这里省略了若干个方法 @Override public boolean put(String key, Bitmap value) { if (super.put(key, value)) { usingCounts.put(key, 0); return true; } else { return false; } } @Override public Bitmap get(String key) { Bitmap value = super.get(key); // Increment usage count for value if value is contained in hardCahe if (value != null) { Integer usageCount = usingCounts.get(value); if (usageCount != null) { usingCounts.put(key, usageCount + 1); } } return value; } @Override public Bitmap remove(String key) { usingCounts.remove(key); return super.remove(key); } @Override protected String removeNext() { Integer minUsageCount = null; String leastUsedValue = null; Set<Entry<String, Integer>> entries = usingCounts.entrySet(); synchronized (usingCounts) { for (Entry<String, 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; } }
与ImageLoader不同的是,我们是用这个记录
private final Map<String, Integer> usingCounts = Collections.synchronizedMap(new HashMap<String, Integer>());
而ImageLoader是用这种类型的记录,其他的基本大同小异,有兴趣的可以去这里下载源码。
private final Map<Bitmap, Integer> usingCounts = Collections.synchronizedMap(new HashMap<Bitmap, Integer>());
题外话:通过这篇博客,学习了更多的缓存算法,同时你们有没有发现,很多地方都用到了Collection框架,而要用好这些,个人觉得去了解他们的原理是非常必要的,尤其是map和List集合,不管说是初学者还是大牛,毕竟万丈高楼也是从平地盖起的,基础非常重要
共同学习,写下你的评论
评论加载中...
作者其他优质文章