LruCache原理分析

2,945 阅读5分钟
原文链接: thinkdevos.net

LruCache这个类在我们现在应用的开发中已经被普遍使用了,今天我们就深度解析这个类,从原理上掌握作者的设计思想以及实现原理

什么是LruCache

借官方的描述,LruCache就是一个持有一定数量强引用数据的缓存。当访问一个数据时,这个数据就会被移动到数据队列的头部(经常用到的数据),当数据添加到缓存满时,队列尾部的数据(也就是不常用到的数据)会被删除并被回收,这就是LruCache的定义,同时这其中也包含了它的实现思想,采用了LRU算法用来管理数据。

LruCache能做什么

在我们平时的开发中,这个用的最多的就是缓存内存图片了,这里我就不在啰嗦,具体实例官方文档已经很详细了,我在这里要说的是,这个不一定用来缓存Bitmap,程序设计中凡是涉及LRU思想的逻辑的实现,都可以使用LruCache。

LruCache原理解析

下面就让我们从源码的角度去解析LruCache的实现

LruCache的成员变量


                                                            
private final LinkedHashMap<K, V> map; //LruCache中的数据存储结构是LinkedHashMap
/** Size of this cache in units. Not necessarily the number of elements. */
private int size; //参考注释,这个只是个单位数,并不是指Map中数据的个数,比如我们缓存Bitmap时,可以定义成Bitmap的大小,单位可以是KB,MB等;但是我们也可以按数量来缓存,比如一个Bitmap,就是1等
private int maxSize; //缓存的最大单位数,这个表示数据的单位要和size统一
private int putCount; //添加数据的次数
private int createCount; //创建数据的次数
private int evictionCount; //删除数据的次数
private int hitCount; //从缓存中找到数据的次数
private int missCount; //从缓存中没有找到数据的次数

                                                        

构造方法

如下,LruCache构造方法中最关键的就是对LinkedHashMap的初始化,第三个参数设置为true;这个参数的作用就是调用map的get方法时,如果找到数据,是否将找到的数据移动到双向链表的尾部


                                                            
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);

                                                        

基本的entryRemoved方法

下面来分析一下entryRemoved方法,这个方法是个空方法,我们先看看方法的参数:

  • evicted 这个参数表示是否因为空间不足发生移除不常使用的数据;true表示是因为空间不足移除数据,false则相反
  • key 移除数据的key值
  • oldValue 正真从链表中移除的数据
  • newValue key键对应的值被替换的新数据

这个方法是个空方法,我们很少用到它,它的作用是什么呢?
这个方法的主要作用是帮助我们对移除的数据进行一些回收操作的,比如Bitmap,我们知道在Android2.3.3之前,Bitmap的像素数据是放在native的堆中的,gc是不能对其进行回收的,这时候我们需要主动调用Bitmap的recycle方法来释放位图的像素内存(Android3.0之后位图的像素数据被关联在jvm的堆内存中,这时就不需要调用recycle方法了),那么entryRemoved在这个时候就会用到,如果LruCache存放的一个位图因为内存不足时被remove,这时要想正真回收位图内存,我们还需主动回收


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

                                                        

基本的sizeOf方法

这个方法就是根据单位来计算key对应value数据大小,比如存放的是Bitmap,sizeOf我们可以定义返回KB,MB等,不过这个只要保持和maxSize单位统一就可以了


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

                                                        

trimToSize方法

这个方法是用来调整cache单位数据大小的,我们就从代码中去详细分析


                                                            
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) { //当cache数据大小小于最大数据限制时退出
                    break;
                }
                //如果cache数据超出限制,则取出cache中最不常用的数据,并将其从cache中移除
                Map.Entry<K, V> toEvict = map.eldest(); 
                if (toEvict == null) {
                    break;
                }
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value); //修改cache的size
                evictionCount++;
            }
            entryRemoved(true, key, value, null); //这个方法解释了
        }
    }

                                                        

常用的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); //修改cache的size
            previous = map.put(key, value); //将key对应的数据put到链表中
            if (previous != null) {
                size -= safeSizeOf(key, previous); //如果发生了相同key数据替换,则对齐cache的size
            }
        }
        if (previous != null) {
            entryRemoved(false, key, previous, value); //这个上边已经详细介绍过了
        }
        trimToSize(maxSize); //调整cache的size
        return previous;
    }

                                                        

常用的get方法


                                                            
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); //如果没有在缓存中找到数据,那么调用调用create方法创建数据
        if (createdValue == null) {
            return null;
        }
        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);
            //如果key对应的数据被创建的数据替换则复位key对应的数据,没有则对齐cache的size
            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;
        }
}

                                                        

create方法

在上述的get方法中我们看到了一个create方法,这个方法作用就是创建key对应的数据,但是使用这个方法的时候一定要慎重,因为这个方法可能是个耗时方法,比如Bitmap的decode

小结

至此,LruCache的关键原理已经讲述完毕,其他的方法比较简单,读者可以自行分析,希望我的文章能帮助到大家