Lrucache 源码解析

LRU原理

使用方法

源码分析

总结


 

  • LRU原理

        LRU全称为Least Recently Used,即最近最少使用,是一种缓存置换算法。大家都知道在各个图片加载的第三方框架中都有他的身影。在各个加载工具流行之前,这个算法还是比较常用的。当然现在不用我们手写了。每个框架都封装的很好。我们前面分析glide的框架,他的缓存机制也是由lru缓存和软引用组成的。在这里先介绍一下lrucache, 既然是一种缓存策略,那么肯定不会无限制的增加,当内存大到某个阈值的时候,会舍弃一部分保存在内存里面的数据,舍弃规则就是最近最少未使用的被舍弃。比如:A,C,D,E,D,A,C,D.如果这个时候内存已满需要舍弃一个。这个时候舍弃的是E。因为它最长时间未被使用。

  • 使用方法

public class LruCacheUtils{

    //首先计算分给缓存的内存大小,一般是app可用内存的1/8;
    long maxSize = Runtime.getRuntime().maxMemory()/8;

    //初始化LruCache
    LruCache<String,Bitmap> lruCache = new LruCache<String,Bitmap>((int)maxSize){

        //重写sizeOf方法,返回存储一个对象需要占用的内存 本例存储的是bitmap,所以返回的是bitmap大 
        //小。这里有一个细节需要注意,sizeof返回的单位必须和maxsize的单位保持一致。否则会造成错误
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
        }
    };

}

      以上是简单的Lrucache的使用,只是重写了一个必须的方法sizeOf,接下来慢慢扩充。在我们使用的过程中,我们其实可以大致算出可以缓存多少图片,一般app的1/8是4m。然后我们可以根据图片的大小然后猜测可以缓存的图片张数,现在很多app都是高清图片,这个时候也许只能缓存几张,这个时候我们需要想办法压缩图片来增加缓存的数量。否则图片过大很容易oom。

  • 源码分析

 首先我们看看Lrucache的结构和成员变量。

public class LruCache<K, V> {

    //存储数据的LinkedHashMap, 其实lrucache就是对它的一个具体应用
    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;  //有效的put方法的使用次数。key,value任意为空均无效
    private int createCount; //create方法调用次数
    private int evictionCount;//被置换出的元素个数
    private int hitCount;
    private int missCount;

    /**
     * @param maxSize for caches that do not override {@link #sizeOf}, this is
     *     the maximum number of entries in the cache. For all other caches,
     *     this is the maximum sum of the sizes of the entries in this cache.
     */
    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);
    }

    其实Lrucache就是对LinkedHashMap的一个具体使用,都是对源码中map的操作, 在构造函数中初始化map,其中两个变量需要注意,一个是0.75f, 一个是最后的boolean变量, 我们可以通过看LinkedHashmap的源码来理解。

 public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

   这个0.75是一个负载因子,与LinkedHaspMap的扩容有关系,0.75表示当LinkedHashMap中数组的长度大于最大长度的0.75倍的时候就需要扩容,这个负载因子过大过小都不合适,下一篇博客会专门讲LinkedHashMap的源码,再次不细说了。知道概念即可。最后的一个布尔变量当它为false时,只按插入的顺序排序,即新放入的顺序会在链表的尾部;而当它为true时,更新或访问某个节点的数据时,这个对应的结点也会被放到尾部。这个也是实现lru算法的根本。

  接下来我们看看Lrucache如何取数据:

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

        V mapValue;

        //首先加锁是为了实现线程安全
        synchronized (this) {
            //这里只是一句但是包含2个内容,首先是获取key对应的值,其次是hashmap会重新
            //整理存储的数据的顺序,可在hashmap中查看这个内容。

            mapValue = map.get(key);
            if (mapValue != null) {
                //如果mapvalue不为空,那么表示以前有过存储直接返回。
                hitCount++;
                return mapValue;
            }
            missCount++;
        }

        //如果根据key没有获取到数据,我们可以自定义生产一个对象放回。如果不重写create函数
        //那么就create直接返回null,这个时候整个函数也直接返回null。如果我们重写create,
        //这个生成的过程也许比较长,在这个期间如果对应的key有了存储数据,那么后面就会产生冲突
        //当发生冲突的时候,会舍弃我们自己生成的这个value,保存原来的值。因为是多线程的所以会  
        //发生这样的事情。

        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        /* 如果不复写create函数,以下不会执行*/

        synchronized (this) {
            createCount++;
            //将生成的值保存到map
            mapValue = map.put(key, createdValue);

            if (mapValue != null) {
                 //如果返回的值不为空,那么说明key已经保存了值,也就是和我们自己生成的值
                 //产生了冲突,这个时候需要保存原来的值,舍弃我们生成的。
            
                map.put(key, mapValue);
            } else {
                //保存成功之后,需要计算保存的元素的内存大小,然后加到size上面,获取当前
                //使用内存的情况。
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
         /*
         * 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
         * 告诉 自定义 的 entryRemoved 方法
         */
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            //这里因为之前的size+=,所以需要重新整理长度。
            trimToSize(maxSize);
            return createdValue;
        }
    }

       只是看lrucache的get方法,我们是没法看出最近使用这个概念的。需要分析LinkedHashMap中的get方法,

 public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

因为我们设置的accessOrder是true,所以我们需要看afterNodeAccess函数
void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMapEntry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }
这个函数将更新过的数据移动到了双向链表的末尾。

    接下来我们看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) {
            //当这个key之前保存过数据,那么需要通知更新保存的数据。
            //在这里一般是置空,我们可以通过复写这个函数来实现我们自己的二级缓存。
            //不过这个二级缓存不能使用内存的缓存了。可以写到sd卡上面。
            entryRemoved(false, key, previous, value);
        }

        //需要重新整理内存情况
        trimToSize(maxSize);
        return previous;
    }

   这里的整个逻辑分以下几步:

   1 先把数据保存到map里面,所以不管这个时候是不是超过了分配的最大内存。

   2 计算存储之后的内存只用情况,该加的就加,需要再减的也要把废弃的data减去。

   3 如果此前存储的对象不为空,那么通知处理需要移除的数据。

   4 计算之后的整体内存情况。

接下来我们看trimToSize函数。

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

  这里通过一个死循环来确保缓存使用的内存大小,小于初始化的最大值,当size大于maxsize的时候,就不停的移除map中的数据在计算知道size小于maxsize。或者map为空。

  所以最后我们可以稍微扩充一下Lrucache

 //初始化LruCache
    LruCache<String,Bitmap> lruCache = new LruCache<String,Bitmap>((int)maxSize){

        //重写sizeOf方法,返回存储一个对象需要占用的内存 本例存储的是bitmap,所以返回的是bitmap大小。
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
        }

        //处理被置换出的元素
        @Override
        protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
            //此方法下可以做一个二级缓存的策略。把置换出的数据保存到本地。然后下次加载的时候,可以现在本地查找。之后在去缓存中查找。也可以在这里释放内存,释放掉被置换出的数据。
        }
    };
  • 总结

   1 Lrucache根本上是通过Linkedhashmap来实现了他的思想。当添加数据导致内存溢出的时候,map会把链表头部数据舍弃,就是舍弃          了最近最久未使用的元素。

   2 Lrucache是线程安全的。

   3 其实可以看出Lrucache中并没有释放内存,只是将LinkedHashMap中的数据移除。 可以通过在entryRemoved中释放。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值