详解基于LRU缓存的LruCache类及其在Android内存优化中的应用

今天与大家分享一下图片的缓存技术,利用它可以提高UI的流畅性、响应速度,给用户好的体验。

如何在内存中做缓存?

通过内存缓存可以快速加载缓存图片,但会消耗应用的内存空间。LruCache类(通过兼容包可以支持到sdk4)很适合做图片缓存,它通过LinkedHashMap保持图片的强引用方式存储图片,当缓存空间超过设置定的限值时会释放掉早期的缓存。

注:在过去,常用的内存缓存实现是通过SoftReference或WeakReference,但不建议这样做。从Android2.3(API等级9)垃圾收集器开始更积极收集软/弱引用,这使得它们相当无效。此外,在Android 3.0(API等级11)之前,存储在native内存中的可见的bitmap不会被释放,可能会导致应用程序暂时地超过其内存限制并崩溃。

什么是LruCache?
LruCache实现原理是什么?

要回答这个两个问题,先要知道什么是LRU。
LRU是Least Recently Used 的缩写,翻译过来就是“最近最少使用”,LRU缓存就是使用这种原理实现,简单的说就是缓存一定量的数据,当超过设定的阈值时就把一些过期的数据删除掉,比如我们缓存100M的数据,当总数据小于100M时可以随意添加,当超过100M时就需要把新的数据添加进来,同时要把过期数据删除,以确保我们最大缓存100M,那怎么确定删除哪条过期数据呢,采用LRU算法实现的话就是将最老的数据删掉。利用LRU缓存,我们能够提高系统的performance.

LruCache源码分析

LruCache.java是 android.support.v4包引入的一个类,其实现原理就是基于LRU缓存算法,
要想实现LRU缓存,我们首先要用到一个类 LinkedHashMap。 用这个类有两大好处:一是它本身已经实现了按照访问顺序的存储,也就是说,最近读取的会放在最前面,最不常读取的会放在最后(当然,它也可以实现按照插入顺序存储)。第二,LinkedHashMap本身有一个方法用于判断是否需要移除最不常读取的数,但是,原始方法默认不需要移除(这是,LinkedHashMap相当于一个linkedlist),所以,我们需要override这样一个方法,使得当缓存里存放的数据个数超过规定个数后,就把最不常用的移除掉。LinkedHashMap的API写得很清楚,推荐大家可以先读一下。
要基于LinkedHashMap来实现LRU缓存,可以选择inheritance, 也可以选择 delegation, android源码选择的是delegation,而且写得很漂亮。下面,就来剖析一下源码的实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
public class LruCache<K, V> {
//缓存 map 集合,要用LinkedHashMap
private final LinkedHashMap<K, V> map;

private int size; //已经存储的大小
private int maxSize; //规定的最大存储空间

private int putCount; //put的次数
private int createCount; //create的次数
private int evictionCount; //回收的次数
private int hitCount; //命中的次数
private int missCount; //丢失的次数

//实例化 Lru,需要传入缓存的最大值,这个最大值可以是个数,比如对象的个数,也可以是内存的大小
//比如,最大内存只能缓存5兆
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);
}

//重置最大存储空间
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}

synchronized (this) {
this.maxSize = maxSize;
}
trimToSize(maxSize);
}

//通过key返回相应的item,或者创建返回相应的item。相应的item会移动到队列的头部,
// 如果item的value没有被cache或者不能被创建,则返回null。
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++;
}

//如果丢失了就试图创建一个item
V createdValue = create(key);
if (createdValue == null) {
return null;
}

synchronized (this) {
createCount++;
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);
}
}

if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}

//创建cache项,并将创建的项放到队列的头部
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) { //返回的先前的value值
size -= safeSizeOf(key, previous);
}
}

if (previous != null) {
entryRemoved(false, key, previous, value);
}

trimToSize(maxSize);
return previous;
}

//清空cache空间
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);
}
}

//删除key相应的cache项,返回相应的value
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}

V previous;
synchronized (this) {
previous = map.remove(key);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}

if (previous != null) {
entryRemoved(false, key, previous, null);
}

return previous;
}

/**
* Called for entries that have been evicted or removed. This method is
* invoked when a value is evicted to make space, removed by a call to
* {@link #remove}, or replaced by a call to {@link #put}. The default
* implementation does nothing.
* 当item被回收或者删掉时调用。改方法当value被回收释放存储空间时被remove调用,
* 或者替换item值时put调用,默认实现什么都没做
* <p>The method is called without synchronization: other threads may
* access the cache while this method is executing.
*
* @param evicted true if the entry is being removed to make space, false
* if the removal was caused by a {@link #put} or {@link #remove}.
* true---为释放空间被删除;false---put或remove导致
* @param newValue the new value for {@code key}, if it exists. If non-null,
* this removal was caused by a {@link #put}. Otherwise it was caused by
* an eviction or a {@link #remove}.
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

//当某Item丢失时会调用到,返回计算的相应的value或者null
protected V create(K key) {
return null;
}

private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}

//这个方法要特别注意,跟我们实例化LruCache的maxSize要呼应,怎么做到呼应呢,比如maxSize的大小为缓存
//的个数,这里就是return 1就ok,如果是内存的大小,如果5M,这个就不能是个数了,就需要覆盖这个方法,返回每个缓存
//value的size大小,如果是Bitmap,这应该是bitmap.getByteCount();
protected int sizeOf(K key, V value) {
return 1;
}

//清空cacke
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}

/**
* For caches that do not override {@link #sizeOf}, this returns the number
* of entries in the cache. For all other caches, this returns the sum of
* the sizes of the entries in this cache.
*/
public synchronized final int size() {
return size;
}

