Android开发中,为了减少用户的流量使用和使APP体验更流畅,我们通常会使用缓存技术。通常来说,缓存分两级。第一级,是内存缓存,它的好处是,读写非常快,缺点则是,过量地使用会使APP整体变得十分卡顿,因为运行的内存不足了,甚至引起OOM。第二级则是文件缓存(File,SQLite等),文件缓存的读写效率要低于内存缓存。但是空间更加的充足。
一级缓存由于空间很有限,我们通常会为它设置一个size,当超过这个size时,缓存会将不常用的内容清掉。
Android中提供了一个方便的容器,用来处理这个缓存问题——LruCache。在此阅读它的源码并做一下笔记。
public class LruCache<K, V> { private final LinkedHashMap<K, V> map; /** Size of this cache in units. Not necessarily the number of elements. */ private int size; private int maxSize; private int putCount; private int createCount; private int evictionCount; private int hitCount; private int missCount;
从这里可以很容易地发现,LruCache本质上就是一个LinkedHashMap,我们对它进行控制,达到上述的效果。先看一下这个类的几个属性。
size是当前缓存的大小
maxSize是缓存的最大大小
后面的属性具体作用在阅读过程中再来理解。
接下来,我们再看一下LruCache的构建函数。
/** * @param maxSize for caches that do not override {@link #sizeOf}, this is * the maximum number of entries in the cache. For all other caches, * this is the maximum sum of the sizes of the entries in this cache. */ public LruCache(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; this.map = new LinkedHashMap<K, V>(0, 0.75f, true); }
这一段代码很简单,就是设置缓存的最大size并新建一个LinkedHashMap的对象。并对输入合法性作了检测,如果其值不大于0,则抛出异常。
接下来是重设maxSize的方法。
/** * Sets the size of the cache. * @param maxSize The new maximum size. * * @hide */ public void resize(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } synchronized (this) { this.maxSize = maxSize; } trimToSize(maxSize); }
除了重设maxSize外,这个方法在最后还对已有的缓存进行了修整。因为当我们将最大缓存修改的比当前缓存还小时,就会有一部分已有的缓存需要清理。所以,我们接下来看一下,清理的方法
/** * @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) { 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) { break; } // BEGIN LAYOUTLIB CHANGE // get the last item in the linked list. // This is not efficient, the goal here is to minimize the changes // compared to the platform version. Map.Entry<K, V> toEvict = null; for (Map.Entry<K, V> entry : map.entrySet()) { toEvict = entry; } // END LAYOUTLIB CHANGE if (toEvict == null) { break; } key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null); } }
整个方法处在一个死循环中,前面先对输入合法性进行了检查,并且只有在 if (size > maxSize)的情况下,才会进行清理、修整。循环中,我们会通过一个for循环找到LinkedHashMap中的最后一项。上面代码中,找到最后一项的代码并不是最优的,它的编写者是为了保持它与线上版本一至。找到最后一项后,获取它的key和value。从LinkedHashMap中移除它,并计算它能够释放的内存大小,再重新检查,现在是内存状况是否满足maxSize的限定,并调用了一个空方法entryRemoved(我们可以通过继承重写这个方法,进行一些扩展)。如果不满足,继续清理。其中safeSizeof是计算缓存中一项的大小的方法,我们再来看看它:
private int safeSizeOf(K key, V value) { int result = sizeOf(key, value); if (result < 0) { throw new IllegalStateException("Negative size: " + key + "=" + value); } return result; } /** * Returns the size of the entry for {@code key} and {@code value} in * user-defined units. The default implementation returns 1 so that size * is the number of entries and max size is the maximum number of entries. * * <p>An entry's size must not change while it is in the cache. */ protected int sizeOf(K key, V value) { return 1; }
由上面的代码可以看到,不论我们存什么,LruCache都认为一项存的大小为1。这样,我们在设置大小时,实际设置的是我们要存多少项数据。如果要我们需要它能够真正地反映我们存的内容的大小,我们需要继承并重写sizeOf这个方法。
至此,就看完了LruCache的一次初始化。接下来,我们来看看它是如何保存数据。
/** * Caches {@code value} for {@code key}. The value is moved to the head of * the queue. * * @return the previous value mapped by {@code key}. */ 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; }
照常先是输入合法性检查,然后会将新的储存值的size加入到整个LruCache的size中。将新的值放入LinkedHashMap中,并取出老的值。然后将老的值占的size从LruCache整个size中减去。接着,调用空方法entryRemoved,最后,修整缓存的size。并将被取代掉的那个值,返回。
可以看到,LruCache的put的方法几乎就是LinkedHashMap的扩展。多了输入合法性检查、调整size,修整缓存三部分。这里的重点是,由于LinkedHashMap是一个有序的Map结构,因此,不论是insert还是update,最近操作的数据,都会放在最前面。
然后,我们再来看看LruCache的get部分:
/** * Returns the value for {@code key} if it exists in the cache or can be * created by {@code #create}. If a value was returned, it is moved to the * head of the queue. This returns null if a value is not cached and cannot * be created. */ 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++; } /* * Attempt to create a value. This may take a long time, and the map * may be different when create() returns. If a conflicting value was * added to the map while create() was working, we leave that value in * the map and release the created value. */ V createdValue = create(key); if (createdValue == null) { return null; } synchronized (this) { createCount++; mapValue = map.put(key, createdValue); if (mapValue != null) { // There was a conflict so undo that last put map.put(key, mapValue); } else { size += safeSizeOf(key, createdValue); } } if (mapValue != null) { entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { trimToSize(maxSize); return createdValue; } }
这个方法比较长,可以分为两部分。第一部分是比较常规的从LinkedHashMap中取值,第二部分则是,如果map中不存在这个值,则新建一个这样的值。但是新建的方法,原类中是一个空方法,需要我们自行继承、重写。
综上,我们已经分析了LruCache这个类的大部分方法。总结下来它的工作模式就是,将新建或者更新的数据放在最前面,每次操作后,检查size大小,如果size超过了maxSize,则将最后面的值进行清理,使size回归正常范围。如果我们继承这个类,我们可以比较方便的扩展如下内容:
1、每个数据占用的内存大小的计算
2、清理缓存后的我们调用的方法
3、如果要获取的值,不存在于LruCache中,我们新建它调用的方法。
Done~