目前Android缓存实现方案大多会用到LruCache和DiskLruCache,这两个类的使用非常简单,但是如果深入底层去问大家几个问题:
1)LruCache的底层实现是怎样的?
2) LruCache是否线程安全?
3) LRU算法具体是一个什么算法?
4) 实际使用的时候为什么必须重写sizeOf方法?
这几个问题,相信会难倒很多人。要回答这些问题,还是必须深入代码层去分析LruCache的实现。
下面我们就带着上面这些问题,去分析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; /** * @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); }
这里可以看到LruCache里面会持有一个LinkedHashMap,loadFactor直接设置为0.75f,同时accessOrder设置为TRUE,也即LRU顺序,LinkedHashMap访问顺序可以按照插入顺序也可以按照LRU顺序来访问。
继续看:
/** * 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; } }
获取数据的get方法,从LinkedHashMap获取数据的时候,有用synchronized做同步控制。根据取出来的mapValue是否为空,来决定是否改变hitCount/missCount的值。
下面的create方法默认返回为NULL,所以如果不重写该方法,下面立即就返回了。看到这个地方,其实可以预想到create可以作为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; }
插入数据的put方法,在插入数据的时候,会使用synchronized做同步控制。插入的时候,size大小的变化,会用到safeSizeOf,这我们放到后面讲。先看看previous,由LinkedHashMap的put方法返回一个值。单独看看LinkedHashMap的put方法的代码:
//LinkedHashMap.java的代码:
@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; }
可以看到如果插入的<K,V>对,如果key的hashCode和之前某个key的hashCode值一样,并且equals比较返回值为true,那么就说明这两个key完全一样,此时accessOrder == true,LinkedHashMap会调用makeTail做一个操作。最后返回之前key对应的value。makeTail我们放到最后来讲。makeTail是实现LRU算法的核心代码。
LinkedHashMap会按照HashMapEntry的hashCode以及equals方法来决定key是否一样。如果hashCode一样,并且equals返回true,LinkedHashMap认为两个Entry是完全一样的,此时只是简单的将最新的value更新进去即可(这里说明有可能Entry的hashCode一样,但是可能会有value不一样的情况,否则也就不会更新value,这应该是HASH算法相关吧)。如果hashCode一样,而equals返回false,那么就继续后续的冲突操作。如果hashCode不一样,那么此时就认为是不同的key。
看看HashMapEntry的hashCode是怎么计算的:
@Override public final int hashCode() { return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); }可以看到和key/value都相关,这里的异或操作,实际上效果和相加是一样的效果。而且从代码来看,key/value均有可能为空,也即LinkedHashMap可以保存Key/value为空的数据对。这点和ConcurrentHashMap是不一样的,ConcurrentHashMap的key/value均不能为空。好了,LinkedHashMap先只看这段代码。我们再返回去看上面LruCache的put方法。有了上面对LinkedHashMap的分析,我们就可以知道为什么这里的size会先增加,然后再减少了。因为两个value的值有可能不一样大,而且大小也有可能是不一样大的。从代码功能上来看,put方法会将<K,V>放入LinkedHashMap,同时调整记录保存对象的size大小,然后使用trimToSize方法来决定是否需要回收<K,V>。
put方法里有两个方法:safeSizeOf和trimToSize,这两个方法对于实现LruCache的功能至关重要。
我们先看看: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; }
一个private方法,里面调用sizeOf方法。再看sizeOf方法:
/** * 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; }可以看到这是一个protected方法,默认返回1,默认表示一个Entry。
再看看上面put方法中这一行代码:
size += safeSizeOf(key, value);结合起来看?是不是有一点点感觉了?实际使用的时候,safeSizeOf应该返回<K,V>这个数据中V这个数据的大小。所以实际使用的时候,我们需要重写sizeOf,占用的来表示实际使用的对象V占用的空间大小。分析到这里,相信对于LruCache中每放入一个数据,这些数据总的空间大小的管理应该很明白了。
再来看看trimToSize这个方法:
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) { break; } Map.Entry<K, V> toEvict = map.eldest(); if (toEvict == null) { break; } key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null); } }
trimToSize用来在占用空间大于maxSize的时候,按照LRU算法回收掉最老的数据。上面代码也很简单,迭代器的返回的entry就是最老的<K,V>了。写到这里,大家可能会问了,怎么就是最老的<K,V>,也即怎么实现的LRU算法?
先说一下什么是LRU算法:
1)新数据加入到链表头部;
2)当数据被访问到(即命中时),将命中的数据移动到链表头部;
3)当缓存满的时候,将链表尾部的数据抛弃掉。
上面我们将LinkedHashMap的makeTail方法留到了最后来讲:我们看看makeTail的代码:
/** * Relinks the given entry to the tail of the list. Under access ordering, * this method is invoked whenever the value of a pre-existing entry is * read by Map.get or modified by Map.put. */ 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++; }首先断开e,然后header的next指向最老的节点,header的prev指向最新的节点,此时e就是最新的节点,会被插入到header.prev和oldTail之间。
这行get方法访问某一个Key(对应一个Entry)的时候,这个节点就会自动被更新到最新的地方。所以最近使用这一点就通过上面的方法实现了。
继续看LinkedHashMap的代码:
@Override void preModify(HashMapEntry<K, V> e) { if (accessOrder) { makeTail((LinkedEntry<K, V>) e); } } @Override void postRemove(HashMapEntry<K, V> e) { LinkedEntry<K, V> le = (LinkedEntry<K, V>) e; le.prv.nxt = le.nxt; le.nxt.prv = le.prv; le.nxt = le.prv = null; // Help the GC (for performance) }
preModify是覆写LinkedHashMap的父类HashMap的preModify方法。HashMap类中put方法在更新key对应的value之前,如果有完全一样的key,就会调用preModify方法。否则认为当前Map中不存在给定的Key,此时会调用addNewEntry来构建一个新的Entry,并放入到LinkedhashMap中。
先看preModify,所做的操作其实就是调用makeTail,也就是LRU算法。分析到这里,我想put/get方法的调用,对Entry的影响应该就很清晰了吧。这也就是LRU的实现。
再看addNewEntry:
@Override void addNewEntry(K key, V value, int hash, int index) { LinkedEntry<K, V> header = this.header; // Remove eldest entry if instructed to do so. LinkedEntry<K, V> eldest = header.nxt; if (eldest != header && removeEldestEntry(eldest)) { remove(eldest.key); } // Create new entry, link it on to list, and put it into table LinkedEntry<K, V> oldTail = header.prv; LinkedEntry<K, V> newTail = new LinkedEntry<K,V>( key, value, hash, table[index], header, oldTail); table[index] = oldTail.nxt = header.prv = newTail; }上面的代码中,removeEldestEntry默认返回false,可以不用理会。
table[index]会存放新的节点newTail,次新的节点oldTail会指向newTail,head.prv也会指向newTail,表示newTail为最新的节点。
同时这里需要注意一个比较精妙的地方:整个LinkedHashMap中,只有判断是否为eldest的地方使用了header->nxt以及init()的时候使用header->nxt指向自己。其余地方均没有出现header->nxt,那么header->next是怎么指向最旧的节点的呢。其实是在addNewEntry这里的代码有做处理,只不过比较隐蔽。
// Create new entry, link it on to list, and put it into table LinkedEntry<K, V> oldTail = header.prv; LinkedEntry<K, V> newTail = new LinkedEntry<K,V>( key, value, hash, table[index], header, oldTail); table[index] = oldTail.nxt = header.prv = newTail;这样分析,初始状态,header链上除了header外,有0个节点。header.nxt以及header.prv其实都指向header。此时oldTail就是header,所以此时oldTail.nxt其实就是header.nxt,此时header.nxt会指向第一个节点newTail(此时只有一个节点,所以最新/最老的节点都是newTail)。
然后继续向header中添加第二个节点节点,此时header.prv就指向newTail(2),newTail(2).nxt指向newTail(1), header.nxt指向newTail(1).
header.prv -> newTail(2).prv -> newTail(1).prv -> header
header.nxt -> newTail(1).nxt -> newTail(1).nxt -> header
然后继续重复这样的过程,就建立起一个双向链表了。
所以通过preModify以及addNewEntry,这样就维护起来其一个通过head的prv/nxt表示的使用新旧程度的链表。
再回头来看看本文开头的问题:代码,
1)LruCache的底层实现是怎样的?(LinkedHashMap实现)
2) LruCache是否线程安全? (线程安全)
3) LRU算法具体是一个什么算法? (看上面代码)
4) 实际使用的时候为什么必须重写sizeOf方法?(用来记录保存的V对象占用的空间大小。)
相信看完上面的代码,这些问题也自动就知道了吧。
最后一个问题:在LruCache释放对象的时候,有什么方法可以监控么?或者说LruCache删除缓存的对象的时候,我们怎么能够知道删除了什么数据?
相信看上面的代码应该很快就能明白了。