1、先推荐一个轻量级缓存框架——ACache(ASimpleCache)
ACache介绍:ACache类似于SharedPreferences,但是比SharedPreferences功能更加强大,SharedPreferences只能保存一些基本数据类型、Serializable、Bundle等数据。 而Acache可以缓存如下数据:普通的字符串、JsonObject、JsonArray、Bitmap、Drawable、序列化的java对象,和 byte数据。 主要特色:- 1:轻,轻到只有一个JAVA文件。
- 2:可配置,可以配置缓存路径,缓存大小,缓存数量等。
- 3:可以设置缓存超时时间,缓存超时自动失效,并被删除。
- 4:支持多进程。
- 1、替换SharePreference当做配置文件
- 2、可以缓存网络请求数据,比如oschina的android客户端可以缓存http请求的新闻内容,缓存时间假设为1个小时,超时后自动失效,让客户端重新请求新的数据,减少客户端流量,同时减少服务器并发量。
- 3、您来说...
2、Android缓存机制
Android缓存分为内存缓存和文件缓存(磁盘缓存)。在早期,各大图片缓存框架流行之前,常用的内存缓存方式是软引用(SoftReference)和弱引用(WeakReference),如大部分的使用方式:HashMap> imageCache;这种形式。从Android 2.3(Level 9)开始,垃圾回收器更倾向于回收SoftReference或WeakReference对象,这使得SoftReference和WeakReference变得不是那么实用有效。同时,到了Android 3.0(Level 11)之后,图片数据Bitmap被放置到了内存的堆区域,而堆区域的内存是由GC管理的,开发者也就不需要进行图片资源的释放工作,但这也使得图片数据的释放无法预知,增加了造成OOM的可能。因此,在Android3.1以后,Android推出了LruCache这个内存缓存类,LruCache中的对象是强引用的。2.1 内存缓存——LruCache源码分析
2.1.1 LRU
LRU,全称Least Rencetly Used,即最近最少使用,是一种非常常用的置换算法,也即淘汰最长时间未使用的对象。LRU在操作系统中的页面置换算法中广泛使用,我们的内存或缓存空间是有限的,当新加入一个对象时,造成我们的缓存空间不足了,此时就需要根据某种算法对缓存中原有数据进行淘汰货删除,而LRU选择的是将最长时间未使用的对象进行淘汰。2.1.2 LruCache实现原理
根据LRU算法的思想,要实现LRU最核心的是要有一种数据结构能够基于访问顺序来保存缓存中的对象,这样我们就能够很方便的知道哪个对象是最近访问的,哪个对象是最长时间未访问的。LruCache选择的是LinkedHashMap这个数据结构,LinkedHashMap是一个双向循环链表,在构造LinkedHashMap时,通过一个boolean值来指定LinkedHashMap中保存数据的方式,LinkedHashMap的一个构造方法如下:
1
2
3
4
5
6
7
8
9
10
11
|
/*
* 初始化LinkedHashMap
* 第一个参数:initialCapacity,初始大小
* 第二个参数:loadFactor,负载因子=0.75f
* 第三个参数:accessOrder=true,基于访问顺序;accessOrder=false,基于插入顺序
*/
public
LinkedHashMap(
int
initialCapacity,
float
loadFactor,
boolean
accessOrder) {
super
(initialCapacity, loadFactor);
init();
this
.accessOrder = accessOrder;
}
|
2.1.3 LruCache源码分析
在了解了LruCache的核心原理之后,就可以开始分析LruCache的源码了。 (1)关键字段根据上面的分析,首先要有总容量、已使用容量、linkedHashMap这几个关键字段,LruCache中提供了下面三个关键字段:
1
2
3
4
5
6
|
//核心数据结构
private
final
LinkedHashMap<k, v=
""
> map;
// 当前缓存数据所占的大小
private
int
size;
//缓存空间总容量
private
int
maxSize;</k,>
|
1
2
3
4
5
6
7
|
private
static
final
int
CACHE_SIZE =
4
*
1024
*
1024
;
//4Mib
LruCache<string,bitmap> bitmapCache =
new
LruCache<string,bitmap>(CACHE_SIZE){
@Override
protected
int
sizeOf(String key, Bitmap value) {
return
value.getByteCount();
//自定义Bitmap数据大小的计算方式
}
};</string,bitmap></string,bitmap>
|
1
2
3
4
5
6
7
|
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
);
}</k,>
|
1
2
3
4
5
6
7
|
private
int
safeSizeOf(K key, V value) {
int
result = sizeOf(key, value);
if
(result <
0
) {
throw
new
IllegalStateException(
"Negative size: "
+ key +
"="
+ value);
}
return
result;
}
|
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
|
<em>
/**
</em><em> * 给对应key缓存value,并且将该value移动到链表的尾部。
</em><em> */
</em>
public
final
V put(K key, V value) {
if
(key ==
null
|| value ==
null
) {
throw
new
NullPointerException(
"key == null || value == null"
);
}
V previous;
synchronized
(
this
) {
// 记录 put 的次数
putCount++;
// 通过键值对,计算出要保存对象value的大小,并更新当前缓存大小
size += safeSizeOf(key, value);
/*
* 如果 之前存在key,用新的value覆盖原来的数据, 并返回 之前key 的value
* 记录在 previous
*/
previous = map.put(key, value);
// 如果之前存在key,并且之前的value不为null
if
(previous !=
null
) {
// 计算出 之前value的大小,因为前面size已经加上了新的value数据的大小,此时,需要再次更新size,减去原来value的大小
size -= safeSizeOf(key, previous);
}
}
// 如果之前存在key,并且之前的value不为null
if
(previous !=
null
) {
/*
* previous值被剔除了,此次添加的 value 已经作为key的 新值
* 告诉 自定义 的 entryRemoved 方法
*/
entryRemoved(
false
, key, previous, value);
}
//裁剪缓存容量(在当前缓存数据大小超过了总容量maxSize时,才会真正去执行LRU)
trimToSize(maxSize);
return
previous;
}
|
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
|
public
void
trimToSize(
int
maxSize) {
/*
* 循环进行LRU,直到当前所占容量大小没有超过指定的总容量大小
*/
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
;
}
<em>
/**
</em><em> * 执行到这,表示当前缓存数据已超过了总容量,需要执行LRU,即将最近最少使用的数据清除掉,直到数据所占缓存空间没有超标;
</em><em> * 根据前面的原理分析,知道,在链表中,链表的头结点是最近最少使用的数据,因此,最先清除掉链表前面的结点
</em><em> */
</em> 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
);
}
}</k,>
|
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
|
<em>
/**
</em><em> * </em><em>根据</em><em>key</em><em>查询缓存,如果该</em><em>key</em><em>对应的</em><em>value</em><em>存在于缓存,直接返回</em><em>value</em><em>;
</em><em>* </em><em>访问到这个结点时,</em><em>LinkHashMap</em><em>会将它移动到双向循环链表的的尾部。
</em><em>* </em><em>如果如果没有缓存的值,则返回</em><em>null</em><em>。(如果开发者重写了</em><em>create()</em><em>的话,返回创建的</em><em>value</em><em>)
</em><em>*/
</em>
public
final
V get(K key) {
if
(key ==
null
) {
throw
new
NullPointerException(
"key == null"
);
}
V mapValue;
synchronized
(
this
) {
// LinkHashMap 如果设置按照访问顺序的话,这里每次get都会重整数据顺序
mapValue = map.get(key);
// 计算 命中次数
if
(mapValue !=
null
) {
hitCount++;
return
mapValue;
}
// 计算 丢失次数
missCount++;
}
/*
* 官方解释:
* 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时
* 候,用这个key执行了put方法,那么此时就发生了冲突,我们在Map中删除这个创建的值,释放被创建的值,保留put进去的值。
*/
V createdValue = create(key);
if
(createdValue ==
null
) {
return
null
;
}
<em>
/***************************
</em><em> * </em><em>不覆写</em><em>create</em><em>方法走不到下面 </em><em>*
</em><em> ***************************/
</em>
/*
* 正常情况走不到这里
* 走到这里的话 说明 实现了自定义的 create(K key) 逻辑
* 因为默认的 create(K key) 逻辑为null
*/
synchronized
(
this
) {
// 记录 create 的次数
createCount++;
// 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值
mapValue = map.put(key, createdValue);
// 如果之前存在相同key的value,即有冲突。
if
(mapValue !=
null
) {
/*
* 有冲突
* 所以 撤销 刚才的 操作
* 将 之前相同key 的值 重新放回去
*/
map.put(key, mapValue);
}
else
{
// 拿到键值对,计算出在容量中的相对长度,然后加上
size += safeSizeOf(key, createdValue);
}
}
// 如果上面 判断出了 将要放入的值发生冲突
if
(mapValue !=
null
) {
/*
* 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
* 告诉 自定义 的 entryRemoved 方法
*/
entryRemoved(
false
, key, createdValue, mapValue);
return
mapValue;
}
else
{
// 上面 进行了 size += 操作 所以这里要重整长度
trimToSize(maxSize);
return
createdValue;
}
}
|
1
2
3
4
5
6
7
8
9
10
|
<em>
/**
</em><em> * 1.</em><em>当被回收或者删掉时调用。该方法当</em><em>value</em><em>被回收释放存储空间时被</em><em>remove</em><em>调用
</em><em>* </em><em>或者替换条目值时</em><em>put</em><em>调用,默认实现什么都没做。
</em><em>* 2.</em><em>该方法没用同步调用,如果其他线程访问缓存时,该方法也会执行。
</em><em>* 3.evicted=true</em><em>:如果该条目被删除空间 (表示 进行了</em><em>trimToSize or remove</em><em>) </em><em>evicted=false</em><em>:</em><em>put</em><em>冲突后 或 </em><em>get</em><em>里成功</em><em>create</em><em>后
</em><em>* </em><em>导致
</em><em>* 4.newValue!=null</em><em>,那么则被</em><em>put()</em><em>或</em><em>get()</em><em>调用。
</em><em>*/
</em>
protected
void
entryRemoved(
boolean
evicted, K key, V oldValue, V newValue) {
}
|
2.1.4 LruCache的使用
上面就是整个LruCache中比较核心的的原理和方法,对于LruCache的使用者来说,我们其实主要注意下面几个点:(1)在构造LruCache时提供一个总的缓存大小;(2)重写sizeOf方法,对存入map的数据大小进行自定义测量;(3)根据需要,决定是否要重写entryRemoved()方法;(4)使用LruCache提供的put和get方法进行数据的缓存 小结:-
LruCache 自身并没有释放内存,只是 LinkedHashMap中将数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存;
-
覆写
entryRemoved
方法能知道 LruCache 数据移除是是否发生了冲突(冲突是指在map.put()的时候,对应的key中是否存在原来的值),也可以去手动释放资源;
2.2磁盘缓存(文件缓存)——DiskLruCache分析
LruCache是一种内存缓存策略,但是当存在大量图片的时候,我们指定的缓存内存空间可能很快就会用完,这个时候,LruCache就会频繁的进行trimToSize()操作,不断的将最近最少使用的数据移除,当再次需要该数据时,又得从网络上重新加载。为此,Google提供了一种磁盘缓存的解决方案——DiskLruCache(DiskLruCache并没有集成到Android源码中,在Android Doc的例子中有讲解)。2.2.1 DiskLruCache实现原理
我们可以先来直观看一下,使用了DiskLruCache缓存策略的APP,缓存目录中是什么样子,如下图: 可以看到,缓存目录中有一堆文件名很长的文件,这些文件就是我们缓存的一张张图片数据,在最后有一个文件名journal的文件,这个journal文件是DiskLruCache的一个日志文件,即保存着每张缓存图片的操作记录,journal文件正是实现DiskLruCache的核心。看到出现了journal文件,基本可以说明这个APP使用了DiskLruCache缓存策略。根据对LruCache的分析,要实现LRU,最重要的是要有一种数据结构能够基于访问顺序来保存缓存中的对象,LinkedHashMap是一种非常合适的数据结构,为此,DiskLruCache也选择了LinkedHashMap作为维护访问顺序的数据结构,但是,对于DiskLruCache来说,单单LinkedHashMap是不够的,因为我们不能像LruCache一样,直接将数据放置到LinkedHashMap的value中,也就是处于内存当中,在DiskLruCache中,数据是缓存到了本地文件,这里的LinkedHashMap中的value只是保存的是value的一些简要信息Entry,如唯一的文件名称、大小、是否可读等信息,如:
1
|
|
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
|
private
final
class
Entry {
private
final
String key;
<em>
/** Lengths of this entry's files. */
</em>
private
final
long
[] lengths;
<em>
/** True if this entry has ever been published */
</em>
private
boolean
readable;
<em>
/** The ongoing edit or null if this entry is not being edited. */
</em>
private
Editor currentEditor;
<em>
/** The sequence number of the most recently committed edit to this entry. */
</em>
private
long
sequenceNumber;
private
Entry(String key) {
this
.key = key;
this
.lengths =
new
long
[valueCount];
}
public
String getLengths()
throws
IOException {
StringBuilder result =
new
StringBuilder();
for
(
long
size : lengths) {
result.append(
' '
).append(size);
}
return
result.toString();
}
<em>
/**</em>
<em>* Set lengths using decimal numbers like "10123".</em>
<em>*/
</em>
private
void
setLengths(String[] strings)
throws
IOException {
if
(strings.length != valueCount) {
throw
invalidLengths(strings);
}
try
{
for
(
int
i =
0
; i < strings.length; i++) {
lengths[i] = Long.<em>parseLong</em>(strings[i]);
}
}
catch
(NumberFormatException e) {
throw
invalidLengths(strings);
}
}
private
IOException invalidLengths(String[] strings)
throws
IOException {
throw
new
IOException(
"unexpected journal line: "
+ Arrays.<em>toString</em>(strings));
}
public
File getCleanFile(
int
i) {
return
new
File(directory, key +
"."
+ i);
}
public
File getDirtyFile(
int
i) {
return
new
File(directory, key +
"."
+ i +
".tmp"
);
}
}
|
1
2
|
private
final
LinkedHashMap<string, entry=
""
> lruEntries
=
new
LinkedHashMap<string, entry=
""
>(
0
,
0
.75f,
true
);</string,></string,>
|
1
|
|
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
|
public
static
DiskLruCache open(File directory,
int
appVersion,
int
valueCount,
long
maxSize)
throws
IOException {
if
(maxSize <=
0
) {
throw
new
IllegalArgumentException(
"maxSize <= 0"
);
}
if
(valueCount <=
0
) {
throw
new
IllegalArgumentException(
"valueCount <= 0"
);
}
// prefer to pick up where we left off
DiskLruCache cache =
new
DiskLruCache(directory, appVersion, valueCount, maxSize);
if
(cache.journalFile.exists()) {
try
{
cache.readJournal();
cache.processJournal();
cache.journalWriter =
new
BufferedWriter(
new
FileWriter(cache.journalFile,
true
),<em>IO_BUFFER_SIZE</em>);
return
cache;
}
catch
(IOException journalIsCorrupt) {
// System.logW("DiskLruCache " + directory + " is corrupt: "
// + journalIsCorrupt.getMessage() + ", removing");
cache.delete();
}
}
// create a new empty cache
directory.mkdirs();
cache =
new
DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return
cache;
}
|
以一个DIRTY前缀开始的,后面紧跟着缓存图片的key。以DIRTY这个这个前缀开头,意味着这是一条脏数据。每当我们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,但不知结果如何。然后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净了”,调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。
在CLEAN前缀和key后面还有一个数值,代表的是该条缓存数据的大小。
因此,我们可以总结DiskLruCache中的工作流程:
1)初始化:通过open()方法,获取DiskLruCache的实例,在open方法中通过readJournal(); 方法读取journal日志文件,根据journal日志文件信息建立map中的初始数据;然后再调用processJournal();方法对刚刚建立起的map数据进行分析,分析的工作,一个是计算当前有效缓存文件(即被CLEAN的)的大小,一个是清理无用缓存文件;
2)数据缓存与获取缓存:上面的初始化工作完成后,我们就可以在程序中进行数据的缓存功能和获取缓存的功能了;
缓存数据的操作是借助DiskLruCache.Editor这个类完成的,这个类也是不能new的,需要调用DiskLruCache的edit()方法来获取实例,如下所示:
publicEditoredit(Stringkey)throwsIOException在写入完成后,需要进行commit()。如下一个简单示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
new
Thread(
new
Runnable() {
@Override
public
void
run() {
try
{
String key = hashKeyForDisk(imageUrl);
//MD5对url进行加密,这个主要是为了获得统一的16位字符
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
//拿到Editor,往journal日志中写入DIRTY记录
if
(editor !=
null
) {
OutputStream outputStream = editor.newOutputStream(
0
);
if
(downloadUrlToStream(imageUrl, outputStream)) {
//downloadUrlToStream方法为下载图片的方法,并且将输出流放到outputStream
editor.commit();
//完成后记得commit(),成功后,再往journal日志中写入CLEAN记录
}
else
{
editor.abort();
//失败后,要remove缓存文件,往journal文件中写入REMOVE记录
}
}
mDiskLruCache.flush();
//将缓存操作同步到journal日志文件,不一定要在这里就调用
}
catch
(IOException e) {
e.printStackTrace();
}
}
}).start();
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
try
{
String key = hashKeyForDisk(imageUrl);
//MD5对url进行加密,这个主要是为了获得统一的16位字符
//通过get拿到value的Snapshot,里面封装了输入流、key等信息,调用get会向journal文件写入READ为前缀的记录
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if
(snapShot !=
null
) {
InputStream is = snapShot.getInputStream(
0
);
Bitmap bitmap = BitmapFactory.decodeStream(is);
mImage.setImageBitmap(bitmap);
}
}
catch
(IOException e) {
e.printStackTrace();
}
|