【转载】LruCache 源码解析
原文地址:https://github.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/blob/master/article/LruCache%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md
1. 简介
LRU 是 Least Recently Used 最近最少使用算法。
曾经,在各大缓存图片的框架没流行的时候。有一种很常用的内存缓存技术:SoftReference 和 WeakReference(软引用和弱引用)。但是走到了 Android 2.3(Level 9)时代,垃圾回收机制更倾向于回收 SoftReference 或 WeakReference 的对象。后来,又来到了 Android3.0,图片缓存在内容中,因为不知道要在是什么时候释放内存,没有策略,没用一种可以预见的场合去将其释放。这就造成了内存溢出。
2. 使用方法
当成一个 Map 用就可以了,只不过实现了 LRU 缓存策略。
使用的时候记住几点即可:
- **1.(必填)**你需要提供一个缓存容量作为构造参数。
- 2.(必填) 覆写 sizeOf方法 ,自定义设计一条数据放进来的容量计算,如果不覆写就无法预知数据的容量,不能保证缓存容量限定在最大容量以内。
- 3.(选填) 覆写 entryRemoved方法 ,你可以知道最少使用的缓存被清除时的数据( evicted, key, oldValue, newVaule )。
- **4.(记住)**LruCache是线程安全的,在内部的 get、put、remove 包括 trimToSize 都是安全的(因为都上锁了)。
- 5.(选填) 还有就是覆写 create方法 。
一般做到 1、2、3、4就足够了,5可以无视 。
以下是 一个 LruCache 实现 Bitmap 小缓存的案例, entryRemoved 里的自定义逻辑可以无视,想看的可以去到我的我的展示demo 里的看自定义 entryRemoved 逻辑。
private static final float ONE_MIB = 1024 * 1024;
// 7MB
private static final int CACHE_SIZE = (int) (7 * ONE_MIB);
private LruCache<String, Bitmap> bitmapCache;
this.bitmapCache = new LruCache<String, Bitmap>(CACHE_SIZE) {
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
} @Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
...
}
};
3. 效果展示
4. 源码分析
4.1 LruCache 原理概要解析
LruCache 就是 利用 LinkedHashMap 的一个特性( accessOrder=true 基于访问顺序 )再加上对 LinkedHashMap 的数据操作上锁实现的缓存策略。
LruCache 的数据缓存是内存中的。
- 1.首先设置了内部 - LinkedHashMap构造参数- accessOrder=true, 实现了数据排序按照访问顺序。
- 2.然后在每次 - LruCache.get(K key)方法里都会调用- LinkedHashMap.get(Object key)。
- 3.如上述设置了 - accessOrder=true后,每次- LinkedHashMap.get(Object key)都会进行- LinkedHashMap.makeTail(LinkedEntry<K, V> e)。
- 4. - LinkedHashMap是双向循环链表,然后每次- LruCache.get->- LinkedHashMap.get的数据就被放到最末尾了。
- 5.在 - put和- trimToSize的方法执行下,如果发生数据量移除,会优先移除掉最前面的数据(因为最新访问的数据在尾部)。
具体解析在: 4.2、4.3、4.4、4.5 。
4.2 LruCache 的唯一构造方法
/**
* LruCache的构造方法:需要传入最大缓存个数
*/
public LruCache(int maxSize) { ... this.maxSize = maxSize;
/*
* 初始化LinkedHashMap
* 第一个参数:initialCapacity,初始大小
* 第二个参数:loadFactor,负载因子=0.75f
* 第三个参数:accessOrder=true,基于访问顺序;accessOrder=false,基于插入顺序
*/
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
第一个参数 initialCapacity 用于初始化该 LinkedHashMap 的大小。
先简单介绍一下 第二个参数 loadFactor,这个其实的 HashMap 里的构造参数,涉及到扩容问题,比如 HashMap 的最大容量是100,那么这里设置0.75f的话,到75容量的时候就会扩容。
主要是第三个参数 accessOrder=true ,这样的话 LinkedHashMap 数据排序就会基于数据的访问顺序,从而实现了 LruCache 核心工作原理。
4.3 LruCache.get(K key)
/**
* 根据 key 查询缓存,如果存在于缓存或者被 create 方法创建了。
* 如果值返回了,那么它将被移动到双向循环链表的的尾部。
* 如果如果没有缓存的值,则返回 null。
*/
public final V get(K key) { ... V mapValue;
synchronized (this) {
// 关键点:LinkedHashMap每次get都会基于访问顺序来重整数据顺序
mapValue = map.get(key);
// 计算 命中次数
if (mapValue != null) {
hitCount++;
return mapValue;
}
// 计算 丢失次数
missCount++;
} /*
* 官方解释:
* 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时
* 候,一个冲突的值被添加到Map,我们在Map中删除这个值,释放被创造的值。
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
} /***************************
* 不覆写create方法走不到下面 *
***************************/ /*
* 正常情况走不到这里
* 走到这里的话 说明 实现了自定义的 create(K key) 逻辑
* 因为默认的 create(K key) 逻辑为null
*/
synchronized (this) {
// 记录 create 的次数
createCount++;
// 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值
mapValue = map.put(key, createdValue); // 如果之前存在相同key的value,即有冲突。
if (mapValue != null) {
/*
* 有冲突
* 所以 撤销 刚才的 操作
* 将 之前相同key 的值 重新放回去
*/
map.put(key, mapValue);
} else {
// 拿到键值对,计算出在容量中的相对长度,然后加上
size += safeSizeOf(key, createdValue);
}
} // 如果上面 判断出了 将要放入的值发生冲突
if (mapValue != null) {
/*
* 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
* 告诉 自定义 的 entryRemoved 方法
*/
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
// 上面 进行了 size += 操作 所以这里要重整长度
trimToSize(maxSize);
return createdValue;
}
}
上述的 get 方法表面并没有看出哪里有实现了 LRU 的缓存策略。主要在 mapValue = map.get(key);里,调用了 LinkedHashMap 的 get 方法,再加上 LruCache 构造里默认设置 LinkedHashMap 的 accessOrder=true。
4.4 LinkedHashMap.get(Object key)
/**
* Returns the value of the mapping with the specified key.
*
* @param key
* the key.
* @return the value of the mapping with the specified key, or {@code null}
* if no mapping for the specified key is found.
*/
@Override public V get(Object key) {
/*
* This method is overridden to eliminate the need for a polymorphic
* invocation in superclass at the expense of code duplication.
*/
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
if (e == null)
return null;
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
} int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
}
return null;
}
其实仔细看 if (accessOrder) 的逻辑即可,如果 accessOrder=true 那么每次 get 都会执行 N 次 makeTail(LinkedEntry<K, V> e) 。
接下来看看:
4.5 LinkedHashMap.makeTail(LinkedEntry<K, V> e)
/**
* Relinks the given entry to the tail of the list. Under access ordering,
* this method is invoked whenever the value of a pre-existing entry is
* read by Map.get or modified by Map.put.
*/
private void makeTail(LinkedEntry<K, V> e) {
// Unlink e
e.prv.nxt = e.nxt;
e.nxt.prv = e.prv; // Relink e as tail
LinkedEntry<K, V> header = this.header;
LinkedEntry<K, V> oldTail = header.prv;
e.nxt = header;
e.prv = oldTail;
oldTail.nxt = header.prv = e;
modCount++;
}
// Unlink e
// Relink e as tail
LinkedHashMap 是双向循环链表,然后此次 LruCache.get -> LinkedHashMap.get 的数据就被放到最末尾了。
以上就是 LruCache 核心工作原理。
接下来介绍 LruCache 的容量溢出策略。
4.6 LruCache.put(K key, V value)
public final V put(K key, V value) {
    ...
    synchronized (this) {
        ...
        // 拿到键值对,计算出在容量中的相对长度,然后加上
        size += safeSizeOf(key, value);
        ...
    }
    ...
    trimToSize(maxSize);
    return previous;
}
记住几点:
- **1.**put 开始的时候确实是把值放入 LinkedHashMap 了,不管超不超过你设定的缓存容量。
- **2.**然后根据 safeSizeOf方法计算 此次添加数据的容量是多少,并且加到size里 。
- **3.**说到 safeSizeOf就要讲到sizeOf(K key, V value)会计算出此次添加数据的大小 。
- **4.**直到 put 要结束时,进行了 trimToSize才判断size是否 大于maxSize然后进行最近很少访问数据的移除。
4.7 LruCache.trimToSize(int maxSize)
public void trimToSize(int maxSize) {
    /*
     * 这是一个死循环,
     * 1.只有 扩容 的情况下能立即跳出
     * 2.非扩容的情况下,map的数据会一个一个删除,直到map里没有值了,就会跳出
     */
    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!");
            }
            // 如果是 扩容 或者 map为空了,就会中断,因为扩容不会涉及到丢弃数据的情况
            if (size <= maxSize || map.isEmpty()) {
                break;
            }
            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
            key = toEvict.getKey();
            value = toEvict.getValue();
            map.remove(key);
            // 拿到键值对,计算出在容量中的相对长度,然后减去。
            size -= safeSizeOf(key, value);
            // 添加一次收回次数
            evictionCount++;
        }
        /*
         * 将最后一次删除的最少访问数据回调出去
         */
        entryRemoved(true, key, value, null);
    }
}
简单描述:会判断之前 size 是否大于 maxSize 。是的话,直接跳出后什么也不做。不是的话,证明已经溢出容量了。由makeTail 图已知,最近经常访问的数据在最末尾。拿到一个存放 key 的 Set,然后一直一直从头开始删除,删一个判断是否溢出,直到没有溢出。
最后看看:
4.8 覆写 entryRemoved 的作用
entryRemoved被LruCache调用的场景:
- 1.(put) put 发生 key 冲突时被调用,evicted=false,key=此次 put 的 key,oldValue=被覆盖的冲突 value,newValue=此次 put 的 value。
- 2.(trimToSize) trimToSize 的时候,只会被调用一次,就是最后一次被删除的最少访问数据带回来。evicted=true,key=最后一次被删除的 key,oldValue=最后一次被删除的 value,newValue=null(此次没有冲突,只是 remove)。
- 3.(remove) remove的时候,存在对应 key,并且被成功删除后被调用。evicted=false,key=此次 put的 key,oldValue=此次删除的 value,newValue=null(此次没有冲突,只是 remove)。
- 4.(get后半段,查询丢失后处理情景,不过建议忽略) get 的时候,正常的话不实现自定义 create的话,代码上看 get 方法只会走一半,如果你实现了自定义的create(K key)方法,并且在 你 create 后的值放入 LruCache 中发生 key 冲突时被调用,evicted=false,key=此次 get 的 key,oldValue=被你自定义 create(key)后的 value,newValue=原本存在 map 里的 key-value。
解释一下第四点吧:**<1>.第四点是这样的,先 get(key),然后没拿到,丢失。<2>.如果你提供了 自定义的 create(key) 方法,那么 LruCache 会根据你的逻辑自造一个 value,但是当放入的时候发现冲突了,但是已经放入了。<3>.**此时,会将那个冲突的值再让回去覆盖,此时调用上述4.的 entryRemoved。
因为 HashMap 在数据量大情况下,拿数据可能造成丢失,导致前半段查不到,你自定义的 create(key) 放入的时候发现又查到了**(有冲突)**。然后又急忙把原来的值放回去,此时你就白白create一趟,无所作为,还要走一遍entryRemoved。
综上就如同注释写的一样:
/**
* 1.当被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用
* 或者替换条目值时put调用,默认实现什么都没做。
* 2.该方法没用同步调用,如果其他线程访问缓存时,该方法也会执行。
* 3.evicted=true:如果该条目被删除空间 (表示 进行了trimToSize or remove) evicted=false:put冲突后 或 get里成功create后
* 导致
* 4.newValue!=null,那么则被put()或get()调用。
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
}
可以参考我的 demo 里的 entryRemoved 。
4.9 LruCache 局部同步锁
在 get, put, trimToSize, remove 四个方法里的 entryRemoved 方法都不在同步块里。因为 entryRemoved 回调的参数都属于方法域参数,不会线程不安全。
本地方法栈和程序计数器是线程隔离的数据区
5. 开源项目中的使用
6. 总结
LruCache重要的几点:
- 1.LruCache 是通过 LinkedHashMap 构造方法的第三个参数的 - accessOrder=true实现了- LinkedHashMap的数据排序基于访问顺序 (最近访问的数据会在链表尾部),在容量溢出的时候,将链表头部的数据移除。从而,实现了 LRU 数据缓存机制。
- **2.**LruCache 在内部的get、put、remove包括 trimToSize 都是安全的(因为都上锁了)。 
- **3.**LruCache 自身并没有释放内存,将 LinkedHashMap 的数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存。 
- **4.**覆写 - entryRemoved方法能知道 LruCache 数据移除是是否发生了冲突,也可以去手动释放资源。
- 5. - maxSize和- sizeOf(K key, V value)方法的覆写息息相关,必须相同单位。( 比如 maxSize 是7MB,自定义的 sizeOf 计算每个数据大小的时候必须能算出与MB之间有联系的单位 )
7. 资源
【转载】LruCache 源码解析的更多相关文章
- Android——LruCache源码解析
		以下针对 Android API 26 版本的源码进行分析. 在了解LruCache之前,最好对LinkedHashMap有初步的了解,LruCache的实现主要借助LinkedHashMap.Lin ... 
- Android源码解析——LruCache
		我认为在写涉及到数据结构或算法的实现类的源码解析博客时,不应该急于讲它的使用或马上展开对源码的解析,而是要先交待一下这个数据结构或算法的资料,了解它的设计,再从它的设计出发去讲如何实现,最后从实现的角 ... 
- jQuery整体架构源码解析(转载)
		jQuery整体架构源码解析 最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性, ... 
- 转载:Bootstrap 源码解析
		Bootstrap 源码解析 前言 Bootstrap 是个CSS库,简单,高效.很多都可以忘记了再去网站查.但是有一些核心的东西需要弄懂.个人认为弄懂了这些应该就算是会了.源码看一波. 栅格系统 所 ... 
- 【转载】Xutils3源码解析
		Github源码地址:https://github.com/wyouflf/xUtils3 原文地址 :http://www.codekk.com/blogs/detail/54cfab086c476 ... 
- 【转载】okhttp源码解析
		转自:http://www.open-open.com/lib/view/open1472216742720.html https://blog.piasy.com/2016/07/11/Unders ... 
- 【转载】FloatingActionButton源码解析
		原文地址:https://github.com/Rowandjj/my_awesome_blog/blob/master/fab_anlysis/README.md loatingActionButt ... 
- 【转载】Scroller源码解析
		原文地址:https://github.com/Skykai521/AndroidSdkSourceAnalysis/blob/master/article/Scroller%E6%BA%90%E7% ... 
- solr&lucene3.6.0源码解析(二)
		上文描述了solr3.6.0怎么采用maven管理的方式在eclipse中搭建开发环境,在solr中,为了提高搜索性能,采用了缓存机制,这里描述的是LRU缓存,这里用到了 LinkedHashMap类 ... 
随机推荐
- 【转】Pyhton 单行、多行注释符号使用方法及规范
			转自:Pyhton 单行.多行注释符号使用方法及规范 python中的注释有多种,有单行注释,多行注释,批量注释,中文注释也是常用的.python注释也有自己的规范,在文章中会介绍到.注释可以起到一个 ... 
- ZooKeeper食谱(八)
			使用ZooKeeper构造高级别应用的指南 在这个文章中,你将会发现使用ZooKeeper来实现高级别功能的指南.所有的它们在客户端上被实现而不需要ZooKeeper特别的支持.希望社区将注意到这些约 ... 
- Ant打jar包时,参数名被修改的问题
			https://blog.csdn.net/landehuxi/article/details/42678117 使用Ant打jar包后,发现jar包中的方法名会在前面自动添加了“param”前缀,导 ... 
- modelsim10 SE 仿真lattice Xp2工程
			1.首先要建立Lattice XP2库 在modelsim10 SE启动后.首先指定Lattice Diamond 1.4 给定的仿真器库源代码编译目录: C:\lscc\diamond\1.4\ca ... 
- [cerc2012][Gym100624B]20181013
- 【uva12232/hdu3461】带权并查集维护异或值
			题意: 对于n个数a[0]~a[n-1],但你不知道它们的值,通过逐步提供给你的信息,你的任务是根据这些信息回答问题: I P V :告诉你a[P] = V I P Q V:告诉你a[P] XOR a ... 
- JS之window对象
			window对象 window属性: opener:打开当前窗口的源窗口,如果这个窗口是由别的网页点击链接跳转过来的,或者是从另外一个页面点击打开窗口打开的,opener就是找到源页面的.如果当前窗口 ... 
- CSS3禁止网页中文本被选中代码
			通常大家会有js来实现,另一个方案就是,将-webkit-user-select 和-moz-user-select 的值设为none,这针对于移动用户,可能会很有用.请谨慎使用这个属性:因为大部分用 ... 
- 5.0docer 网络链接
			docker0 :linux的虚拟网桥 虚拟网桥特点: 1.可以设置ip地址 2.相当于拥一个隐藏的虚拟网卡 安装网桥工具 apt-get install bridge-utils brctl ... 
- 【转】关于Scapy
			关于Scapy Scapy的是一个强大的交互式数据包处理程序(使用python编写).它能够伪造 或者解码大量的网络协议数据包,能够发送.捕捉.匹配请求和回复包等等.它可以很容易地处理一些典型操作,比 ... 
