您的位置:首页 > 移动开发 > Android开发

Android内存缓存管理LruCache源码解析与示例

2017-04-21 15:16 871 查看
在深圳出差,非常忙,抽空写文章,这些文章的质量很可能不高,但还是希望可以帮到你。

在加载图片的时候,我们要考虑到内存问题,(内存缓存作为最先被读取的数据,应该存储那些经常使用的数据对象,且内存容量有限,内存缓存的容量应该限定。)如果你加载是高清无码大图很可能会造成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。有兴趣的可以自己去看一下。

本篇文章是个人的理解,如有错误请指出。欢迎大家一起交流!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息