public synchronized final int maxSize() {
return maxSize;
}

/**
* Returns the number of times {@link #get} returned a value that was
* already present in the cache.
*/
public synchronized final int hitCount() {
return hitCount;
}

/**
* Returns the number of times {@link #get} returned null or required a new
* value to be created.
*/
public synchronized final int missCount() {
return missCount;
}

public synchronized final int createCount() {
return createCount;
}

public synchronized final int putCount() {
return putCount;
}

//返回被回收的数量
public synchronized final int evictionCount() {
return evictionCount;
}

//返回当前cache的副本,从最近最少访问到最多访问
public synchronized final Map<K, V> snapshot() {
return new LinkedHashMap<K, V>(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的缓存机制。
此外,还有一个开源的使用磁盘缓存的方法DiskLruCache,源代码:https://github.com/JakeWharton/DiskLruCache
其详细的使用方法可以参考链接:http://www.tuicool.com/articles/JB7RNj

LruCache应用实例

根据上面的代码,当我们用LruCache来缓存图片时,一定要重写protected int sizeOf(K key, V value) {}方法,否则,最大缓存的是数量而不是占用内存大小。重写protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}方法,在里面处理内存回收。
下面例子继承LruCache,实现相关方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class BitmapCache<K, V extends Bitmap> extends LruCache<K, V>{
private BitmapRemovedCallBack mEnterRemovedCallBack;

public BitmapCache(int maxSize, BitmapRemovedCallBack callBack) {
super(maxSize);
mEnterRemovedCallBack = callBack;
}

//当缓存大于我们设定的最大值时,会调用这个方法,我们在这里面做内存释放操作
@Override
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
if (evicted && oldValue != null){
//在回收bitmap之前,务必要先在下面的回调方法中将bitmap设给的View的bitmapDrawable设为null
//否则,bitmap被回收后,很容易出现cannot draw recycled bitmap的报错。切记!
mEnterRemovedCallBack.onBitmapRemoved(key);
oldValue.recycle();
}
}

//获取每个 value 的大小
@Override
protected int sizeOf(K key, V value) {
int size = 0;
if (value != null) {
size = value.getByteCount();
}
return size;
}

public interface BitmapRemovedCallBack<K>{
void onBitmapRemoved(K key);
}
}

使用BitmapCache时,在构造方法中传入最大缓存量和一个回掉方法就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private BitmapCache<String, Bitmap> mMemoryCache;  

private BitmapCache.BitmapRemovedCallBack<String> mEnteryRemovedCallBack =
new BitmapCache.BitmapRemovedCallBack<String>() {
@Override
public void onBitmapRemoved(String key) {
//处理回收bitmap前,清空相关view的bitmap操作
}
};
Override
protected void onCreate(Bundle savedInstanceState) {
// 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。
// BitmapCache通过构造函数传入缓存值,以bit为单位。
int memClass = ((ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
// 使用单个应用最大可用内存值的1/8作为缓存的大小。
int cacheSize = 1024 * 1024 * memClass / 8;
mMemoryCache = new BitmapCache<String, Bitmap>(cacheSize, mEnteryRemovedCallBack);
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}

public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}

当向 ImageView 中加载一张图片时,首先会在 BitmapCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片:

1
2
3
4
5
6
7
8
9
10
11
public void loadBitmap(int resId, ImageView imageView) { 
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(R.drawable.image_placeholder);
BitmapLoadingTask task = new BitmapLoadingTask(imageView);
task.execute(resId);
}
}

BitmapLoadingTask后台线程,把新加载出来的图片以键值对形式放到缓存中:

1
2
3
4
5
6
7
8
9
10
class BitmapLoadingTask extends AsyncTask<Integer, Void, Bitmap> { 
// 在后台加载图片。
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100);
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
}

总结

1、LruCache 是基于 Lru算法实现的一种缓存机制;
2、Lru算法的原理是把近期最少使用的数据给移除掉,当然前提是当前数据的量大于设定的最大值。
3、LruCache 没有真正的释放内存,只是从 Map中移除掉数据,真正释放内存还是要用户手动释放。
4、手动释放bitmap的内存时,需要先清除相关view中的bitmap。