源码分析1:Android的LruCache类分析

目前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删除缓存的对象的时候,我们怎么能够知道删除了什么数据?

相信看上面的代码应该很快就能明白了。


























评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值