LruCache源码分析

1、LruCache概述:

位于android.util包中,是在Android3.1(Honeycomb MR1)中加入的。在之前的版本中,可以通过supprot包使用。

LruCache的本质仍然是一个缓存区,持有对“值”的强引用。其特色在于缓存淘汰算法使用的是LRU算法。

LRU(Least recently used,最近最少使用)算法是根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

当每次命中一个“值”的时候,会将这个“值”移动到链表头部。如果队列已满,会抛弃链表尾部的“值”,使其能被垃圾回收器在合适的时间点回收掉。



2、LruCache的实现:

首先看下它的成员变量:

private int size;   //已用大小
private int maxSize;//容量

private int putCount;   //存放次数
private int createCount; //为key创建value的次数
private int evictionCount;  //抛弃最老的value的次数
private int hitCount;   //命中次数
private int missCount;  //未命中次数



然后是LruCache的构造方法:

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);
}

从最后一行可以看到,其底层使用的LinkedHashmap。map的初始容量为0,加载因子为0.75,最后一个参数true表明它是基于访问排序的,也就是说当get到一个“值”后,这个“值”就会移动到尾部。如下:

首先放入四个值
map.put(1, “a”);
map.put(2, “b”);
map.put(3, “c”);
map.put(4, “d”);

此时链表中的顺序是:a -> b -> c -> d

然后访问前两个值
map.get(1);
map.get(2);

之后链表的顺序就变成了:c -> d -> a -> b

还真有点Lru的感觉。


接下来看它的put方法:

/**
 * 根据键来缓存值,并将值移动到表头
 *
 * @return 返回这个键之前关联的值
 */

public final V put(K key, V value) {

    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }

    V previous; //用来保存key之前关联的值

    synchronized (this) {
        putCount++;
        size += safeSizeOf(key, value);
        //获取key之前对应的value,如果之前没有value,那么便是null
        previous = map.put(key, value);
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }

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

    trimToSize(maxSize);
    return previous;
}

//空函数
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

可以看到首先进行了合法性检查,其不允许null作为键或值(LinkedHashmap是允许键或值为null的)。

接下来同步锁定整个类,并完成以下操作:

  • 存放次数putCount增加;
  • 计算新值的大小,并更新已用大小;
  • 将新值存放进去(如果键关联着旧值,那么同时删掉旧值);
  • 如果旧值存在的话,当然需要更新size(减去旧值的大小)。

至此,存放新值和删除旧值操作已经完成。至于后面的entryRemoved其实是一个空方法,我们重写后便可以来定制自己的逻辑。

最后最后,调用trimToSize,检查有没有超过最大容量,如果超过了,那就扔掉最老的那个。扔掉后再检查再扔,直到小于等于最大容量。



再看一下它的get方法:

/**
 * 如果键对应的值存在或者这个值能被创建出来的话,返回这个值
 * 如果这个值被返回了,那么它将会被移动到链表头部 
 * 如果没找到而且不能被创建的话,返回null
 */
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++;
    }

    /*
     * 尝试为此键去创建一个对应的值 。
     * 创建的过程可能很耗时,即当创建完成时可能map已经改变了。
     * 所以如果在create的过程中向map中放入了与此键对应的值,
     * 我们会放弃创建的值,使用存进来的值。
     */

    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;
    }
}


//默认返回为null的create函数:
protected V create(K key) {
    return null;
}

首先仍是合法性检查。
接着加锁、根据key去获取value。
如果取到了,命中次数加1,返回value,执行完毕;

如果没取到,未命中次数加1,然后去尝试创建一个值。在源码中,create函数是一个空函数,直接返回null。而get函数发现创建的值为null,也直接返回null,执行完毕。

如果我们重写了create(K key)函数,使其不返回null,那么会继续往下执行:
首先是创建次数增加,然后将新的值存放进去。

如果存放的时候发现又有值了(因为创建的过程可能很耗时,在这期间我们又向里面插入了此键与一个值),那么便将这个值放进去,同时抛弃创建的值。最后将这个值返回,执行完毕。

如果创建、存放一切正常,那么修正已用大小,然后调用trimToSize清理下超出容量的缓存,最后返回这个创建的值。



最后看一下trimToSize方法:

/**
 * 移除掉最老的键值对,直到已用大小<=传入的最大容量
 *
 * @param maxSize 函数执行完毕后期望最大容量
 *                  如果是-1的话代表清空整个map
 */
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);
    }
}

函数内部是一个while循环,退出的条件是已用大小小于传入的最大容量,不然的话就扔掉最老的那一个键值对,同时抛弃次数加1,更新已用大小。

如果传入的maxSize是-1的话,意味着会一直抛弃最老的实体,同时调用entryRemoved方法,直到map为空。所以里面有一个清空缓存区的函数:

/**
 * Clear the cache, calling entryRemoved on each removed entry.
 */
public final void evictAll() {
    trimToSize(-1); // -1 will evict 0-sized elements
}




3、LruCache的分析:

(1)在LruCache的源码中,并没有发现关于LRU算法的具体实现。这是因为其实现主要位于LinkedHashMap中,即如果我们创建LinkedHashMap时使用三个参数的构造方法、并且最后一个参数传入true,那么就使用了LRU进行缓存抛弃算法。所以,具体的还是得看看LinkedHashMap源码。我会继续的。

(2)源码中有两个好玩的函数:

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

V create(K key) {}

entryRemoved是在移除元素后调用,第一个参数代表是不是被抛弃,true代表调用时机为删除最老的元素,false代表更新key对应的value是移除了就的value。

create是在get方法未命中后调用的。

重写这两个方法能使根据我们的需要定制自己的逻辑,还是很棒的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值