Android内存缓存——理解LruCache和LinkedHashMap

博主最近在学习Bitmap高效加载和3级缓存(内存缓存、本地缓存和网络缓存)管理。LruCache(least recent used cache)是一种高效且普遍使用的管理策略。因此,便开启了LruCache源码学习之旅。

注意,本文中涉及的LruCache源码为support v4包中的LruCache。

Table of Contents

1  LruCache结构

2  待开发者覆写的方法

2.1  sizeOf

2.2  entryRemoved()回调

2.3  create()方法

3  公共方法——put()

4  公共方法——get()

5  公共方法——remove()

6  LinkedHashMap中LRU思想的实现

6.1  LinkedHashMap的数据结构

6.2  添加数据时,LRU思想的实现

6.3  Iterator.next()返回最老的数据

7  结语

1  LruCache结构

private final LinkedHashMap<K, V> map;

private int size;
private int maxSize;

private int putCount;
private int createCount;
private int evictionCount;
private int hitCount;
private int missCount;

map: 利用LinkedHashMap实现key-value存储,LinkedHashMap是双端链表,可按访问顺序或存储顺序来进行排序;链头是“最老”的对象,链尾是“最年轻”的对象;

size:缓存的真实大小,该大小指的是带有单位的大小,如byte或kb等;

maxSize:缓存真实大小的最大值;

putCount:用于记录put()方法被调用的次数;

createCount:用于记录create()方法被调用的次数;

evictionCount:用于记录缓存中被驱逐项目的数量;

hitCount:调用get(key)方法时,若缓存中存在key对应的value,即命中,该变量记录了缓存命中的次数;

missCount:调用get(key)方法时,若缓存中存在key对应的value,即未命中,该变量记录了缓存未命中的次数;

上述变量均对应相关公开的get()方法。

2  待开发者覆写的方法

2.1  sizeOf

方法源码为:

protected int sizeOf(K key, V value) {
        return 1;
 }

该方法的意义在于:开发者可按单位自己定义缓存中每个对象的大小,若缓存中存放的是Bitmap,则可写为:

protected int sizeOf(String key, Bitmap value) {
        return value.getByteCount() / 1024;
}

那么size对应的单位为kb。

2.2  entryRemoved()回调

在LruCache中,该方法是个空方法,可由开发者自定义实现。

protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

当调用了remove()、get()、put()或内部调用了trimToSize()的时候,会在不同情况下删除某个缓存,此时会回调entryRemoved;

根据各个参数,可以判断回调的具体时机;

若evicted为true,则回调发生在trimToSize()里,表示要删除一些缓存来控制缓存池的大小;这种情况下的删除就叫eviction,表示驱逐、赶出。

若evicted为false,则表示发生在remove、get或put内部。

若newValue不是null,那么回调肯定发生在put()当中;否则发生在trimToSize或remove中。

2.3  create()方法

该方法的默认实现为:

protected V create(K key) {
        return null;
}

若不覆写,则表示缓存池针对key不会创建任何对象;

该方法只会get()中调用。

当用某个key来get(V key)一个值,但get不到对象时,会调用该方法,根据开发者的意图缓存池是否需要自己来创建这个key对应的value。

调用该方法时,是线程不安全的;针对多线程,缓存池会选择舍弃刚创建的对象;

什么叫舍弃?看一下这种情况:在多线程的环境下,线程A在缓存池中get(key)得不到值,于是调用create(key)来创建这个值;若同时,线程B向缓存池put了该key对应的值;此时缓存池会舍弃掉线程A正在创建的对象,采纳线程B已经put进入的value。

3  公共方法——put()

看一下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;
}

看一下put()方法的逻辑:

首先进入synchronized语块,线程安全;

putCount++,记录调用put()的次数;

size增加,记录缓存的实际大小;

将缓存的value放进map里,并获得可能的旧值,若存在旧值,那么记录缓存实际大小的size将调整。

若存在旧值,将调用entryRemoved()方法,该方法由开发者实现,作为替换了缓存值后的回调,可以看到,entryRemoved()方法已经不是线程安全的了。

调用trimToSize,比较size和maxSize,看是否需要通过删除一些缓存来保持缓存池的大小。

返回替换的旧值。

其中trimToSize()方法源码如下:

public void trimToSize(int maxSize) {
    while (true) {
        K key;
        V value;
        synchronized (this) {
            //......
            //......
            if (size <= maxSize || map.isEmpty()) break;
            
            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
            key = toEvict.getKey();
            value = toEvict.getValue();
            map.remove(key);
            size -= safeSizeOf(key, value);
            evictionCount++;
        }
        entryRemoved(true, key, value, null);
    }
}

进入无限循环,且进入synchronized代码块进行线程保护;

当size小于等于maxSize或map为空时,将直接退出循环;

通过LinkedHashMap的迭代器获取下一个缓存,next在队列中是缓存时间最长的、最少用到的项目,获得后将它删除;这里也体现了least recent used的思想。

相应地减少缓存的真实大小,并让evictionCount数量+1,记录被驱逐的条目的数量;

离开synchronized代码块;

调用entryRemoved()方法,注意参数的设置代表了不同的意义,该方法由开发者实现,完成删除一个条目后的回调;可以看见entryRemoved()调用都是非线程安全的。

继续循环,直到缓存大小满足要求。

4  公共方法——get()

get()方法的源码如下:

