@@ -3,26 +3,26 @@ LruCache 源码解析
3
3
4
4
## 1. 简介
5
5
6
- > Lru 是 Least Recently Used 最近最少使用算法。
6
+ > LRU 是 Least Recently Used 最近最少使用算法。
7
7
8
8
> 曾经,在各大缓存图片的框架没流行的时候。有一种很常用的内存缓存技术:SoftReference 和 WeakReference(软引用和弱引用)。但是走到了 Android 2.3(Level 9)时代,垃圾回收机制更倾向于回收 SoftReference 或 WeakReference 的对象。后来,又来到了 Android3.0,图片缓存在内容中,因为不知道要在是什么时候释放内存,没有策略,没用一种可以预见的场合去将其释放。这就造成了内存溢出。
9
9
10
10
11
11
## 2. 使用方法
12
12
13
- ** 当成一个 Map 用就可以了,只不过实现了 Lru 缓存策略**
13
+ ** 当成一个 Map 用就可以了,只不过实现了 LRU 缓存策略** 。
14
14
15
15
使用的时候记住几点即可:
16
- - ** 1.(必填)** 你需要提供一个缓存容量作为构造参数
17
- - ** 2.(个人认为必填 )** 覆写 ` sizeOf ` 方法 ,自定义设计一条数据放进来的容量计算。
18
- - ** 2 .(选填)** 覆写 ` entryRemoved ` 方法 ,你可以知道最少使用的缓存被清除时的数据( evicted, key, oldValue, newVaule )
19
- - ** 3 .(记住)** LruCache是线程安全的,在内部的 get、put、remove 包括 trimToSize 都是安全的(因为都上锁了)。
20
- - ** 4 .(选填)** 还有就是覆写 ` create ` 方法
16
+ - ** 1.(必填)** 你需要提供一个缓存容量作为构造参数。
17
+ - ** 2.(必填 )** 覆写 ` sizeOf ` 方法 ,自定义设计一条数据放进来的容量计算,如果不覆写就无法预知数据的容量,不能保证缓存容量限定在最大容量以内 。
18
+ - ** 3 .(选填)** 覆写 ` entryRemoved ` 方法 ,你可以知道最少使用的缓存被清除时的数据( evicted, key, oldValue, newVaule )。
19
+ - ** 4 .(记住)** LruCache是线程安全的,在内部的 get、put、remove 包括 trimToSize 都是安全的(因为都上锁了)。
20
+ - ** 5 .(选填)** 还有就是覆写 ` create ` 方法 。
21
21
22
- 一般做到 ** 1、2、3就足够了,4可以无视 ** 。
22
+ 一般做到 ** 1、2、3、4就足够了,5可以无视 ** 。
23
23
24
24
25
- 以下是 一个 ** LruCache 实现 Bitmap 小缓存的案例** , ` entryRemoved ` 里的自定义逻辑可以无视,这里是我的展示 demo 里的自定义 ` entryRemoved ` 逻辑(。>﹏<。)
25
+ 以下是 一个 ** LruCache 实现 Bitmap 小缓存的案例** , ` entryRemoved ` 里的自定义逻辑可以无视,想看的可以去到我的我的展示 [ demo] ( https://github.com/CaMnter/AndroidLife/blob/master/app/src/main/java/com/camnter/newlife/views/activity/lrucache/LruCacheActivity.java ) 里的看自定义 ` entryRemoved ` 逻辑。
26
26
``` java
27
27
private static final float ONE_MIB = 1024 * 1024 ;
28
28
// 7MB
@@ -33,78 +33,48 @@ this.bitmapCache = new LruCache<String, Bitmap>(CACHE_SIZE) {
33
33
return value. getByteCount();
34
34
}
35
35
36
-
37
- /**
38
- * 1.当被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用
39
- * 或者替换条目值时put调用,默认实现什么都没做。
40
- * 2.该方法没用同步调用,如果其他线程访问缓存时,该方法也会执行。
41
- * 3.evicted=true:如果该条目被删除空间 (表示 进行了trimToSize or remove) evicted=false:put冲突后 或 get里成功create后 导致
42
- * 4.newValue!=null,那么则被put()或get()调用。
43
- */
44
36
@Override
45
37
protected void entryRemoved (boolean evicted , String key , Bitmap oldValue , Bitmap newValue ) {
46
- mEntryRemovedInfoText. setText(
47
- String . format(Locale . getDefault(), LRU_CACHE_ENTRY_REMOVED_INFO_FORMAT ,
48
- evicted, key, oldValue != null ? oldValue. hashCode() : " null" ,
49
- newValue != null ? newValue. hashCode() : " null" ));
50
- // 见上述 3.
51
- if (evicted) {
52
- // 进行了trimToSize or remove (一般是溢出了 或 key-value被删除了 )
53
- if (recentList. contains(key)) {
54
- recentList. remove(key);
55
- refreshText(mRecentInfoText, LRU_CACHE_RECENT_FORMAT , recentList);
56
- }
57
- } else {
58
- // put冲突后 或 get里成功create 后
59
- recentList. remove(key);
60
- refreshText(mRecentInfoText, LRU_CACHE_RECENT_FORMAT , recentList);
61
- }
62
- if (cacheList. contains(key)) {
63
- cacheList. remove(key);
64
- refreshText(mCacheDataText, LRU_CACHE_CACHE_DATA_FORMAT , cacheList);
65
- }
38
+ ...
66
39
}
67
40
};
68
41
```
69
42
70
43
## 3. 效果展示
71
44
72
- ### 3.1 效果一(验证 Lru,最近没访问的,在溢出时优先被清理)
73
- <img src =" http://ww1.sinaimg.cn/large/006lPEc9jw1f36odh8wjdg31401z4u0x.gif " width =" 320x " />
74
- <img src =" http://ww4.sinaimg.cn/large/006lPEc9jw1f36p56qcjzj31401z4qa7.jpg " width =" 320 " />
45
+ [ LruCache 效果展示] ( https://github.com/CaMnter/AndroidLife/blob/master/article/LruCache%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90_%E6%95%88%E6%9E%9C%E5%B1%95%E7%A4%BA.md )
75
46
76
- ** 前提:** 设置 LruCache 最大容量为 7MB,把图1、2、3放入了,此时占用容量为:1.87+0.38+2.47=4.47MB。
77
47
78
- ** 执行操作** :
79
- - ** 1.** 然后点 get 图3一共16次(** 证明访问次数和Lru没关系,只有访问顺序有关系** )。Recent visit显示了图3
80
- - ** 2.** 先 get 图2,再 get 图1,** 制造最近访问顺序为:<1> <2> <3>**
81
- - ** 3.** put 图4,预算容量需要4.47+2.47=7.19MB。会溢出。
82
- - ** 4.** 溢出了,删除最近没访问的图3。
83
- - ** 5.** 观察 ` entryRemoved ` 数据 图三被移除了(对照hashcode)
48
+ ## 4. 源码分析
84
49
85
- ---
50
+ ### 4.1 LruCache 原理概要解析
86
51
87
- ### 3.2 效果二(验证 entryRemoved 的 evicted=false,可以验证冲突)
88
- <img src =" http://ww3.sinaimg.cn/large/006lPEc9jw1f36oy2uii5g31401z44l6.gif " width =" 320x " /> <img src =" http://ww2.sinaimg.cn/large/006lPEc9jw1f36p6e8t4jj31401z4gt0.jpg " width =" 320x " />
89
-
90
- ** 前提:** 执行了效果一,put 了图4,删除了最近没访问的图3。
52
+ LruCache 就是 ** 利用 LinkedHashMap 的一个特性( accessOrder=true 基于访问顺序 )再加上对 LinkedHashMap 的数据操作上锁实现的缓存策略** 。
91
53
92
- ** 执行操作 ** :再一次 put 图4,发生冲突,拿到 key、冲突 value 以及 put 的 value,这里我放到是同一个 hashcode 的 bitmap,所以 hashcode 一样,但是无关紧要吧。
54
+ ** LruCache 的数据缓存是内存中的 ** 。
93
55
94
- ## 4. 源码分析
56
+ - 1.首先设置了内部 ` LinkedHashMap ` 构造参数 ` accessOrder=true ` , 实现了数据排序按照访问顺序。
57
+
58
+ - 2.然后在每次 ` LruCache.get(K key) ` 方法里都会调用 ` LinkedHashMap.get(Object key) ` 。
59
+
60
+ - 3.如上述设置了 ` accessOrder=true ` 后,每次 ` LinkedHashMap.get(Object key) ` 都会进行 ` LinkedHashMap.makeTail(LinkedEntry<K, V> e) ` 。
61
+
62
+ - 4.` LinkedHashMap ` 是双向循环链表,然后每次 ` LruCache.get ` -> ` LinkedHashMap.get ` 的数据就被放到最末尾了。
63
+
64
+ - 5.在 ` put ` 和 ` trimToSize ` 的方法执行下,如果发成数据量移除了,会优先移除掉最前面的数据(因为最新访问的数据在尾部)。
65
+
66
+ ** 具体解析在:** * 4.2* 、* 4.3* 、* 4.4* 、* 4.5* 。
95
67
96
- LruCache 就是 ** 利用 LinkedHashMap 的一个特性再加上对 LinkedHashMap 的数据操作上锁实现的缓存策略** 。
97
68
98
- ### 4.1 LruCache 的唯一构造方法
69
+ ### 4.2 LruCache 的唯一构造方法
99
70
``` java
100
71
/**
101
72
* LruCache的构造方法:需要传入最大缓存个数
102
73
*/
103
74
public LruCache(int maxSize) {
104
- // 最大缓存个数小于0,会抛出IllegalArgumentException
105
- if (maxSize <= 0 ) {
106
- throw new IllegalArgumentException (" maxSize <= 0" );
107
- }
75
+
76
+ ...
77
+
108
78
this . maxSize = maxSize;
109
79
/*
110
80
* 初始化LinkedHashMap
@@ -121,21 +91,20 @@ public LruCache(int maxSize) {
121
91
122
92
主要是第三个参数 ` accessOrder=true ` ,** 这样的话 LinkedHashMap 数据排序就会基于数据的访问顺序,从而实现了 LruCache 核心工作原理** 。
123
93
124
- ### 4.2 LruCache.get(K key)
94
+ ### 4.3 LruCache.get(K key)
125
95
``` java
126
96
/**
127
97
* 根据 key 查询缓存,如果存在于缓存或者被 create 方法创建了。
128
- * 如果值返回了,那么它将被移动到队列的头部 。
98
+ * 如果值返回了,那么它将被移动到双向循环链表的的尾部 。
129
99
* 如果如果没有缓存的值,则返回 null。
130
100
*/
131
101
public final V get(K key) {
132
- if (key == null ) {
133
- throw new NullPointerException (" key == null" );
134
- }
135
-
102
+
103
+ ...
104
+
136
105
V mapValue;
137
106
synchronized (this ) {
138
- // LinkHashMap 如果设置按照访问顺序的话,这里每次get都会重整数据顺序
107
+ // 关键点:LinkedHashMap每次get都会基于访问顺序来重整数据顺序
139
108
mapValue = map. get(key);
140
109
// 计算 命中次数
141
110
if (mapValue != null ) {
@@ -200,10 +169,10 @@ public final V get(K key) {
200
169
}
201
170
}
202
171
```
203
- 上述的 ` get ` 方法表面并没有看出哪里有实现了Lru的缓存策略 。主要在 ` mapValue = map.get(key) ` ;里,** 调用了 LinkedHashMap 的 get 方法,再加上 LruCache 构造里默认设置 LinkedHashMap 的 accessOrder=true** 。
172
+ 上述的 ` get ` 方法表面并没有看出哪里有实现了 LRU 的缓存策略 。主要在 ` mapValue = map.get(key) ` ;里,** 调用了 LinkedHashMap 的 get 方法,再加上 LruCache 构造里默认设置 LinkedHashMap 的 accessOrder=true** 。
204
173
205
174
206
- ### 4.3 LinkedHashMap.get(Object key)
175
+ ### 4.4 LinkedHashMap.get(Object key)
207
176
``` java
208
177
/**
209
178
* Returns the value of the mapping with the specified key.
@@ -243,9 +212,9 @@ public final V get(K key) {
243
212
```
244
213
其实仔细看 ` if (accessOrder) ` 的逻辑即可,如果 ` accessOrder=true ` 那么每次 ` get ` 都会执行 N 次 ` makeTail(LinkedEntry<K, V> e) ` 。
245
214
246
- 接下来看看
215
+ 接下来看看:
247
216
248
- ### 4.4 LinkedHashMap.makeTail(LinkedEntry<K, V> e)
217
+ ### 4.5 LinkedHashMap.makeTail(LinkedEntry<K, V> e)
249
218
``` java
250
219
/**
251
220
* Relinks the given entry to the tail of the list. Under access ordering,
@@ -273,17 +242,17 @@ private void makeTail(LinkedEntry<K, V> e) {
273
242
* // Relink e as tail*
274
243
<img src =" http://ww3.sinaimg.cn/large/006lPEc9jw1f36m68rkisj31kw1eswnd.jpg " width =" 500x " />
275
244
276
- LinkedHashMap是双向循环链表 ,然后此次 ** LruCache.get -> LinkedHashMap.get** 的数据就被放到最末尾了。
245
+ LinkedHashMap 是双向循环链表 ,然后此次 ** LruCache.get -> LinkedHashMap.get** 的数据就被放到最末尾了。
277
246
278
- ** 以上就是 LruCache 核心工作原理(。>﹏<。) **
247
+ ** 以上就是 LruCache 核心工作原理** 。
279
248
280
249
---
281
250
282
- 接下来介绍 ** LruCache 的容量溢出策略**
251
+ 接下来介绍 ** LruCache 的容量溢出策略** 。
283
252
284
- 上述展示场景中,7M的容量,我添加三张图后,不会溢出,put<4>后必然会超过7MB。
285
253
286
- ### 4.5 LruCache.put(K key, V value)
254
+
255
+ ### 4.6 LruCache.put(K key, V value)
287
256
``` java
288
257
public final V put(K key, V value) {
289
258
...
@@ -301,10 +270,10 @@ public final V put(K key, V value) {
301
270
记住几点:
302
271
- ** 1.** put 开始的时候确实是把值放入 LinkedHashMap 了,** 不管超不超过你设定的缓存容量** 。
303
272
- ** 2.** 然后根据 ` safeSizeOf ` 方法计算 此次添加数据的容量是多少,并且加到 ` size ` 里 。
304
- - ** 3.** 说到 ` safeSizeOf ` 就要讲到 ` sizeOf(K key, V value) ` 会计算出此次添加数据的大小 (像上面的 Demo,我的容量是7MB,我每次添加进来的 Bitmap 要是不覆写 sizeOf 方法的话,会视为该 bitmap 的容量计算为默认的容量计算 return 1。如此一来,这样的话 7MB 的 LruCache 容量可以放7x1024x1024张图片?明显这样的逻辑是不对的!)
305
- - ** 4.** 直到 put 要结束时,进行了 ` trimToSize ` 才判断 ` size ` 是否 大于 ` maxSize ` 然后进行最近很少访问数据的移除
273
+ - ** 3.** 说到 ` safeSizeOf ` 就要讲到 ` sizeOf(K key, V value) ` 会计算出此次添加数据的大小 。
274
+ - ** 4.** 直到 put 要结束时,进行了 ` trimToSize ` 才判断 ` size ` 是否 大于 ` maxSize ` 然后进行最近很少访问数据的移除。
306
275
307
- ### 4.6 LruCache.trimToSize(int maxSize)
276
+ ### 4.7 LruCache.trimToSize(int maxSize)
308
277
``` java
309
278
public void trimToSize(int maxSize) {
310
279
/*
@@ -344,16 +313,16 @@ public void trimToSize(int maxSize) {
344
313
```
345
314
简单描述:会判断之前 ` size ` 是否大于 ` maxSize ` 。是的话,直接跳出后什么也不做。不是的话,证明已经溢出容量了。由 ` makeTail ` 图已知,最近经常访问的数据在最末尾。拿到一个存放 key 的 Set,然后一直一直从头开始删除,删一个判断是否溢出,直到没有溢出。
346
315
347
- ---
316
+ ---
348
317
349
- 最后看看
318
+ 最后看看:
350
319
351
- ### 4.7 覆写 entryRemoved 的作用
320
+ ### 4.8 覆写 entryRemoved 的作用
352
321
353
322
entryRemoved被LruCache调用的场景:
354
- - ** 1.(put)** put 发生 key 冲突时被调用,** evicted=false,key=此次 put 的 key,oldValue=被覆盖的冲突 value,newValue=此次 put 的 value**
355
- - ** 2.(trimToSize)** trimToSize 的时候,只会被调用一次,就是最后一次被删除的最少访问数据带回来。** evicted=true,key=最后一次被删除的 key,oldValue=最后一次被删除的 value,newValue=null(此次没有冲突,只是 remove)**
356
- - ** 3.(remove)** remove的时候,存在对应 key,并且被成功删除后被调用。** evicted=false,key=此次 put的 key,oldValue=此次删除的 value,newValue=null(此次没有冲突,只是 remove)**
323
+ - ** 1.(put)** put 发生 key 冲突时被调用,** evicted=false,key=此次 put 的 key,oldValue=被覆盖的冲突 value,newValue=此次 put 的 value** 。
324
+ - ** 2.(trimToSize)** trimToSize 的时候,只会被调用一次,就是最后一次被删除的最少访问数据带回来。** evicted=true,key=最后一次被删除的 key,oldValue=最后一次被删除的 value,newValue=null(此次没有冲突,只是 remove)** 。
325
+ - ** 3.(remove)** remove的时候,存在对应 key,并且被成功删除后被调用。** evicted=false,key=此次 put的 key,oldValue=此次删除的 value,newValue=null(此次没有冲突,只是 remove)** 。
357
326
- ** 4.(get后半段,查询丢失后处理情景,不过建议忽略)** get 的时候,正常的话不实现自定义 ` create ` 的话,代码上看 get 方法只会走一半,如果你实现了自定义的 ` create(K key) ` 方法,并且在 你 create 后的值放入 LruCache 中发生 key 冲突时被调用,** evicted=false,key=此次 get 的 key,oldValue=被你自定义 create(key)后的 value,newValue=原本存在 map 里的 key-value** 。
358
327
359
328
解释一下第四点吧:** <1>.** 第四点是这样的,先 get(key),然后没拿到,丢失。** <2>.** 如果你提供了 自定义的 ` create(key) ` 方法,那么 LruCache 会根据你的逻辑自造一个 value,但是当放入的时候发现冲突了,但是已经放入了。** <3>.** 此时,会将那个冲突的值再让回去覆盖,此时调用上述4.的 entryRemoved。
@@ -374,7 +343,16 @@ entryRemoved被LruCache调用的场景:
374
343
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
375
344
}
376
345
```
377
- 可以参考我的 demo 里的 ` entryRemoved ` (。>﹏<。)
346
+ 可以参考我的 [ demo] ( https://github.com/CaMnter/AndroidLife/blob/master/app/src/main/java/com/camnter/newlife/views/activity/lrucache/LruCacheActivity.java ) 里的 ` entryRemoved ` 。
347
+
348
+ ### 4.9 LruCache 局部同步锁
349
+
350
+ 在 ` get ` , ` put ` , ` trimToSize ` , ` remove ` 四个方法里的 ` entryRemoved ` 方法都不在同步块里。因为 ` entryRemoved ` 回调的参数都属于方法域参数,不会线程不安全。
351
+
352
+ > 本地方法栈和程序计数器是线程隔离的数据区
353
+
354
+
355
+
378
356
379
357
## 5. 开源项目中的使用
380
358
@@ -385,16 +363,15 @@ protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
385
363
386
364
LruCache重要的几点:
387
365
388
- - ** 1.** LruCache 是通过 LinkedHashMap 构造方法的第三个参数的 ` accessOrder=true ` 实现了 Lru 算法缓存机制 。
366
+ - ** 1.** LruCache 是通过 LinkedHashMap 构造方法的第三个参数的 ` accessOrder=true ` 实现了 ` LinkedHashMap ` 的数据排序 ** 基于访问顺序 ** (最近访问的数据会在链表尾部),在容量溢出的时候,将链表头部的数据移除。从而,实现了 LRU 数据缓存机制 。
389
367
390
368
- ** 2.** LruCache 在内部的get、put、remove包括 trimToSize 都是安全的(因为都上锁了)。
391
369
392
- - ** 3.** 覆写 ` entryRemoved ` 方法能知道 LruCache 数据移除是是否发生了冲突。
393
-
394
- - ** 4.** ` maxSize ` 和 ` sizeOf(K key, V value) ` 方法的覆写息息相关,必须相同单位。( 比如 maxSize 是7MB,自定义的 sizeOf 计算每个数据大小的时候必须能算出与MB之间有联系的单位 )
370
+ - ** 3.** LruCache 自身并没有释放内存,将 LinkedHashMap 的数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存。
395
371
396
- - ** 5 .** LruCache 自身并没有释放内存,将 LinkedHashMap 的数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存 。
372
+ - ** 4 .** 覆写 ` entryRemoved ` 方法能知道 LruCache 数据移除是是否发生了冲突,也可以去手动释放资源 。
397
373
374
+ - ** 5.** ` maxSize ` 和 ` sizeOf(K key, V value) ` 方法的覆写息息相关,必须相同单位。( 比如 maxSize 是7MB,自定义的 sizeOf 计算每个数据大小的时候必须能算出与MB之间有联系的单位 )
398
375
399
376
400
377
@@ -404,7 +381,7 @@ LruCache重要的几点:
404
381
[ LruCacheActivity] ( https://github.com/CaMnter/AndroidLife/blob/master/app/src/main/java/com/camnter/newlife/views/activity/lrucache/LruCacheActivity.java )
405
382
406
383
407
- [ LruCache注释源码 ] ( https://github.com/CaMnter/AndroidLife/blob/master/app/src/main/java/com/camnter/newlife/utils/cache/LruCache.java )
384
+ [ LruCache 注释源码 ] ( https://github.com/CaMnter/AndroidLife/blob/master/app/src/main/java/com/camnter/newlife/utils/cache/LruCache.java )
408
385
409
386
410
387
0 commit comments