Android源码解析——LruCache
我认为在写涉及到数据结构或算法的实现类的源码解析博客时,不应该急于讲它的使用或马上展开对源码的解析,而是要先交待一下这个数据结构或算法的资料,了解它的设计,再从它的设计出发去讲如何实现,最后从实现的角度来讲回源码,才能深入理解。这是最新读了一些博客之后的思考。对此问题如果你有其他见解,欢迎留言交流。
LRU
在读LruCache源码之前,我们先来了解一下这里的Lru
是什么。LRU
全称为Least Recently Used
,即最近最少使用,是一种缓存置换算法。我们的缓存容量是有限的,它会面临一个问题:当有新的内容需要加入我们的缓存,但我们的缓存空闲的空间不足以放进新的内容时,如何舍弃原有的部分内容从而腾出空间用来放新的内容。解决这个问题的算法有多种,比如LRU,LFU,FIFO等。
需要注意区分的是LRU
和LFU
。前者是最近最少使用,即淘汰最长时间未使用的对象;后者是最近最不常使用,即淘汰一段时间内使用最少的对象。比如我们缓存对象的顺序是:A B C B D A C A ,当需要淘汰一个对象时,如果采用LRU算法,则淘汰的是B,因为它是最长时间未被使用的。如果采用LFU算法,则淘汰的是D,因为在这段时间内它只被使用了一次,是最不经常使用的。
了解了LRU
之后,我们再来看一下LruCache是如何实现的。
LinkedHashMap
我们看一下LruCache
的结构,它的成员变量及构造方法定义如下(这里分析的是android-23里的代码):
private final LinkedHashMap<K, V> map;
private int size; //当前缓存内容的大小。它不一定是元素的个数,比如如果缓存的是图片,一般用的是图片占用的内存大小
private int maxSize; // 最大可缓存的大小
private int putCount; // put 方法被调用的次数
private int createCount; // create(Object) 被调用的次数
private int evictionCount; // 被置换出来的元素的个数
private int hitCount; // get 方法命中缓存中的元素的次数
private int missCount; // get 方法未命中缓存中元素的次数
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
对象当中的。那么,LinkedHashMap
是什么?它是怎么实现LRU
这种缓存策略的?
LinkedHashMap
继承自HashMap
,不同的是,它是一个双向循环链表,它的每一个数据结点都有两个指针,分别指向直接前驱和直接后继,这一个我们可以从它的内部类LinkedEntry
中看出,其定义如下:
static class LinkedEntry<K, V> extends HashMapEntry<K, V> {
LinkedEntry<K, V> nxt;
LinkedEntry<K, V> prv;
/** Create the header entry */
LinkedEntry() {
super(null, null, 0, null);
nxt = prv = this;
}
/** Create a normal entry */
LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next,
LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) {
super(key, value, hash, next);
this.nxt = nxt;
this.prv = prv;
}
}
LinkedHashMap
实现了双向循环链表的数据结构,它的定义如下:
public class LinkedHashMap<K, V> extends HashMap<K, V> {
transient LinkedEntry<K, V> header;
private final boolean accessOrder;
}
当链表不为空时,header.nxt
指向第一个结点,header.prv
指向最后一个结点;当链表为空时,header.nxt
与header.prv
都指向它本身。
accessOrder
是指定它的排序方式,当它为false
时,只按插入的顺序排序,即新放入的顺序会在链表的尾部;而当它为true
时,更新或访问某个节点的数据时,这个对应的结点也会被放到尾部。它通过构造方法public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
来赋值。
我们来看一下加入一个新结点时的方法执行过程:
@Override void addNewEntry(K key, V value, int hash, int index) {
LinkedEntry<K, V> header = this.header;
// Remove eldest entry if instructed to do so.
LinkedEntry<K, V> eldest = header.nxt;
if (eldest != header && removeEldestEntry(eldest)) {
remove(eldest.key);
}
// Create new entry, link it on to list, and put it into table
LinkedEntry<K, V> oldTail = header.prv;
LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
key, value, hash, table[index], header, oldTail);
table[index] = oldTail.nxt = header.prv = newTail;
}
可以看到,当加入一个新结点时,结构如下:
当accessOrder
为true
时,更新或者访问一个结点时,它会把这个结点移到尾部,对应代码如下:
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),也就是把旧的尾部的nxt
以及头部的prv
指向它,并让它的nxt
指向头部,把它的prv
指向旧的尾部。如下图:
除此之外,LinkedHashMap
还提供了一个方法public Entry<K, V> eldest()
,它返回的是最老的结点,当accessOrder
为true
时,也就是最近最少使用的结点。
LruCache
熟悉了LinkedHashMap
之后,我们发现,通过它来实现Lru
算法也就变得理所当然了。我们所需要做的,就只剩下定义缓存的最大大小,记录缓存当前大小,在放入新数据时检查是否超过最大大小。所以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;
然后我们来读一下它的get方法:
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++;
}
/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
*/
//尝试创建一个值,这个方法的默认实现是直接返回null。但是在它的设计中,这个方法可能执行完成之后map已经有了变化。
V createdValue = create(key);
if (createdValue == null) {//如果不为没有命名的key创建新值,则直接返回
return null;
}
synchronized (this) {
createCount++;
//将创建的值放入map中,如果map在前面的过程中正好放入了这对key-value,那么会返回放入的value
mapValue = map.put(key, createdValue);
if (mapValue != null) {//如果不为空,说明不需要我们所创建的值,所以又把返回的值放进去
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);//为空,说明我们更新了这个key的值,需要重新计算大小
}
}
if (mapValue != null) {//上面放入的值有冲突
entryRemoved(false, key, createdValue, mapValue);// 通知之前创建的值已经被移除,而改为mapValue
return mapValue;
} else {
trimToSize(maxSize);//没有冲突时,因为放入了新创建的值,大小已经有变化,所以需要修整大小
return createdValue;
}
}
LruCache
是可能被多个线程同时访问的,所以在读写map
时进行加锁。当获取不到对应的key
的值时,它会调用其create(K key)
方法,这个方法用于当缓存没有命名时计算一个key所对应的值,它的默认实现是直接返回null。这个方法并没有加上同步锁,也就是在它进行创建时,map
可能已经有了变化。
所以在get方法中,如果create(key)
返回的V不为null
,会再把它给放到map
中,并检查是否在它创建的期间已经有其他对象也进行创建并放到map
中了,如果有,则会放弃这个创建的对象,而把之前的对象留下,否则因为我们放入了新创建的值,所以要计算现在的大小并进行trimToSize
。
trimToSize
方法是根据传进来的maxSize,如果当前大小超过了这个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++;
}
entryRemoved(true, key, value, null);
}
}
接下来,我们再来看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) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
主要逻辑是,计算新增加的大小,加入size,然后把key-value放入map中,如果是更新旧的数据(map.put(key, value)
会返回之前的value),则减去旧数据的大小,并调用entryRemoved(false, key, previous, value)
方法通知旧数据被更新为新的值,最后也是调用trimToSize(maxSize)
修整缓存的大小。
剩下的其他方法,比如删除里面的对象,或进行调整大小的操作,逻辑上都和上面的类似,这里略过。LruCache还定义了一些变量用于统计缓存命中率等,这里也不再进行赘述。
结语
LruCache的源码分析就到这里,它对LRU算法的实现主要是通过LinkedHashMap
来完成。另外,使用LRU算法,说明我们需要设定缓存的最大大小,而缓存对象的大小在不同的缓存类型当中的计算方法是不同的,计算的方法通过protected int sizeOf(K key, V value)
实现,这里的默认实现是存放的元素的个数。举个例子,如果我们要缓存Bitmap对象,则需要重写这个方法,并返回bitmap对象的所有像素点所占的内存大小之和。还有,LruCache在实现的时候考虑到了多线程的访问问题,所以在对map进行更新时,都会加上同步锁。
LruCache是对LRU策略的内存缓存的实现,基于它,我们可以去实现自己的图片缓存或其它缓存等。除了内存缓存的LRU算法实现,谷歌在后来的系统源码中也曾经加上该算法的磁盘缓存的实现,目前在android-23的示例DisplayingBitmaps中,也有对应的源码DiskLruCache.java
。对了,关于如何使用LruCache来实现图片内存缓存的具体代码,同样可以参照谷歌提供的这个示例代码中的ImageCache.java
(在线浏览示例代码请翻墙访问:https://android.googlesource.com/platform/developers/samples/android/+/master/ui/graphics/DisplayingBitmaps/Application/src/main/java/com/example/android/displayingbitmaps/util/)。
另外,啰嗦一句:LRU的缓存策略由来已久,图片缓存也并非没有策略,弱引用和软引用更不是各种图片框架没流行之前的很常用的内存缓存技术,垃圾回收机制更倾向于回收弱引用和软引用对象的这种说法也是不妥当的。
参考文献:
《缓存》 维基百科:https://zh.wikipedia.org/wiki/%E7%BC%93%E5%AD%98
友情校对:寒枫
Android源码解析——LruCache的更多相关文章
- Android源码解析系列
转载请标明出处:一片枫叶的专栏 知乎上看了一篇非常不错的博文:有没有必要阅读Android源码 看完之后痛定思过,平时所学往往是知其然然不知其所以然,所以为了更好的深入Android体系,决定学习an ...
- android源码解析(十七)-->Activity布局加载流程
版权声明:本文为博主原创文章,未经博主允许不得转载. 好吧,终于要开始讲讲Activity的布局加载流程了,大家都知道在Android体系中Activity扮演了一个界面展示的角色,这也是它与andr ...
- Android源码解析——Toast
简介 Toast是一种向用户快速展示少量信息的视图.当它显示时,它会浮在整个应用层的上面,并且不会获取到焦点.它的设计思想是能够向用户展示些信息,但又能尽量不显得唐突.本篇我们来研读一下Toast的源 ...
- Android源码解析——AsyncTask
简介 AsyncTask 在Android API 3引入,是为了使UI线程能被正确和容易地使用.它允许你在后台进行一些操作,并且把结果带到UI线程中,而不用自己去操纵Thread或Handler.它 ...
- Android 源码解析 之 setContentView
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/41894125,本文出自:[张鸿洋的博客] 大家在平时的开发中,对于setCont ...
- Android源码解析——Handler、Looper与MessageQueue
本文的目的是来分析下 Android 系统中以 Handler.Looper.MessageQueue 组成的异步消息处理机制,通过源码来了解整个消息处理流程的走向以及相关三者之间的关系 需要先了解以 ...
- Android 源码解析之AsyncTask
AsyncTask相信大家都不陌生,它是为了简化异步请求.更新UI操作而诞生的.使用它不仅可以完成我们的网络耗时操作,而且还可以在完成耗时操作后直接的更新我们所需要的UI组件.这使得它在android ...
- 【Android源码解析】View.post()到底干了啥
emmm,大伙都知道,子线程是不能进行 UI 操作的,或者很多场景下,一些操作需要延迟执行,这些都可以通过 Handler 来解决.但说实话,实在是太懒了,总感觉写 Handler 太麻烦了,一不小心 ...
- Android 源码解析:单例模式-通过容器实现单例模式-懒加载方式
本文分析了 Android 系统服务通过容器实现单例,确保系统服务的全局唯一. 开发过 Android 的用户肯定都用过这句代码,主要作用是把布局文件 XML 加载到系统中,转换为 Android 的 ...
随机推荐
- 类似吸顶功能解决ios不能实时监听onscroll的触发问题
问题:近期项目需要一个类似西东功能,当页面向上滚动160px后div固定在顶部 解决方法:首先,想到的是window.onscroll方法 .fixed{position:fixed;-webkit- ...
- EFCore CodeFirst 连接MySql
一.工具及环境 Visual Studio 2017 15.4.3 MySql Navicat for MySQL 二.Entity Framwork Core 2.0 MySql Code Firs ...
- JavaScript数据结构与算法(三) 优先级队列的实现
TypeScript方式实现源码 // Queue类和PriorityQueue类实现上的区别是,要向PriorityQueue添加元素,需要创建一个特殊的元素.这个元素包含了要添加到队列的元素(它可 ...
- python3 Serial 串口助手的接收读取数据
其实网上已经有许多python语言书写的串口,但大部分都是python2写的,没有找到一个合适的python编写的串口助手,只能自己来写一个串口助手,由于我只需要串口能够接收读取数据就可以了,故而这个 ...
- Shell的基本命令(第一天),根据w3c学习得
Shell是一种应用程序,提供一个界面访问操作系统内核的服务. 1:编写shell脚本 vi test.sh #!/bin/bash #指定这个脚本需要什么解释器来执行 echo "Hell ...
- [SCOI 2010]字符串
Description lxhgww最近接到了一个生成字符串的任务,任务需要他把n个1和m个0组成字符串,但是任务还要求在组成的字符串中,在任意的前k个字符中,1的个数不能少于0的个数.现在lxhgw ...
- POJ2449 Remmarguts' Date
"Good man never makes girls wait or breaks an appointment!" said the mandarin duck father. ...
- BZOJ4423 Bytehattan
Description 比特哈顿镇有n*n个格点,形成了一个网格图.一开始整张图是完整的. 有k次操作,每次会删掉图中的一条边(u,v),你需要回答在删除这条边之后u和v是否仍然连通. Input 第 ...
- [Codeforces]856E - Satellites
传送门 做法:每个卫星分别用连到左边圆与x轴交点的线的斜率和连到右边交点的线旋转90度的斜率可以表示成一个区间,问题转化成支持加/删区间和询问其中两个区间是否有交以及它们的交是否被其他区间包含.我一开 ...
- ●UVA 10674 Tangents
题链: https://vjudge.net/problem/UVA-10674 题解: 计算几何,求两个圆的公切线. <算法竞赛入门经典——训练指南>P266,讲得很清楚的. 大致是分为 ...