当前位置: 首页 > 工具软件 > LRUCache > 使用案例 >

LruCache源码解析

颛孙晗昱
2023-12-01

lrucache,最近最少使用缓存策略,源码其实很简单,没有多少行。下面我们分两个部分来解析:

第一部分:如何使用

    /**
     * 存储的key类型
     * 存储的value类型
     * 设置最大存储容量
     * 计算每个存储内容大小
     */
    LruCache<String, Bitmap> bitmapLruCache=new LruCache<String, Bitmap>(1024*1024){
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
        }
    };
    
    public void useLruCache(){
        //保存数据
        bitmapLruCache.put("111",bitmap);
        //获取数据
        Bitmap bitmap = bitmapLruCache.get("111");
        if(null!=bitmap){
            //使用数据
        }
    }

第二部分:源码分析

首先我们先来翻译一下LruCache源码头文件内容:

* A cache that holds strong references to a limited number of values. Each time
 * a value is accessed, it is moved to the head of a queue. When a value is
 * added to a full cache, the value at the end of that queue is evicted and may
 * become eligible for garbage collection.
   //这个类是一个强引用的有一定大小限制的缓存类。每次访问这个存储的内容,他都将被放到队列头部
   //(这里应该是表述错误,应该是放到队列尾部).如果一个内容想要被添加进一个容量已经满的缓存中
   //那么处于缓存队列尾部的内容将会被移除队列,并且被垃圾回收器回收。
 *
 * <p>If your cached values hold resources that need to be explicitly released,
 * override {@link #entryRemoved}.
   //如果缓存持有的内容需要被释放掉,那么需要重写entryRemoved方法。
 *
 * <p>If a cache miss should be computed on demand for the corresponding keys,
 * override {@link #create}. This simplifies the calling code, allowing it to
 * assume a value will always be returned, even when there's a cache miss.
   //如果一个被移除的缓存要求重新回到缓存队列中,那么可以重新create方法。这个方法,允许一个
   //将要被回收的内容被重新恢复。
 *
 * <p>By default, the cache size is measured in the number of entries. Override
 * {@link #sizeOf} to size the cache in different units. For example, this cache
 * is limited to 4MiB of bitmaps:
  //通常情况下,缓存内容的大小根据缓存条目数量来衡量。重新sizeOf方法,可以改变计算缓存内容的方 
  //法。例如,缓存内容控制在4M以内的缓存bitmap的实例:
 * <pre>   {@code
 *   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();
 *       }
 *   }}</pre>
 *
 * <p>This class is thread-safe. Perform multiple cache operations atomically by
 * synchronizing on the cache: <pre>   {@code
 *   synchronized (cache) {
 *     if (cache.get(key) == null) {
 *         cache.put(key, value);
 *     }
 *   }}</pre>
 *   //这个类是线程安全的。
 * <p>This class does not allow null to be used as a key or value. A return
 * value of null from {@link #get}, {@link #put} or {@link #remove} is
 * unambiguous: the key was not in the cache.
     //这个类不允许存储的key或者value为null。如果以key保存的内容不存在,那么get put remove方法
    //将会返回null

通过头文件,我们可以知道:

1.lrucache是线程安全的,关键操作都是枷锁的

2.不允许key或者value为null去缓存数据

3.sizeof方法应该说是必须要重写的,用来计算存储内容大小。如果被缓存的内容移除时需要释放掉,那么entryRemoved 方法中可以释放。当然create方法可以让那些被释放的内容起死回生。

4.上面给出了一个如何使用的示例。

源码分析:

1.创建

 /**
     * @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) {
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }

设置最大缓存容量,初始化LinkedHashMap,双向链表保存数据,特别注意最后一个参数是true,表示如果链表节点每次被访问则会重新排序,将访问的节点放入到链表尾部。

2.存储数据

public final V put(K key, V value) {
        V previous;
        synchronized (this) {
            putCount++;
            //获取插入内容大小
            size += safeSizeOf(key, value);
            //将新内容放入链表中,同时返回旧内容
            previous = map.put(key, value);
            if (previous != null) {
                //如果旧内容存在,则计算新内容和旧内容差值,重新设置给目前缓存内容大小
                size -= safeSizeOf(key, previous);
            }
        }
        //如果旧内容存在,则调用这个方法,我们可以在这个方法中做回收处理
        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }
        //重新比较目前缓存内容大小和设置最大值,确定是否需要释放缓存
        trimToSize(maxSize);
        return previous;
    }

计算存储内容大小,放入链表中,如果之前链表中有内容,则计算新内容和老内容大小差值,并且调用entryRemoved方法,我们可以在这个方法中释放旧内容。计算缓存内容是否超过设定最大值。trimToSize 方法我们看一下:

private void trimToSize(int maxSize) {
        //只要目前缓存内容大小超过最大值,则不停的删除头节点
        while (true) {
            K key;
            V value;
            synchronized (this) {
                //如果内容大小小于最大值,则退出循环
                if (size <= maxSize) {
                    break;
                }
                //获取头节点
                Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }
                // END LAYOUTLIB CHANGE

                if (toEvict == null) {
                    break;
                }
                //删除头结点
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }
            entryRemoved(true, key, value, null);
        }
    }

只要目前存储的内容大于设定的最大值,则将链表的头节点数据删除掉,直到小于设置的最大值。

3.获取数据:

public final V get(K key) {
        V mapValue;
        synchronized (this) {
            //获取内容,同时将此节点放到到链表尾部
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
        }
    }

此方法会获取到节点内容,同时会将节点重新排序,放入到链表尾部。

我们可以看到,lrucache充分利用了linkhashmap源码只要节点被访问则会重新排序,将访问节点放到最后节点的特点。

Lrucache源码分析完了,但是貌似没有说过他的缺点是什么,好像都是优点,那么他的缺点是什么呢?lrucache有一个致命的缺点,那就是被他引用的对象都是强引用,强引用特点就是宁可jvm挂掉,也不释放对象,所以lrucache反而会增大内存消耗,特别是当内存紧张的时候,系统需要释放一些内存来保命时,但是对于lrucache他没有任何办法?那怎么解决呢?我们把他持有的对象换成软引用不就可以了吗?用法如下:

LruCache<String, SoftReference<String>> lruCache=new LruCache<String, SoftReference<String>>(1000){
            @Override
            protected int sizeOf(String key, SoftReference<String> value) {
                if(value.get()!=null){
                    return value.get().length();
                }
                return 0;
            }
        }

好像是完美解决了,当内存不足时,被软引用持有的对象就会被回收了。但是,但是,仔细想想,lrucache是用来干什么的?保存最近最多使用对象的,如果你内存不足了,就把我对象给回收掉了,那我的意义在哪呢?本来我A对象刚使用了,结果你给我回收了,是不是跟我的设计初衷不符合了呢?而且还面临着,被回收了之后我就需要重新计算目前占用内存大小问题。所以说,这个方案不可取。那怎么完美解决呢?参考glide源码解决,glide是将三级缓存扩充到了四级,第一级lrucache,第二级弱引用+引用计数法,第三级本地缓存,第四级才是联网。当我们获取图片时,会先从lrucache中获取,如果有,则将图片从lrucache中remove掉,同时添加到弱引用中,同时弱引用中有个计算器,引用数量+1,下次再去lrucache取时,肯定就没有了,会去弱引用中找,引用数量再+1,当不用的时候,引用数量-1,直到为0,则重新将弱引用对象放入到lrucache中。也就是说,lrucache只持有最近使用的,当前正在使用的则用弱引用持有,那么如果内存紧张了,则可以直接将lrucache清空释放掉内存,这就完美解决了这个致命问题。同时,也说明glide实际使用的最大内存比lrucache申请的内存要大,因为正在使用的图片内存没计算在内。

 类似资料: