Android LruCache 源码解析

Lru算法,即最近最少使用算法。是对于内存管理的一种策略,会将最近最少使用的数据缓存移除。

举例说明:
    若是缓存大小为4,按顺序添加数据分别是 1 2 3 4
    此时访问数据 2 ,则访问完成后内部排序就会变成 1 3 4 2
    若是再添加一个数据5,则会删除1,此时内部排序为3 4 2 5
    即超过最大容量时,会将使用最少的时间最久的删除,然后添加新的数据

android 不同的渠道配置不同版本_数据

Android中的Lru算法

  在android中,官方已经为我们实现了LruCache。其内部是通过LinkedHashMap来维护数据的。LinkedHashMap又是以HashMap为基础实现的有序的Map。
  在Android中常用于图片的缓存。由于图片占的内存都比较大,而在android中内存又是非常重要并且紧缺的,因此不可能把所有图片都维持在内存中的。但是图片又通常是从网络加载的比较慢,要是每次使用都重新从网络加载,对于用户的体验不是太友好。
  因此如何对图片进行合理的缓存就显得比较重要了,而Lru则是比较合理的一种缓存方式,将经常使用的缓存下来,而不经常使用则丢弃。

LruCache的使用
//在LruCache的注释中已经给出了示例
    
    int cacheSize = 4 * 1024 * 1024; // 4MiB
    LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
        }

  LruCache构造函数需要传入一个maxSize作为缓存的大小。另外通常还需要重写sizeOf方法,该方法默认返回1。
  缓存大小和缓存单位必须匹配。若是maxSize是以内存大小为单位的,则sizeOf方法必须返回每一个数据的实际大小。如上例,若是每个Bitmap的大小刚好为1MiB,则可以缓存4个Bitmap,若是不重写sizeOf的话,则会存储4 * 1024 * 1024个Bitmap
  后面就是与Map的用法一致了,即通过put添加,通过get获取缓存实例。

接下来将具体分析一下LruCache的实现
LruCache中的变量
//内部维护的LinkedHashMap,通过键值对来存储数据。
    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;
构造方法
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);
    }

 在构造函数中初始化LikedHashMap,这是管理缓存数据的主要成员。其内部通过数组+双向链表+红黑树(java 8)实现。前两个参数分别是哈希桶初始大小,负载因子。(具体请参看HashMap的源码)
 第三个参数控制内部排列顺序:
  false:按照插入顺序排列
  true:按照最近使用次序排列

接下来就是最常用的方法get和put
get方法

 该方法会返回缓存中的数据,并且将该数据移动到队尾(由LinkedHashMap实现)。
 若是无法获得key对应的值,将会试图通过create方法创建一个value。该方法在LinkedHashMap中定义,并且默认是一个空实现,可根据需求重写该方法。若是create创建的值在插入map中时发现该值已经存在了(并发中可能会出现这种情况),则会丢弃创建值并不会进行插入。

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

        // 若是在map中存在,则返回该值
        V mapValue;
        synchronized (this) {
            mapValue = map.get(key);//该方法在LinkedHashMap中会将该数据移动到队尾
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }
        

        //试图通过create方法创建,默认空实现返回null
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        synchronized (this) {
            createCount++;
            //创建成功则将结果存入缓存
            //若是map.put放入的key在缓存中已经存在,则不替换该值,直接返回
            //若是不存在,则正常插入并返回null
            mapValue = map.put(key, createdValue);
            if (mapValue != null) {
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            //从缓存中删除条目的时候调用,默认空实现。
            //这里对应create创建成功但是插入失败的情况
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            //执行到这里则代表通过create创建的value满足条件,并且存入了缓存中
            trimToSize(maxSize);//重新调整大小
            return createdValue;
        }
    }
对于get方法中涉及的一些方法
//该方法在缓存未命中时调用,用来创建相应的key对应的value值
    //如果无法计算出来则返回null,否则返回计算value值
    //若是计算出来值后,对应的key已经在缓存中存在,该计算值将会被丢弃
    //默认空实现返回null,可根据需求重写该方法。
    protected V create(K key) {
        return null;
    }
    
    //当数据从缓存中移除remove()或者替换put()的时候调用。
    //第一个参数true表示自动的移除数据(数据超出缓存大小则会将最少使用的最久的数据删除),
    //false表示被remove()移除或者被put()替换
    //默认空实现,可根据需求进行重写。
    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
    
    //直接调用sizeOf方法
    private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }
    
    //调整缓存大小,该方法在后面的put方法后再进行解释
    private void trimToSize(int maxSize){...}
