LruCache 源码解析

一、抛个问题

判断分析题:LruCache 能够减少磁盘读写的次数。

二、简要总结

  1. LruCache 是一种比较简单的缓存实现,更好的实现应该要记录每个数据被调用的次数,按照 “ 最经常使用数据优先级最高 ” 的原则进行缓存。
  2. LruCache 使用 LinkHashMap 作为内部数据结构,来实现 Least Recently Used 缓存。LinkHashMap 与 HashMap 相比,就多了一个双向链表,用来存储数据访问顺序,即当缓存数据达到最大容量时,只要删除第 1 个元素即可。
  3. 可以重写 LruCache 几个方法,来达到我们进行缓存器定制的需求可以重写 LruCache 几个方法,来达到我们进行缓存器定制的需求:
  • protected V create(K key):用来从磁盘文件读取数据,当从缓存中获取不到数据时,LruCache 会调用该方法,默认返回 null。通过重写该方法,可以直接通过 LruCache 的 get()接口获取到目标数据。
  • protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue):缓存更改数据,删除数据时的回调方法,通过该方法,可以将缓存的更新操作同步到磁盘文件中,也可以进行资源释放。
  • protected int sizeOf(K key, V value):单个元素所占的空间大小。如果存储的元素数据量较小,且容量不大,那么该方法可以不重写;但如果涉及到缓存图片之类的大数据量元素,那么就要考虑重写该方法,防止内存异常。
  1. LruCache get()操作,如果缓存中存在目标数据,那么是不会去磁盘文件中再获取一次的。如果遇到如下场景:两个进程同时对一个磁盘数据进行读写,并且两个进程都存在相对应的数据缓存,当 A 进程更新磁盘文件的数据时,B 进程无法取到最新的数据,只能等待 A 进程该数据缓存过期后,再从磁盘获取,才能获取到最新的数据。或者通过重写 get(),或者通过建立监听器等行为来确保数据同步。总之,使用 LruCache 时,一定要结合自身的业务场景。

三、源码分析

由于该类代码量不多,比较简单,所以对整份代码进行分析(基于 Android K)

public class LruCache<K, V> {
	// 缓存的核心数据结构
    private final LinkedHashMap<K, V> map;
	// 当前 cache 所占的大小
    private int size;
    // 整个缓存机制的最大容量
    private int maxSize;
	// 调用 put() 的次数
    private int putCount;
	// 调用 create() 的次数
    private int createCount;
    // 因为 size > maxSize,导致移除元素出 map 的次数
    private int evictionCount;
    // 通过 map 获取到数据的次数
    private int hitCount;
    // 不能通过 map 获取到数据的次数
    private int missCount;

    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        // 这个 0.75 是扩展因子,但在 HashMap 体系下的类的没用的
        // 因为 HashMap 固定用的就是 0.75,而不会管这个扩展因子设置为多少
     	// 具体原因可以参考 HashMap.DEFAULT_LOAD_FACTOR 的解释
     	/**
          * The default load factor. Note that this implementation ignores the
          * load factor, but cannot do away with it entirely because it's
          * mentioned in the API.
          *
          * <p>Note that this constant has no impact on the behavior of the program,
          * but it is emitted as part of the serialized form. The load factor of
          * .75 is hardwired into the program, which uses cheap shifts in place of
          * expensive division.
         */
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }

    // 重新定义缓存的最大容量,如果当前容量超标了,那么移除最久的元素
    public void resize(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }

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

    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++;
        }
		// 尝试去创建一个元素,由用户自行定制,我们可以去磁盘文件读取
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);

            if (mapValue != null) {
                // 如果从磁盘中读取数据的过程中,缓存中已经被插入了目标元素,那么将本次新建的元素移除掉
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
        	// 通知用户,有一个元素被移除了,要进行资源释放等操作
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }

    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) {
        	// 通知用户,有一个元素被移除了,要进行资源释放等操作
            entryRemoved(false, key, previous, value);
        }

        trimToSize(maxSize);
        return previous;
    }

	// 为缓存瘦身,使缓存大小 <= maxSize
    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++;
            }
			// true 表示由于容量超标,需要移除旧元素
            entryRemoved(true, key, value, null);
        }
    }

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

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

        return previous;
    }

    // 通知用户,有元素被移除了,需要实现资源回收
    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
	// 由用户自行实现从磁盘读取数据的过程
    protected V create(K key) {
        return null;
    }

    private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }
	
	// 如果只是数据量较小,可以不重写该方法,如果单个元素的数据量较大,那么需要重写该方法,该方法与 maxSize 是搭配使用的。
    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;
    }

    /**
     * Returns a copy of the current contents of the cache, ordered from least
     * recently accessed to most recently accessed.
     */
    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);
    }
}

回过头来看看开头的问题:LruCache 能够减少磁盘读写的次数?
我认为:不重写 get()方法的前提下,LruCache 能够有效减少读磁盘的次数,但是会造成缓存数据与磁盘数据不同步,不过我们也可以结合现有的接口create()put()等方法,来实现强制刷新缓存;但是 LruCache 不能很好地减少磁盘写的次数,因为磁盘文件数据必须是正确的,否则可能影响其他应用的流程。当然,这得看具体业务来决定粒度的大小,比如磁盘文件只被当前进程使用,那么完全可以通过定时、触发器(移除元素)时,再将缓存强制写入磁盘。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值