public final V get(K key) {
    //......
    V mapValue;
    synchronized (this) {
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }
    synchronized (this) {
        createCount++;
        mapValue = map.put(key, createdValue);
        if (mapValue != null) {
            map.put(key, mapValue);
        } else {
            size += safeSizeOf(key, createdValue);
        }
    }
    if (mapValue != null) {
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {
        trimToSize(maxSize);
        return createdValue;
    }
}

首先,进入synchronized语块,线程安全;

从map中获取值,若该值存在,hitCount +1,并返回取到的值。

若此时map中不存在该值,missCount +1。

离开synchronized语块,线程不安全

若未命中,调用create()方法,由缓存池创建该key对应的value;若缓存池不创建该value,则直接返回null

若缓存池创建了该value,则再次进入synchronized语块,线程安全;

此时createCount + 1;

map put()创建的新值;

若在create()过程中,其他线程针对该key,put()了某个value至缓存池,此时put()将返回一个非null值。那么此时,将会把该值重新put()进map,保留这个值,并调用entryRemoved()舍弃刚才create()创建的的新值。

若在create()过程中,其他线程没有put()入value,则根据新创建的值,调整缓存池的大小。

注意,使用LinkedHashMap的get()方法时,返回的值将被重新插入到队列的最前端。这里也体现了least recent used的思想。

5  公共方法——remove()

源码如下:

    public final V remove(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V previous;
        synchronized (this) {
            previous = map.remove(key);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, null);
        }

        return previous;
    }

从map中删除指定key的value;

若成功删除,要调整缓存池的大小,以及调用entryRemoved回调。

6  LinkedHashMap中LRU思想的实现

在3中,LinkedHashMap.put()方法中,通过LinkedEntryIterator的next()中,直接返回了最老的对象,并将它删除,实现了缓存的更新。

6.1  LinkedHashMap的数据结构

//The head (eldest) of the doubly linked list.
transient LinkedHashMapEntry<K,V> head;
// The tail (youngest) of the doubly linked list.
transient LinkedHashMapEntry<K,V> tail;

static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
   LinkedHashMapEntry<K,V> before, after;
   LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
       super(hash, key, value, next);
   }
}

从注释就可看出,head指向最老的对象,tail指向最新的对象;

LinkedHashMapEntry就是HashMap中的Node。

HashMap使用Node[] table通过数组+链表的形式存储数据。Node中含有hash code、key和value。通过hash code可以找到Node链首在table[]中的位置。关于HashMap这里就不做多的讨论。

6.2  添加数据时,LRU思想的实现

LinkedHashMap.put()时直接使用HashMap的put() -> putVal()

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
   Node<K,V>[] tab; 
   Node<K,V> p; 
   int n, i;
   //table为null时初始化table
   if ((tab = table) == null || (n = tab.length) == 0)
       n = (tab = resize()).length;
   //hash在table中没有数据,直接加入node
   if ((p = tab[i = (n - 1) & hash]) == null)
       tab[i] = newNode(hash, key, value, null);
   //......
   //之后的流程不贴代码,直接以注释说明
   //hash码在table中有数据时,需要比较hash码和key
   //若相等,则替换已有值
   //若不等,则在该table[index]下的Node链中,队尾加入新的Node
   afterNodeInsertion(evict);
   return null;
}

其中LinkedHashMap自己实现了newNode()和afterNodeInsertion。

newNode()源码如下:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMapEntry<K,V> p = new LinkedHashMapEntry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
    LinkedHashMapEntry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

当LinkedHashMap里还没有数据时,head和tail都为null,此时新加入的数据将赋给tail和head,二者此时指向同一个对象;

当LinkedHashMap里有数据后,旧的tail让after指向新数据,新数据成为tail。

所以,这个过程就遵循 了LRU的思想,最老的数据在head,最新的数据在tail。

afterNodeInsertion()是在添加了数据之后调用,源码如下:

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMapEntry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

removeEldestEntry()将通过size来判断是否需要移除最老的数据;而最老的数据就在head。

综上,完成了LinkedHashMap关于LRU的缓存更新。

6.3  Iterator.next()返回最老的数据

数组结构如下:

abstract class LinkedHashIterator {
    LinkedHashMapEntry<K,V> next;
    LinkedHashMapEntry<K,V> current;
    int expectedModCount;
    
    LinkedHashIterator() {
        next = head;
        expectedModCount = modCount;
        current = null;
    }
    //.......
}

nextNode()方法:

final LinkedHashMapEntry<K,V> nextNode() {
    LinkedHashMapEntry<K,V> e = next;
    //......
    current = e;
    next = e.after;
    return e;
}

可以看到,初始化时,直接把head赋值给next;

nextNode中直接返回的是当前的next。

7  结语

最后,做一下小结:

要使用LruCache,需开发者结合自己的需求分别实现sizeOf、create()和entryRemoved()。其中sizeOf最好实现一下,以区分size和count的意义,其他两个方法可以选择实现。

create()和entryRemoved()在LruCache中调用是非线程安全的,这一点需要开发者注意。

get()、put()和remove()的核心都是线程安全的;

LruCache实际是以LinkedHashMap为依托,进行存取和放置。由于LinkedHashMap派生自HashMap,故它可通过数组+链表的方式存储数据。

而LinkedHashMap中又维持了head和tail组成的双端链表,使其实现了LRU的思想。

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值