说完了get方法,剩下的就是put了
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++;
            //增加当前大小,并调用put插入数据
            size += safeSizeOf(key, value);
            previous = map.put(key, value);
            //若是缓存中已经存在该key对应的value,则将当前大小减去被替换的value的大小
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        //替换的回调,上文已经说过
        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }

        //调整大小
        trimToSize(maxSize);
        return previous;
    }
trimToSize

该方法在涉及到数据的添加的时候会被调用,用于计算当前大小是否已经超过了缓存大小

private void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
                //若是当前size小于0,或者缓存为空但是当前大小不为空,则抛出异常
                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 = null;
                for (Map.Entry<K, V> entry : map.entrySet()) {
                    toEvict = entry;
                }
                
                if (toEvict == null) {
                    break;
                }
                
                /******************标记结束*****************/

                key = toEvict.getKey();
                value = toEvict.getValue();
                //移除该元素
                map.remove(key);
                //计算当前大小并开始下一次循环,直至当前size不大于最大size
                size -= safeSizeOf(key, value);
                evictionCount++;
            }
            entryRemoved(true, key, value, null);
        }
    }

  在android.util.LruCache(Android-28)中,实现代码如上,但是在android.support.v4.util.LruCache(28.0.0版本)中则有些不一样。
  区别主要是在trimToSize方法中移除最久不使用的数据这里,上述代码已经标记出来了。

标记区的代码区别如下:
/*******android.support.v4.util.LruCache(28.0.0)**************/
    Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
/****************end *****************************************/
  
    
/************android.util.LruCache(android-28)****************/
    Map.Entry<K, V> toEvict = null;
    for (Map.Entry<K, V> entry : map.entrySet()) {
        toEvict = entry;
    }
    if (toEvict == null) {
        break;
    }
/*******************end***************************************/
    
    
/*************android.util.LruCache(android-24~27)************/
    Map.Entry<K, V> toEvict = map.eldest();
    if (toEvict == null) {
        break;
    }
/***********************end***********************************/

  对于LinkedHashMap,内部是使用的HashMap实现的,不同的是LinkedHashMap对于HashMap的节点增加了两个元素before和after,用于将节点连接成双向链表。因此对于LinkedHashMap而言,可以看做是双链表。在Java8中,每次插入数据都是将数据插入到队尾。(具体可查看LinkedHashMap源码)

  对比可以看到上面的标记区的代码的不同,在v4包中的LruCache通过map.entrySet().iterator().next()得到的是队首的节点,该节点在Java8中的确是最近最不常使用的节点。

  在Android-24~27中的android.util包里的LruCache则是通过map.eldest()获得,该方法在Java8源码中是不存在的,是android添加的一个方法,该方法**public Map.Entry<K, V> eldest() { return head; }**返回的是head,也就是队首,与v4包获取到的节点是一样的。

  在Android-28中的android.util包里的LruCache是通过遍历entrySet获取最后一个节点,也就是队尾节点。但是在Java8中队尾节点却是最近最新使用的数据,对于Lru删除这个节点的做法,让我很是疑惑。

remove方法

将对应Key移除缓存,若是成功则返回移除value,失败返回null

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

        V previous;
        synchronized (this) {
            //移除map中的对应值,成功则返回移除的value
            previous = map.remove(key);
            if (previous != null) {
                //成功则将当前size减去移除的大小
                size -= safeSizeOf(key, previous);
            }
        }

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

        return previous;
    }
其余方法
//重新设置缓存大小
    public void resize(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }

        synchronized (this) {
            this.maxSize = maxSize;
        }
        trimToSize(maxSize);
    }


    //一般需要自己实现,默认返回1
    protected int sizeOf(K key, V value) {
        return 1;
    }

    //移除缓存中所有数据
    public final void evictAll() {
        trimToSize(-1); // -1 will evict 0-sized elements
    }

    //获取当前已使用缓存大小
    public synchronized final int size() {
        return size;
    }

   //获取缓存总共大小
    public synchronized final int maxSize() {
        return maxSize;
    }

    public synchronized final int hitCount() {
        return hitCount;
    }

    public synchronized final int missCount() {
        return missCount;
    }

    public synchronized final int createCount() {
        return createCount;
    }

    public synchronized final int putCount() {
        return putCount;
    }

    public synchronized final int evictionCount() {
        return evictionCount;
    }

    public synchronized final Map<K, V> snapshot() {
        return new LinkedHashMap<K, V>(map);
    }

    @Override public synchronized final String toString() {
        int accesses = hitCount + missCount;
        int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
        return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
                maxSize, hitCount, missCount, hitPercent);
    }