LruCache的源码分析已经很多了,看了很多遍,但是自己走一遍分析,才是真正的掌握,将知识转化到自身。
用途
LruCache的用途就是缓存,但是缓存并不能一直无限量的缓存,设备内存始终是有限的,所以缓存需要有一个删除策略。
常见例子
图片加载基本是每个App的必备项了,虽然我们使用都是第三方加载框架,但是加载框架内部也会缓存图片Bitmap对象,也是使用LruCache来缓存,所以我们了解LruCache,对加载框架的缓存逻辑也能知道核心了。
- 启动一个AsyncTask任务,OkHttp加载图片Bitmap,从网络获取前先从LruCache中查询是否有缓存,有则直接返回,无则进行网络请求,请求完毕再保存到LruCache缓存中,下次再去获取则会命中缓存,复用缓存而无需请求。
- 创建图片缓存,缓存大小为设备最大内存容量的8分之一。创建LruCache,我们一般需要复写sizeOf(),LruCache会回调sizeOf()来获取缓存对象占用的内存大小。
还有2个可选复写的方法:
- entryRemoved()为缓存对象被删除时的回调。
- create()为获取不到缓存时调用,我们可以新建缓存对象,但在图片缓存中我们并不需要,返回null或者不复写即可(默认就是返回null),请求完网络后再保存,或者在这里请求再返回,但是不建议,毕竟每个缓存对象的获取方式不同。
/**
* 创建图片缓存
*/
private void createImageCache() {
//取设备内存最大容量的8分之一作为缓存大小
long maxMemorySize = Runtime.getRuntime().maxMemory();
int cacheSize = (int) (maxMemorySize / 8);
mLruCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
//计算Bitmap占用的内存
return getBitmapSize(value);
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
Log.d(TAG, "缓存对象被删除");
}
@Override
protected Bitmap create(String key) {
//get()方法获取不到缓存时调用,我们不返回数据,让去请求网络获取
return super.create(key);
}
};
}
- 新建AsyncTask异步任务,doInBackground()中,先查询缓存是否存在,不存在再让请求网络,再保存到网络,如果缓存存在,则直接复用缓存。
/**
* 兼容获取Bitmap大小
*/
private int getBitmapSize(Bitmap bitmap) {
//API 19
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
return bitmap.getAllocationByteCount();
}
//API 12
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.HONEYCOMB_MR1) {
return bitmap.getByteCount();
}
// Earlier Version
return bitmap.getRowBytes() * bitmap.getHeight();
}
private static class DownloadImageTask extends AsyncTask {
private OkHttpClient mClient;
private Callback mCallback;
/**
* 回调接口
*/
public interface Callback {
/**
* 执行前回调
*/
void onStart();
/**
* 执行后回调
*
* @param bitmap 执行结果
*/
void onFinish(Bitmap bitmap);
/**
* 尝试获取缓存
*/
Bitmap getCache(String key);
}
public DownloadImageTask(OkHttpClient client, Callback callback) {
mClient = client;
mCallback = callback;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
mCallback.onStart();
}
@Override
protected Bitmap doInBackground(String... urls) {
try {
String url = urls[0];
//1.先从内存缓存中找
Bitmap cacheBitmap = mCallback.getCache(url);
if (cacheBitmap != null) {
Log.d(TAG, "缓存命中");
return cacheBitmap;
}
Log.d(TAG, "缓存不命中,请求网络");
//2.缓存找不到,请求网络
Request request = new Request.Builder()
.url(url)
.build();
Call call = mClient.newCall(request);
Response response = call.execute();
return BitmapFactory.decodeStream(response.body().byteStream());
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
mCallback.onFinish(bitmap);
}
}
原理
LruCache的缓存是使用的最近最少使用的策略,当访问元素时,将元素移动到表尾,当缓存容量到达最大值时移除表头元素(最少使用的元素)。
使用Key-Value的方式缓存数据自然想到Map这种键值对的数据结构,而LruCache的策略则使用了LinkedHashMap。
为什么使用LinkedHashMap呢?因为LinkedHashMap的构造方法,有一个accessOrder参数,默认为false,则为按插入顺序排序,true则使用访问顺序排序,访问越多,越排得后。使用LinkedHashMap则天然支持最近最少使用的策略。
构造方法。
- 指定最大容量为参数创建。不允许配置小于等于0.
- 并创建了一个LinkedHashMap,指定最后的accessOrder字段为true,则代表按访问顺序排序,将经常访问的元素放到表尾。
/**
* @param maxSize 最大缓存容量
*/
public LruCache(int maxSize) {
//最大缓存的对象大小,不能小于等于0,否则抛出异常
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
//缓存映射,最后的accessOrder字段设置为true,则按照访问的顺序排序,否则以插入的顺序排序
this.map = new LinkedHashMap(0, 0.75f, true);
}
添加缓存元素。
- key、value都不能为null,否则抛出异常。
- 同步锁,处理并发,safeSizeOf()就是调用到我们的sizeOf()获取缓存对象的占用大小。并将占用大小和已占用大小累加。
- 通过map.put()添加缓存元素。put()方法有一个返回值,意思是如果之前已经有key保存了,则将之前保存的value返回,再覆盖上新的value。
- 如果put()有返回值,则将他的占用大小和累加值相减。
- 由于删除了旧值,所以回调entryRemoved()。
- put()操作改变了缓存,调用trimToSize()调整内存。
/**
* 添加缓存
*
* @param key 缓存Key
* @param value 缓存值
*/
public final V put(K key, V value) {
//不能存储null键和null值
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
//以前的值,只有当map里面已经记录了key,通过put方法就会返回之前缓存的value
V previous;
synchronized (this) {
//插入的数量自增
putCount++;
//将已缓存的对象大小和本次添加的缓存对象的大小相加
size += safeSizeOf(key, value);
//添加缓存
previous = map.put(key, value);
//之前添加过,将大小减回去
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
//由于本次缓存会覆盖之前的值,所以相当于删除之前的值,回调entryRemoved通知元素被删除
if (previous != null) {
entryRemoved(false, key, previous, value);
}
//每次调整缓存,都去判断如果缓存满了,按照LRU最近最少使用策略删除缓存
trimToSize(maxSize);
return previous;
}
修整缓存占用
- 开启一个死循环,不断检查当前占用内存是否大于最大值,否则一直循环删除最近最少使用的对象,直到内存占用小于最大值才停止。
- map遍历,找出表头元素。
- 通过map.remove()移除最近最少使用的元素。
- 由于删除了元素,调用safeSizeOf()调整内存计数。
- 回调entryRemoved()提示删除了元素。
/**
* 整理缓存,如果内存超出,删除表头元素
*/
private 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;
}
//不断获取第一个元素,不断while循环删除,直到内存大小比最大内存大小小才停下
Map.Entry toEvict = map.entrySet().iterator().next();
//如果为null不处理
if (toEvict == null) {
break;
}
//获取本次要删除的元素键值
key = toEvict.getKey();
value = toEvict.getValue();
//移除元素
map.remove(key);
//将删除的元素的内存减去
size -= safeSizeOf(key, value);
//回收次数自增
evictionCount++;
}
//调用entryRemoved()提醒删除了元素
entryRemoved(true, key, value, null);
}
}
获取缓存元素
- 不允许key为null,否则抛出异常。
- 同步锁包保证并发获取。
- 通过map.get(key)获取缓存元素。
- 获取不到缓存元素,则调用create(key)来新建元素。
- create()获取新建的元素不为null,则将元素保存到map,处理就和上面的put()处理流程是一致的。
- 操作完毕,调用trimToSize(),检查是否需要清理内存。
/**
* 获取缓存,会将本次获取的元素放到表尾
*/
public final V get(K key) {
//不允许key为null
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//从map中获取value,由于我们设置了accessOrder为true,所以会将元素移动到表尾
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
//map中获取不到元素,调用create()方法创建一个
V createdValue = create(key);
if (createdValue == null) {
return null;
}
//将对象缓存到map和put()方法是一样的
synchronized (this) {
createCount++;
//put添加缓存,但是如果返回了旧值,则将旧值保存回去(意外)
mapValue = map.put(key, createdValue);
if (mapValue != null) {
//保存原来的值
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
//有旧值,从map中删除掉
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
//每次调整缓存,都去判断如果缓存满了,按照LRU最近最少使用策略删除缓存
trimToSize(maxSize);
return createdValue;
}
}
移除缓存元素
- 同样key不能为null,否则抛出异常。
- 通过map.remove(key)移除元素
- 调用safeSizeOf()计算内存占用大小。
- 调用entryRemoved()通知缓存元素被删除。
/**
* 移除元素
*
* @return 元素Key
*/
public final V remove(K key) {
//不允许key为null
if (key == null) {
throw new NullPointerException("key == null");
}
//移除的元素
V previous;
synchronized (this) {
//从map中移除元素
previous = map.remove(key);
//改变缓存的大小
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
//回调entryRemoved告知元素被删除
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
完整源码和注释
public class LruCache {
private final LinkedHashMap map;
/**
* 当前缓存的对象的总内存
*/
private int size;
/**
* 配置的最大缓存容量
*/
private int maxSize;
/**
* 调用put()方法缓存对象的次数
*/
private int putCount;
/**
* 调用create()方法创建对象的次数
*/
private int createCount;
/**
* 调用trimToSize()方法回收对象的次数
*/
private int evictionCount;
/**
* 取调用get()命中缓存的次数
*/
private int hitCount;
/**
* 调用get()方法不命中的次数
*/
private int missCount;
/**
* @param maxSize 最大缓存容量
*/
public LruCache(int maxSize) {
//最大缓存的对象大小,不能小于等于0,否则抛出异常
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
//缓存映射,最后的accessOrder字段设置为true,则按照访问的顺序排序,否则以插入的顺序排序
this.map = new LinkedHashMap(0, 0.75f, true);
}
/**
* 重新设置最大缓存大小
*
* @param maxSize 新的缓存大小
*/
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) {
//不允许key为null
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//从map中获取value,由于我们设置了accessOrder为true,所以会将元素移动到表尾
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
//map中获取不到元素,调用create()方法创建一个
V createdValue = create(key);
if (createdValue == null) {
return null;
}
//将对象缓存到map和put()方法是一样的
synchronized (this) {
createCount++;
//put添加缓存,但是如果返回了旧值,则将旧值保存回去(意外)
mapValue = map.put(key, createdValue);
if (mapValue != null) {
//保存原来的值
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
//有旧值,从map中删除掉
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
//每次调整缓存,都去判断如果缓存满了,按照LRU最近最少使用策略删除缓存
trimToSize(maxSize);
return createdValue;
}
}
/**
* 添加缓存
*
* @param key 缓存Key
* @param value 缓存值
*/
public final V put(K key, V value) {
//不能存储null键和null值
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
//以前的值,只有当map里面已经记录了key,通过put方法就会返回之前缓存的value
V previous;
synchronized (this) {
//插入的数量自增
putCount++;
//将已缓存的对象大小和本次添加的缓存对象的大小相加
size += safeSizeOf(key, value);
//添加缓存
previous = map.put(key, value);
//之前添加过,将大小减回去
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
//由于本次缓存会覆盖之前的值,所以相当于删除之前的值,回调entryRemoved通知元素被删除
if (previous != null) {
entryRemoved(false, key, previous, value);
}
//每次调整缓存,都去判断如果缓存满了,按照LRU最近最少使用策略删除缓存
trimToSize(maxSize);
return previous;
}
/**
* 整理缓存,如果内存超出,删除表头元素
*/
private 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;
}
//不断获取第一个元素,不断while循环删除,直到内存大小比最大内存大小小才停下
Map.Entry toEvict = map.entrySet().iterator().next();
//如果为null不处理
if (toEvict == null) {
break;
}
//获取本次要删除的元素键值
key = toEvict.getKey();
value = toEvict.getValue();
//移除元素
map.remove(key);
//将删除的元素的内存减去
size -= safeSizeOf(key, value);
//回收次数自增
evictionCount++;
}
//调用entryRemoved()提醒删除了元素
entryRemoved(true, key, value, null);
}
}
/**
* 移除元素
*
* @return 元素Key
*/
public final V remove(K key) {
//不允许key为null
if (key == null) {
throw new NullPointerException("key == null");
}
//移除的元素
V previous;
synchronized (this) {
//从map中移除元素
previous = map.remove(key);
//改变缓存的大小
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
//回调entryRemoved告知元素被删除
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;
}
/**
* 实际就是调用了sizeOf()方法
*/
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
/**
* 返回对象的内存占用大小,默认为,一般我们都会重写
*/
protected int sizeOf(K key, V value) {
return 1;
}
/**
* 清除所有缓存
*/
public final void evictAll() {
//-1代表清除所有元素
trimToSize(-1);
}
/**
* 获取已缓存的对象总大小
*/
public synchronized final int size() {
return size;
}
/**
* 获取配置的对象缓存最大大小
*/
public synchronized final int maxSize() {
return maxSize;
}
/**
* 获取调用get()命中缓存的次数
*/
public synchronized final int hitCount() {
return hitCount;
}
/**
* 获取调用get()方法不命中的次数
*/
public synchronized final int missCount() {
return missCount;
}
/**
* 获取调用create()方法创建对象的次数
*/
public synchronized final int createCount() {
return createCount;
}
/**
* 获取调用put()方法缓存对象的次数
*/
public synchronized final int putCount() {
return putCount;
}
/**
* 获取调用trimToSize()方法回收对象的次数
*/
public synchronized final int evictionCount() {
return evictionCount;
}
/**
* 获取缓存map的一个副本
*/
public synchronized final Map snapshot() {
return new LinkedHashMap(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的最近最少使用的策略是通过LinkedHashMap,将LinkedHashMap的构造方法的accessOrder字段设置true,让LinkedHashMap按访问顺序排序,不断将常用的元素放到表尾,不常用的元素放到表头。每次增、删、改都判断缓存内存是否大于最大缓存值,如果大于则用一个死循环不断将表头元素移除,直到内存小于最大内存。