LruCache源码分析

265 阅读4分钟

上一篇文章将Glide的基本源码基本分析完。

但提到图片加载,LruCache也是一个不可忽视的关键点,因而这里再对LruCache进行分析:

本文LruCache来源于android.suppport.v4.util中:

public class LruCache<K, V> {
    //存储缓存
    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;

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

    //重新设置最大缓存
    public void resize(int maxSize) {
        //确保最大缓存大于0
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }

        synchronized (this) {
            this.maxSize = maxSize;
        }
        //对当前的缓存做一些操作以适应新的最大缓存大小
        trimToSize(maxSize);
    }

    //获取缓存
    public final V get(K key) {
        //确保key不为null
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
            mapValue = map.get(key);
            //如果可以获取key对应的value
            if (mapValue != null) {
                //命中数加一
                hitCount++;
                return mapValue;
            }
            //如果根据key获取的value为null,未命中数加一
            missCount++;
        }
        
        //省略无关代码......
    }

    
    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) {
                //如果之前map中对应key存在value不为null,由于重复的key新添加的value会覆盖上一个value,
                //所以当前缓存大小应该再减去之前value的大小
                size -= safeSizeOf(key, previous);
            }
        }
        //根据缓存最大值调整缓存
        trimToSize(maxSize);
        return previous;
    }

    //根据最大缓存大小对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!");
                }
                //当前缓存大小小于最大缓存,或LinkedHashMap为空时跳出循环
                if (size <= maxSize || map.isEmpty()) {
                    break;
                }
                
                //遍历LinkedHashMap,删除顶部的(也就是最先添加的)元素,
                //直到当前缓存大小小于最大缓存,或LinkedHashMap为空
                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

          //省略无关代码......
        }
    }

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

        return previous;
    }

    private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }

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


    public final void evictAll() {
        trimToSize(-1); // -1 will evict 0-sized elements
    }

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


从上述源码可以看出,LruCache内部主要靠一个LinkedHashMap存储缓存,这里使用LinkedHashMap而不使用普通的HashMap正式看中了它的顺序性,即LinkedHashMap元素的存储顺序就是我们存入的顺序,而HashMap则无法保证这一点。


我们都知道Lru算法就是最近最少使用算法,而LruCache是如何保证在缓存大于最大缓存大小时移除的就是最近最少使用的元素呢?

关键在于trimToSize(int maxSize)这个方法内部,在它的内部开启了一个循环遍历LinkedHashMap删除顶部的(也就是最先添加的)元素,知道当前缓存大小小于最大缓存,或LinkedHashMap为空。

    //根据最大缓存大小对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!");
                }
                //当前缓存大小小于最大缓存,或LinkedHashMap为空时跳出循环
                if (size <= maxSize || map.isEmpty()) {
                    break;
                }
                
                //遍历LinkedHashMap,删除顶部的(也就是最先添加的)元素,
                //直到当前缓存大小小于最大缓存,或LinkedHashMap为空
                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

          //省略无关代码......
        }
    }

这里需要注意的是由于LinkedHashMap的特性,它的存储顺序就是存放的顺序,所以位于顶部的元素就是最近最少使用的元素,正是由于这个特点,从而实现了当缓存不足时优先删除最近最少使用的元素。