Skip to content

Commit 4fef5e6

Browse files
committed
修复 LruCache源码解析 的各种问题。
1 parent 7712352 commit 4fef5e6

File tree

1 file changed

+69
-92
lines changed

1 file changed

+69
-92
lines changed

article/LruCache源码解析.md

Lines changed: 69 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,26 @@ LruCache 源码解析
33

44
## 1. 简介
55

6-
> Lru 是 Least Recently Used 最近最少使用算法。
6+
> LRU 是 Least Recently Used 最近最少使用算法。
77
88
>曾经,在各大缓存图片的框架没流行的时候。有一种很常用的内存缓存技术:SoftReference 和 WeakReference(软引用和弱引用)。但是走到了 Android 2.3(Level 9)时代,垃圾回收机制更倾向于回收 SoftReference 或 WeakReference 的对象。后来,又来到了 Android3.0,图片缓存在内容中,因为不知道要在是什么时候释放内存,没有策略,没用一种可以预见的场合去将其释放。这就造成了内存溢出。
99
1010

1111
## 2. 使用方法
1212

13-
**当成一个 Map 用就可以了,只不过实现了 Lru 缓存策略**
13+
**当成一个 Map 用就可以了,只不过实现了 LRU 缓存策略**
1414

1515
使用的时候记住几点即可:
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` 方法
2121

22-
一般做到 **1、2、3就足够了,4可以无视**
22+
一般做到 **1、2、3、4就足够了,5可以无视**
2323

2424

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` 逻辑
2626
```java
2727
private static final float ONE_MIB = 1024 * 1024;
2828
// 7MB
@@ -33,78 +33,48 @@ this.bitmapCache = new LruCache<String, Bitmap>(CACHE_SIZE) {
3333
return value.getByteCount();
3434
}
3535

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-
*/
4436
@Override
4537
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+
...
6639
}
6740
};
6841
```
6942

7043
## 3. 效果展示
7144

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)
7546

76-
**前提:** 设置 LruCache 最大容量为 7MB,把图1、2、3放入了,此时占用容量为:1.87+0.38+2.47=4.47MB。
7747

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. 源码分析
8449

85-
---
50+
### 4.1 LruCache 原理概要解析
8651

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 的数据操作上锁实现的缓存策略**
9153

92-
**执行操作**:再一次 put 图4,发生冲突,拿到 key、冲突 value 以及 put 的 value,这里我放到是同一个 hashcode 的 bitmap,所以 hashcode 一样,但是无关紧要吧。
54+
**LruCache 的数据缓存是内存中的**
9355

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*
9567

96-
LruCache 就是 **利用 LinkedHashMap 的一个特性再加上对 LinkedHashMap 的数据操作上锁实现的缓存策略**
9768

