在平常的开发当中,咱们主要的工做就是把用户想要看的信息经过界面展现出来,不免就要和数据打交道,对于一些用户关心的数据,咱们确定是要每次都要从网络拿最新的数据展现。css
可是对于一些图片数据,若是咱们每次都从网络读取图片未免就有点浪费资源了,不只会浪费用户的流量,也会影响咱们 App 的性能,因此一般的作法就是对图片作缓存处理。java
相信你们都听过图片的三级缓存,下面先讲下什么是三级缓存?算法
三级缓存主要由三部分构成:缓存
咱们知道,在内存中对数据处理的速度是最快的,硬盘次之,而网络上读取数据,显示的时候,也要加载到内存中而后进行展现。安全
因此,咱们彻底能够将部分用户关心的,最常用的图片保存在内存和硬盘中,当下次展现的时候快速的进行展现,而且不用浪费用户流量。网络
好比咱们要加载一个图片,地址是 url,若是实现了三级缓存,那么咱们在要显示图片的时候,进行如下步骤:ide
上图中就展现了一个完整的实现了三级缓存的图片加载的流程,仔细分析下流程图,相信你已经知道什么是三级缓存是什么了源码分析
咱们知道,内存和硬盘空间是有限的,咱们在实现内存缓存和硬盘缓存的时候,不能够无休止的往缓存中添加数据,必然是要设置和合适的空间去缓存数据,当咱们设置的空间满的时候,咱们须要移除一部分数据,而后添加新的数据进入缓存。性能
这就遇到了一个问题:空间满的时候,先移除哪些数据呢?this
确定是先移除用户最不常用的数据,把用户常用的数据留在缓存中,保证用户能够快速的访问到数据,这就使用到了 LRU 算法
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“若是数据最近被访问过,那么未来被访问的概率也更高,若是数据最近没被,那么将来被访问的概率就比较低,优先删除”。
接下来咱们看下 Lru 算法在 Android 中的应用 LruCache 是怎么实现的(DiskLruCache 原理相似,本文不在将)
内存缓存可使用 Android 3.1 之后提供的一个缓存类:LruCache,这个类实现了 LRU 算法。
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.
If your cached values hold resources that need to be explicitly released, override entryRemoved(boolean, K, V, V).
If a cache miss should be computed on demand for the corresponding keys, override create(K). This simplifies the calling code, allowing it to assume a value will always be returned, even when there's a cache miss.
LruCache 是存储了有限数量的强引用的缓存,每次访问一个值的时候,会将其移动到队列的头部,当一个值添加到已经满的队列的时候,会将队列尾部的元素移除掉,让 GC 回收掉。
若是缓存的值明确的要知道已经释放,须要重写 entryRemoved(boolean, K, V, V) 方法,作一些本身的处理
若是缓存用没有找到一个对应的值,能够经过 create(K),简化了调用代码,容许即便是没有找到对应值的状况下可以返回一个值。
public class LruCache<K, V> {
// LruCache 的核心 LinkedHashMap
private final LinkedHashMap<K, V> map;
// 当前缓存大小
private int size;
// 最大缓存大小
private int maxSize;
// 插入次数
private int putCount;
// 建立次数,只有重写 create(K) 方法的时候会改变
private int createCount;
// 移除数据次数,缓存满的时候,插入新数据的时候,移除旧数据的时候,会改变这个值.
private int evictionCount;
// 命中次数,也就是 get 查找到元素的次数
private int hitCount;
// 未命中次数,也就是 get 没查找到元素的次数
private int missCount;
/** * @param maxSize 若是没有重写 sizeOf 方法,maxSize 就是缓存中元素的最大个数 * 若是重写了 sizeOf 方法,则 maxSize 就是全部缓存元素大小(也就是每一个元素乘以自身大小的总和) */
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
// 建立了一个默认容量 默认负载因子 ,容许访问排序的 LinkedHashMap.
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
}
复制代码
上面的最主要的就是设置的 maxSize 以及内部的定义的容许访问排序的 LinkedHashMap。
maxSize 在重写了 sizeOf 方法的状况下,表明的就是咱们每一个元素乘以自身大小以后累加的容许的最大值。
LinkedHashMap的对象 map 则是实现 LruCache 的核心,前面在 LinkedHashMap 源码分析 中已经讲了,若是在建立 LinkedHashMap 的时候,指定了 accessOrder 为 true 的话,那么就会在访问 LinkedHashMap 的过程当中,会对内部的元素从新排序,这里就是实现 LruCache 的关键部分。
虽然咱们设置了 accessOrder 为 true 实现了访问时的元素排序,可是还远远不够,由于 LruCache 会在必定时候移除最久未访问的元素,那达到什么程度移除?怎么移除?
答案是在 LruCache 中去实现的,下面就按传统的方式来了解 LruCache 的增删改查的相关操做,经过一个完整的流程分析,基本能了解整个 LruCache 的实现。
使用缓存,确定要先把数据添加到缓存中,咱们才能访问,在 LruCache 中添加缓存的操做是 put 方法:
public final V put(K key, V value) {
// 键值对不可为空
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
// 旧值
V previous;
// 同步代码块,使用 this 也就是说同时只能由一个线程操做这个对象
synchronized (this) {
putCount++;
// 先经过safeSizeOf方法计算当前传入的 value 的大小,累加的 size
size += safeSizeOf(key, value);
// 把键值对插入到 LinkedHashMap 中,若是有返回值,说明存在相同的 key,取出旧值给 previous
previous = map.put(key, value);
// 若是存在旧值,则从当前大小中删除旧值占用的大小.
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
// 若是 存在旧值,至关于把旧值移除了,这里调用 entryRemoved 方法.
// entryRemoved 默认是空实现,若是用户有需求,能够本身实现,完成一些资源的释放工做.
if (previous != null) {
entryRemoved(false, key, previous, value);
}
// 这个是最关键的方法,用来计算当前大小是否符合要求.
trimToSize(maxSize);
// 返回旧值
return previous;
}
复制代码
在 put 方法里面咱们看到了,使用 LruCache 缓存是不容许键值对为空的,而且在执行插入操做的时候,使用了 Synchronized 关键字对代码进行线程同步,保证了插入操做的线程安全。
而后计算了当前插入值的大小,累加到 size 上,执行完插入操做之后,若是以前存在相同的 key 值,则把以前元素的大小从 size 上面给移除掉。若是没有存在,就什么也不作。
若是用户重写了 entryRemoved 操做,也会回调 entryRemoved方法,让用户执行一些资源释放等工做。
最后调用了trimToSize(maxSize) 方法,这个方法是个核心方法,主要计算当前大小是否超过了设置的最大值,超过了则会将最近最少使用的元素移除。
在 LruCache 里面,控制缓存容量不超过咱们设置的最大值的关键点就是这个 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 || map.isEmpty()) {
break;
}
// 使用 map.entrySet() 表明从 LinkedHashMap 的头结点开始遍历,在
// 上篇文章里面看了源码,能够参考下面的连接
// 从头开始遍历,那只取一次,toEvict 就是头节点的元素
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
// 要删除元素的 key
key = toEvict.getKey();
// 要删除元素的 value
value = toEvict.getValue();
// 使用 LinkedHashMap 的 remove 方法删除指定元素
map.remove(key);
// 从新计算当前 size 的大小
size -= safeSizeOf(key, value);
// 移除次数+1
evictionCount++;
}
// 调用用户自定义的 entryRemoved() 若是用户定义了的话
entryRemoved(true, key, value, null);
}
}
复制代码
首先开启了一个无限循环,在循环里面的同步代码块里面会判断当前的容量 size 是否超过最大容量 maxSize。
若是没超过,结束循环。 若是超过,就会遍历内部的 LinkedHashMap 对象 map,这里使用的是 map.entrySet(),在上一篇LinkedHashMap 源码分析 里面咱们对 LinkedHashMap 的遍历作了简单的介绍,map.entrySet() 最终是调用 LinkedHashIterator 里面的 nextNode 拿到节点,而后在 LinkedEntryIterator 里面从节点里面 经过 nextNode() 拿到 entry 的值。
上篇源码里面讲了,在 LinkedHashIterator 的构造方法里面是从头节点开始取值的,因此这里的调用的 next 方法拿的就是头节点。
因此在 trimToSize 方法里面主要作的事情就是:若是容量没超过最大值,返回,若是超过最大值,就依次移除头节点元素,一直到容量知足设定的最大值。
public final V remove(K key) {
// 不容许 null 值
if (key == null) {
throw new NullPointerException("key == null");
}
// 删除的元素
V previous;
// 同步代码块保证线程安全
synchronized (this) {
// 删除元素,并把值赋给 previous
previous = map.remove(key);
//若是以前有 key 对应的值,将其减去
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
// 若是用户重写了entryRemoved 而且 以前有与 key 对应的值,执行entryRemoved。
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
复制代码
这里也很简单,住要是经过内部的 LinkedHashMap 移除元素,而后再把原来缓存中的对应的值删掉。
public final V get(K key) {
// 不容许 null key
if (key == null) {
throw new NullPointerException("key == null");
}
// value 的值
V mapValue;
// 同步代码块保证当前实例的线程安全
synchronized (this) {
// 经过 LinkedHashMap 的 get 方法去寻找
mapValue = map.get(key);
// 找到只,直接返回,命中值 +1
if (mapValue != null) {
hitCount++;
return mapValue;
}
// 没找到,未命中次数+1
missCount++;
}
// 这个地方意识,没有经过 get 方法找到,可是你想要有返回值,那么久能够重写 create 方法本身建立一个 返回值、。
V createdValue = create(key);
// 建立的值为 null ,直接返回 null
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
//将createdValue加入到map中,而且将原来键为key的对象保存到mapValue
mapValue = map.put(key, createdValue);
// 原来位置不为空,
if (mapValue != null) {
// There was a conflict so undo that last put
// 撤销上一步的操做,依旧把原来的值放到缓存。,替换掉新建立的值
map.put(key, mapValue);
} else {
// 原来key 对应的没值,计算当前缓存大小。
size += safeSizeOf(key, createdValue);
}
}
// 至关于一个替换操做,先用 createdValue 替换原来的值,而后这里移除掉 createdValue 。返回原来 key 对应的值。
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
// 调用trimToSize方法看是否须要回收旧数据
trimToSize(maxSize);
return createdValue;
}
}
复制代码
get方法前半部分是,从 map 里面取值,若是取到就返回。
若是没取到,而且重写了 create(K) 方法,就会先把 create(K) 方法建立的 value 保存到缓存,若是新建立的 value 保存的位置原来有值,就会替换回来。而且执行 entryRemoved 方法给调用者回调。
前面也讲了,LruCache 会根据元素的访问顺序进行排序。其实这里内部调用 LinkedHashMap 的 get 或者 put 方法的时候会调用到 afterNodeAccess 方法, 在 LinkedHashMap 的 afterNodeAccess 方法中对内部元素排序,这在上一篇 LinkedHashMap 中有讲到。
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
复制代码
这里仍是调用的 trimToSize 方法,传入的 -1,前面分析过,trimToSize 方法内部有一个循环,会在执行了
if (size <= maxSize || map.isEmpty()) {
break;
}
复制代码
之后,才会终止循环,这里传入 -1,也就是当 map.isEmpty() 的时候,终止循环,也就把缓存清空了。
经过对前面文章的阅读,相信你对 Android 提供给咱们的 LruCache 有了清除的认识。
归纳来讲就是: LruCache 中维护了一个 LinkedHashMap,该 LinkedHashMap 建立的时候,设置了 accessOrder 为 true,其内部元素不是已插入顺序排序,而是以访问顺序排序的。当调用put()方法获取数据的时候,会在内部的 map 中添加元素,并调用 trimToSize() 判断缓存是否已满,若是满了就删除 LinkedHashMap 中位于头节点的元素,即近期最少访问的元素。当调用 get() 方法访问缓存对象时,就会调用 LinkedHashMap 的 get() 方法得到对应集合元素,进而调用 LinkedHashMap 内部实现的 afterNodeAccess 方法将元素移动到尾节点。
事实上,LRU 算法是一种算法,具体的实现仍是要看我的,只不过这里 Google 为咱们提供了实现好的 LruCache,咱们也是能够本身实现一个相似的 LruCache 的。
最重要的仍是要懂思想啊。