在深圳出差,非常忙,抽空写文章,这些文章的质量很可能不高,但还是希望可以帮到你。
在加载图片的时候,我们要考虑到内存问题,(内存缓存作为最先被读取的数据,应该存储那些经常使用的数据对象,且内存容量有限,内存缓存的容量应该限定。)如果你加载是高清无码大图很可能会造成OOM,那我们需要一个东西来管理这个图片与其缓存。
今天我们来讲一下LruCache的原理及实现,这个谷歌推荐的内存缓存的方法。
那么Lru是什么?
LRU全称为Least Recently Used,即最近最少使用,是一种缓存置换算法。(多加一句LFU(least frequently used )算法,则淘汰的是最不经常使用的)。
问题:当有新的内容需要加入我们的缓存,但我们的缓存空闲的空间不足以放进新的内容时,如何舍弃原有的部分内容从而腾出空间用来放新的内容。
让我们进入一下LruCache的学习
首先让我们看一下使用方法中的初始化MemoryCache:
public class BitmapMemoryCache {
private static final String TAG = "BitmapMemoryCache";
private static BitmapMemoryCache sInstance = new BitmapMemoryCache();
private LruCache<String, Bitmap> mMemoryCache;
public Map<String, SoftReference<Bitmap>> mImageCacheMap = new HashMap<String, SoftReference<Bitmap>>();
/*单例模式*/
public BitmapMemoryCache getInstance() {
return BitmapMemoryCache.sInstance;
}
private BitmapMemoryCache() {
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//获取系统分配给应用的总内存大小,不是获取系统全部的的
int cacheSize = maxMemory / 8;//设置图片内存缓存占用八分之一,要依据你申请下来的和你估算使用的大小来
Log.e(TAG, "" + cacheSize);
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 重写此方法来衡量每张图片的大小,默认返回图片数量。
Log.w(TAG, "addBitmapTo " + (bitmap.getByteCount() / 1024));
return bitmap.getByteCount() / 1024;
}
};
}
}
接着让我们进入LruCache源码
明显的可以看出LruCache是利用了LinkedHashMap
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
/** Size of this cache in units. Not necessarily the number of elements. */
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方法未获取到缓存中元素的次数
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other caches,
* this is the maximum sum of the sizes of the entries in this cache.
*/
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);
}
/**
* Sets the size of the cache.
*//设置缓存大小
* @param maxSize The new maximum size.
*/
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
synchronized (this) {
this.maxSize = maxSize;
}
trimToSize(maxSize);
}
}
走到这里我们有个疑问—LinkedHashMap是什么?它是怎么实现LRU这种缓存策略的?
看文中的一句代码:
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
让我们进入LinkedHashMap源码来看一下。
进入构造方法查看。
/**
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
参数说明:
- initialCapacity 初始容量大小,使用无参构造方法时,此值默认是4(安卓SdkVersion 24中默认4,这里是使用了父类HashMap的默认值)
- loadFactor 加载因子,使用无参构造方法时,此值默认是 0.75f(安卓SdkVersion 24中默认0.75,这里是使用了父类HashMap的默认值)
- accessOrder false: 基于插入顺序 true: 基于访问顺序
LinkedHashMap继承自HashMap,不同的是,它是一个双向循环链表,它的每一个数据结点都有两个指针,分别指向直接前驱和直接后继,这一个我们可以从它的内部类LinkedEntry中看出,其定义如下:
/**
private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
// These fields comprise the doubly linked list used for iteration.
LinkedHashMapEntry<K,V> before, after;
//一个双向循环链表,它的每一个数据结点都有两个指针,
//分别指向直接前驱和直接后继
LinkedHashMapEntry(int hash, K key, V value, HashMapEntry<K,V> next) {
super(hash, key, value, next);
}
private void remove() {
before.after = after;
after.before = before;
}
// Inserts this entry before the specified existing entry in the list.
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
void recordRemoval(HashMap<K,V> m) {
remove();
}
}
LinkedHashMap实现了双向循环链表的数据结构。
1,当链表不为空时,header.after指向第一个结点,header.before指向最后一个结点;
2,当链表为空时,header.after与header.before都指向它本身。
@Override
void init() {
header = new LinkedHashMapEntry<>(-1, null, null,null);
header.before = header.after = header;
}
accessOrder是指定它的排序方式,当它为false时,只按插入的顺序排序,即新放入的顺序会在链表的尾部;而当它为true时,更新或访问某个节点的数据时,这个对应的结点也会被放到尾部。
它通过构造方法public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)来赋值。
我们加入一个新结点来看一下方法执行过程(在LinkedHashMap中):
void addEntry(int hash, K key, V value, int bucketIndex) {
// Previous Android releases called removeEldestEntry() before actually
// inserting a value but after increasing the size.
// The RI is documented to call it afterwards.
// **** THIS CHANGE WILL BE REVERTED IN A FUTURE ANDROID RELEASE ****
//这个地方我专门去看了24和25,他并没有改,不知什么原因,欺骗我的感情
// Remove eldest entry if instructed 如果得到通知,移除最旧的
LinkedHashMapEntry<K,V> eldest = header.after;
if (eldest != header) {
boolean removeEldest;
size++;
try {
removeEldest = removeEldestEntry(eldest);
} finally {
size--;
}
if (removeEldest) {
removeEntryForKey(eldest.key);
}
}
super.addEntry(hash, key, value, bucketIndex);//调用父类的添加方法
}
好,我们来看一下父类的添加方法:
想要理解HashMap可以看我的这篇HashMap源码解析
//父类的这个方法也是将新添加的放入尾部,这里既是链表的尾部
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> e = table[bucketIndex];
table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
size++;
}
当我们加入新的元素之后,链表的顺序如图:
那么当我们访问了或者是更新了某个元素(当accessOrder为true时),链表里的元素位置怎么变化呢?
让我们来看一下get(Object key)方法的流程:
public V get(Object key) {
LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);//获取LinkedHashMapEntry
if (e == null)
return null;
e.recordAccess(this);//此方法记录下来,并重新排序
return e.value;
}
进入recordAccess()查看:
这个方法是在LinkedHashMapEntry内部:
//从这段代码中我们看到,首先执行remove,在执行addBefore
LinkedHashMapEntry<K,V> before, after;
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
private void remove() {//这是将此节点取出,如图一
before.after = after;
after.before = before;
//这有点绕,按照我的图片来捋一遍
// node1.after=node3;
// node3.before=node1
}
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {//此处将header传入,将操作的节点放置链表末尾
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
图一:
是不是理解了LinkedHashMap的排序原理了?
熟悉了LinkedHashMap,让我们来分析LruCache;
我们发现,通过它来实现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++;//获取到缓存中的元素的次数+1,在文章头部有这几个参数的介绍
return mapValue;
}
missCount++;//未获取到缓存中的元素的次数+1
}
V createdValue = create(key);
if (createdValue == null) {
return null;//如果没有为key创建新值成功,则直接返回null
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
//create调用次数+1 将创建的值放入map中,如果map在前面的过程中正好放入了这对key-value,那么会返回放入的value
if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
/如果不为null,说明不需要我们所创建的值,所以把返回的值放进去
} else {
size += safeSizeOf(key, createdValue);
//为null,说明我们更新了这个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,则会移除最老的结点,直到不超过。
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.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);
}
}
接下来,我们再来看LruCach的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)修整缓存的大小。
文末附上我以前写的一个管理类
/**
* Bitmap缓存,简单缓存.
* Created by ChangMingShan on 2015/12/26.
*/
public class BitmapMemoryCache {
private static final String TAG = "BitmapMemoryCache";
private static BitmapMemoryCache sInstance = new BitmapMemoryCache();
private LruCache<String, Bitmap> mMemoryCache;
/**
* 单例模式.
*/
public static BitmapMemoryCache getInstance() {
return BitmapMemoryCache.sInstance;
}
private BitmapMemoryCache() {
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 重写此方法来衡量每张图片的大小,默认返回图片数量。
return bitmap.getByteCount() / 1024;
}
};
}
public synchronized void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (mMemoryCache.get(key) == null) {
if (key != null && bitmap != null)
mMemoryCache.put(key, bitmap);
} else
Log.w(TAG, "the res is aready exits");
}
public synchronized Bitmap getBitmapFromMemCache(String key) {
Bitmap bm = mMemoryCache.get(key);
if (key != null) {
return bm;
}
return null;
}
/**
* 移除缓存
*
* @param key
*/
public synchronized void removeImageCache(String key) {
if (key != null) {
if (mMemoryCache != null) {
Bitmap bm = mMemoryCache.remove(key);
if (bm != null)
bm.recycle();
}
}
}
/**
* 移除缓存
*/
public synchronized void clearImageCache() {
if (mMemoryCache != null) {
if (mMemoryCache.size() > 0) {
Log.d("CacheUtils",
"mMemoryCache.size() " + mMemoryCache.size());
mMemoryCache.evictAll();
Log.d("CacheUtils", "mMemoryCache.size()" + mMemoryCache.size());
}
mMemoryCache = null;
}
}
public Bitmap loadLocal(String path) {
Bitmap bitmap=BitmapFactory.decodeFile(path);
addBitmapToMemoryCache(path, bitmap);
return getBitmapFromMemCache(path);
}
public void clearCache() {
if (mMemoryCache != null) {
if (mMemoryCache.size() > 0) {
Log.d("CacheUtils",
"mMemoryCache.size() " + mMemoryCache.size());
mMemoryCache.evictAll();
Log.d("CacheUtils", "mMemoryCache.size()" + mMemoryCache.size());
}
mMemoryCache = null;
}
}
/*
将图片进行压缩
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 设置了此属性一定要记得将值设置为false
Bitmap bitmap = null;
bitmap = BitmapFactory.decodeFile(url, options);
int be = (int) ((options.outHeight > options.outWidth ? options.outHeight / 150
: options.outWidth / 200));
if (be <= 0) // 判断200是否超过原始图片高度
be = 1; // 如果超过,则不进行缩放
options.inSampleSize = be;
options.inPreferredConfig = Bitmap.Config.ARGB_4444;
options.inPurgeable = true;
options.inInputShareable = true;
options.inJustDecodeBounds = false;
try {
bitmap = BitmapFactory.decodeFile(url, options);
} catch (OutOfMemoryError e) {
System.gc();
Log.e(TAG, "OutOfMemoryError");
}
*/
}
结语
通过上面的分析,我们了解到LruCache是通过LinkedHashMap来实现,使用LRU算法。
LruCache是对LRU策略的内存缓存的实现,后来的系统源码中也曾经加上该算法的磁盘缓存的实现,也有对应磁盘缓存的源码DiskLruCache.Java。有兴趣的可以自己去看一下。
本篇文章是个人的理解,如有错误请指出。欢迎大家一起交流!