98-
### 4.1 LruCache 的唯一构造方法
69+
### 4.2 LruCache 的唯一构造方法
9970
```java
10071
/**
10172
* LruCache的构造方法:需要传入最大缓存个数
10273
*/
10374
public LruCache(int maxSize) {
104-
// 最大缓存个数小于0,会抛出IllegalArgumentException
105-
if (maxSize <= 0) {
106-
throw new IllegalArgumentException("maxSize <= 0");
107-
}
75+
76+
...
77+
10878
this.maxSize = maxSize;
10979
/*
11080
* 初始化LinkedHashMap
@@ -121,21 +91,20 @@ public LruCache(int maxSize) {
12191

12292
主要是第三个参数 `accessOrder=true`**这样的话 LinkedHashMap 数据排序就会基于数据的访问顺序,从而实现了 LruCache 核心工作原理**
12393

124-
### 4.2 LruCache.get(K key)
94+
### 4.3 LruCache.get(K key)
12595
```java
12696
/**
12797
* 根据 key 查询缓存,如果存在于缓存或者被 create 方法创建了。
128-
* 如果值返回了,那么它将被移动到队列的头部
98+
* 如果值返回了,那么它将被移动到双向循环链表的的尾部
12999
* 如果如果没有缓存的值,则返回 null。
130100
*/
131101
public final V get(K key) {
132-
if (key == null) {
133-
throw new NullPointerException("key == null");
134-
}
135-
102+
103+
...
104+
136105
V mapValue;
137106
synchronized (this) {
138-
// LinkHashMap 如果设置按照访问顺序的话,这里每次get都会重整数据顺序
107+
// 关键点:LinkedHashMap每次get都会基于访问顺序来重整数据顺序
139108
mapValue = map.get(key);
140109
// 计算 命中次数
141110
if (mapValue != null) {
@@ -200,10 +169,10 @@ public final V get(K key) {
200169
}
201170
}
202171
```
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**
204173

205174

206-
### 4.3 LinkedHashMap.get(Object key)
175+
### 4.4 LinkedHashMap.get(Object key)
207176
```java
208177
/**
209178
* Returns the value of the mapping with the specified key.
@@ -243,9 +212,9 @@ public final V get(K key) {
243212
```
244213
其实仔细看 `if (accessOrder)` 的逻辑即可,如果 `accessOrder=true` 那么每次 `get` 都会执行 N 次 `makeTail(LinkedEntry<K, V> e)`
245214

246-
接下来看看
215+
接下来看看
247216

248-
### 4.4 LinkedHashMap.makeTail(LinkedEntry<K, V> e)
217+
### 4.5 LinkedHashMap.makeTail(LinkedEntry<K, V> e)
249218
```java
250219
/**
251220
* Relinks the given entry to the tail of the list. Under access ordering,
@@ -273,17 +242,17 @@ private void makeTail(LinkedEntry<K, V> e) {
273242
*// Relink e as tail*
274243
<img src="http://ww3.sinaimg.cn/large/006lPEc9jw1f36m68rkisj31kw1eswnd.jpg" width="500x"/>
275244

276-
LinkedHashMap是双向循环链表,然后此次 **LruCache.get -> LinkedHashMap.get** 的数据就被放到最末尾了。
245+
LinkedHashMap 是双向循环链表,然后此次 **LruCache.get -> LinkedHashMap.get** 的数据就被放到最末尾了。
277246

278-
**以上就是 LruCache 核心工作原理(。>﹏<。)**
247+
**以上就是 LruCache 核心工作原理**
279248

280249
---
281250

282-
接下来介绍 **LruCache 的容量溢出策略**
251+
接下来介绍 **LruCache 的容量溢出策略**
283252

284-
上述展示场景中,7M的容量,我添加三张图后,不会溢出,put<4>后必然会超过7MB。
285253

286-
### 4.5 LruCache.put(K key, V value)
254+
255+
### 4.6 LruCache.put(K key, V value)
287256
```java
288257
public final V put(K key, V value) {
289258
...
@@ -301,10 +270,10 @@ public final V put(K key, V value) {
301270
记住几点:
302271
- **1.**put 开始的时候确实是把值放入 LinkedHashMap 了,**不管超不超过你设定的缓存容量**
303272
- **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` 然后进行最近很少访问数据的移除
306275

307-
### 4.6 LruCache.trimToSize(int maxSize)
276+
### 4.7 LruCache.trimToSize(int maxSize)
308277
```java
309278
public void trimToSize(int maxSize) {
310279
/*
@@ -344,16 +313,16 @@ public void trimToSize(int maxSize) {
344313
```
345314
简单描述:会判断之前 `size` 是否大于 `maxSize` 。是的话,直接跳出后什么也不做。不是的话,证明已经溢出容量了。由 `makeTail` 图已知,最近经常访问的数据在最末尾。拿到一个存放 key 的 Set,然后一直一直从头开始删除,删一个判断是否溢出,直到没有溢出。
346315

347-
---
316+
---
348317

349-
最后看看
318+
最后看看
350319

351-
### 4.7 覆写 entryRemoved 的作用
320+
### 4.8 覆写 entryRemoved 的作用
352321

353322
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)**
357326
- **4.(get后半段,查询丢失后处理情景,不过建议忽略)** get 的时候,正常的话不实现自定义 `create` 的话,代码上看 get 方法只会走一半,如果你实现了自定义的 `create(K key)` 方法,并且在 你 create 后的值放入 LruCache 中发生 key 冲突时被调用,**evicted=false,key=此次 get 的 key,oldValue=被你自定义 create(key)后的 value,newValue=原本存在 map 里的 key-value**
358327

359328
解释一下第四点吧:**<1>.**第四点是这样的,先 get(key),然后没拿到,丢失。**<2>.**如果你提供了 自定义的 `create(key)` 方法,那么 LruCache 会根据你的逻辑自造一个 value,但是当放入的时候发现冲突了,但是已经放入了。**<3>.**此时,会将那个冲突的值再让回去覆盖,此时调用上述4.的 entryRemoved。
@@ -374,7 +343,16 @@ entryRemoved被LruCache调用的场景:
374343
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
375344
}
376345
```
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+
378356

379357
## 5. 开源项目中的使用
380358

@@ -385,16 +363,15 @@ protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
385363

386364
LruCache重要的几点:
387365

388-
- **1.**LruCache 是通过 LinkedHashMap 构造方法的第三个参数的 `accessOrder=true` 实现了 Lru 算法缓存机制
366+
- **1.**LruCache 是通过 LinkedHashMap 构造方法的第三个参数的 `accessOrder=true` 实现了 `LinkedHashMap` 的数据排序**基于访问顺序** (最近访问的数据会在链表尾部),在容量溢出的时候,将链表头部的数据移除。从而,实现了 LRU 数据缓存机制
389367

390368
- **2.**LruCache 在内部的get、put、remove包括 trimToSize 都是安全的(因为都上锁了)。
391369

392-
- **3.**覆写 `entryRemoved` 方法能知道 LruCache 数据移除是是否发生了冲突。
393-
394-
- **4.**`maxSize``sizeOf(K key, V value)` 方法的覆写息息相关,必须相同单位。( 比如 maxSize 是7MB,自定义的 sizeOf 计算每个数据大小的时候必须能算出与MB之间有联系的单位 )
370+
- **3.**LruCache 自身并没有释放内存,将 LinkedHashMap 的数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存。
395371

396-
- **5.**LruCache 自身并没有释放内存,将 LinkedHashMap 的数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存
372+
- **4.**覆写 `entryRemoved` 方法能知道 LruCache 数据移除是是否发生了冲突,也可以去手动释放资源
397373

374+
- **5.**`maxSize``sizeOf(K key, V value)` 方法的覆写息息相关,必须相同单位。( 比如 maxSize 是7MB,自定义的 sizeOf 计算每个数据大小的时候必须能算出与MB之间有联系的单位 )
398375

399376

400377

@@ -404,7 +381,7 @@ LruCache重要的几点:
404381
[LruCacheActivity](https://github.com/CaMnter/AndroidLife/blob/master/app/src/main/java/com/camnter/newlife/views/activity/lrucache/LruCacheActivity.java)
405382

406383

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)
408385

409386

410387

0 commit comments

Comments
 (0)