提到内存优化,就会想到内存缓存,而提到内存缓存就必须得提到android提供的Lru缓存方案,它的核心就是LruCache类,因此,从源码的角度去看看它的工作原理。
在android3.1.x(API 12)之前,我们用到的是android.util包下的LruCache,在此之后,我们可以用android.support.v4.util包下的LruChache,其实这两个包下的LruCache代码一样,只是为了兼容。
通常我们要得到LruCache对象,一般这样获取:
int maxMemory = (int) (Runtime.getRuntime().maxMemory()/1024); int cacheMemorySize = maxMemory/8; LruCache cache = new LruCache(cacheMemorySize);
那我们先从它的构造函数看起:
public LruCache(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; this.map = new LinkedHashMap(0, 0.75f, true); }
这里先判断分配的缓存大小是否大于0,不是则抛出异常;然后就是关键的地方,初始化了一个LinkedHashMap,它就是用来缓存我们需要保存起来的对象。
当我们要把需要缓存从网络下载下来的图片时,我们会这么做:
if(cache.get(url) == null){ cache.put(url,bitmap); }
当LruCache中没有缓存过这个图片时,我们才将图片存入,我们来看看LruCache的put()方法是如何实现的:
public final V put(K key, V value) { if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } V previous; synchronized (this) { putCount++; size += safeSizeOf(key, value); previous = map.put(key, value); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, value); } trimToSize(maxSize); return previous; }
如果url或bitmap为null,则抛出异常。接着进入同步代码块,定义了一个int型计数器putCount,每往其中存入数据,putCount+1;接着计算需要缓存内存大小的总和size.他通过safeSizeOf(K key, V value)方法获取
private int safeSizeOf(K key, V value) { int result = sizeOf(key, value); if (result < 0) { throw new IllegalStateException("Negative size: " + key + "=" + value); } return result; }
这里看到它是直接调用的sizeof()方法的
protected int sizeOf(K key, V value) { return 1; }
可以看到,这个方法默认返回是1,所以如果想要得到确切的缓存大小,应该在开始初始化LruCache时覆写这个方法,将计算得到的图片占用内存大小回调到回调方法中。所以,开始初始化LruCache的时候我们应该改成这样:
LruCache cache = new LruCache(cacheMemorySize){ @Override protected int sizeOf(String key, Bitmap value) { return value.getByteCount()/1024; } }
这样,就把图片所占用的内存大小返回给了size。接着,判断当前bitmap是否已经存入过,存过,则将这个图片的内存占用大小从size中去除,这样就避免了重复计算同一张图片的内存占用大小。这样,同步代码快就走完了,接着往下看:
if (previous != null) { entryRemoved(false, key, previous, value); }
判断当前bitmap是否存入过,是则进入entryRemoved()方法
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
这是个空的方法,当item被明确要求移除腾出空间时,重写这个方法,它会在item被回收时被remove方法调用,而在被替换时,会被put方法调用。我们可以通过判断传入的evicted值来确定当前的操作。总结一下就是:
evicted = true,说明它正在被移除;evicted = false,说明它是被remove或put掉了 newValue是key所对应的新的value,如过value不为null,这时动作是通过put实现的。如果value为null,则这个动作是通过remove实现的。
好了,entryRemove()方法分析完了,接着往下走,来到trimToSize(maxSize)
public void trimToSize(int maxSize) { while (true) { K key; V 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 toEvict = map.entrySet().iterator().next(); key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null); } }
这个方法一进来就是一个while循环,然后进入同步代码块,这里就做了一件事,当存入的对象占用内存大小大于我们预设的缓存大小时,遍历map集合,从头开始移除map里面的对象,每移除一个对象,size就减去这个item所占内存的大小(其实就是这个bitmap所占的内存),同时调用一次entryRemoved()方法。这样,put方法就分析完了;
最后,我们来看看LruCache的get方法:
public final V get(K key) { if (key == null) { throw new NullPointerException("key == null"); } V mapValue; synchronized (this) { mapValue = map.get(key); if (mapValue != null) { hitCount++; return mapValue; } missCount++; }
这个方法做的是很简单,如果key为null,抛出异常。反之,从map中取出value返回。
好了,看到这里,我们基本搞清楚了LruCache的工作原理了,总结如下:
LruCache内部通过维护一个LinkedHashMap来存入或移除对象 LruCache的基本操作是线程安全的 LruCache通过判断存入对象占内存总量是否大于预设的缓存大小,大于,则从头部开始移除存入的对象,直到占用内存不超过预设的缓存内存为止
好了,就这么多了,后续我会写一篇关于LruCache应用的文章,希望大家多多关注。