服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Android - 从源代码分析Android Universal ImageLoader的缓存处理机制

从源代码分析Android Universal ImageLoader的缓存处理机制

2021-05-09 20:33陈哈哈 Android

这篇文章主要介绍了从源代码分析Android Universal ImageLoader的缓存处理机制 的相关资料,需要的朋友可以参考下

通过本文带大家一起看过uil这个国内外大牛都追捧的图片缓存类库的缓存处理机制。看了uil中的缓存实现,才发现其实这个东西不难,没有太多的进程调度,没有各种内存读取控制机制、没有各种异常处理。反正uil中不单代码写的简单,连处理都简单。但是这个类库这么好用,又有这么多人用,那么非常有必要看看他是怎么实现的。先了解uil中缓存流程的原理图。

原理示意图

主体有三个,分别是ui,缓存模块和数据源(网络)。它们之间的关系如下:

从源代码分析Android Universal ImageLoader的缓存处理机制

① ui:请求数据,使用唯一的key值索引memory cache中的bitmap。

② 内存缓存:缓存搜索,如果能找到key值对应的bitmap,则返回数据。否则执行第三步。

③ 硬盘存储:使用唯一key值对应的文件名,检索sdcard上的文件。

④ 如果有对应文件,使用bitmapfactory.decode*方法,解码bitmap并返回数据,同时将数据写入缓存。如果没有对应文件,执行第五步。

⑤ 下载图片:启动异步线程,从数据源下载数据(web)。

⑥ 若下载成功,将数据同时写入硬盘和缓存,并将bitmap显示在ui中。

接下来,我们回顾一下uil中缓存的配置(具体的见《universal image loader.part 2》)。重点关注注释部分,我们可以根据自己需要配置内存、磁盘缓存的实现。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
file cachedir = storageutils.getcachedirectory(context,
"universalimageloader/cache");
imageloaderconfiguration config = new
imageloaderconfiguration .builder(getapplicationcontext())
.maximagewidthformemorycache()
.maximageheightformemorycache()
.httpconnecttimeout()
.httpreadtimeout()
.threadpoolsize()
.threadpriority(thread.min_priority + )
.denycacheimagemultiplesizesinmemory()
.memorycache(new usingfreqlimitedcache()) // 你可以传入自己的内存缓存
.disccache(new unlimiteddisccache(cachedir)) // 你可以传入自己的磁盘缓存
.defaultdisplayimageoptions(displayimageoptions.createsimple())
.build();

uil中的内存缓存策略

1. 只使用的是强引用缓存
•lrumemorycache(这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用,下面我会从源码上面分析这个类)

2.使用强引用和弱引用相结合的缓存有

usingfreqlimitedmemorycache(如果缓存的图片总量超过限定值,先删除使用频率最小的bitmap)
•lrulimitedmemorycache(这个也是使用的lru算法,和lrumemorycache不同的是,他缓存的是bitmap的弱引用)
•fifolimitedmemorycache(先进先出的缓存策略,当超过设定值,先删除最先加入缓存的bitmap)
•largestlimitedmemorycache(当超过缓存限定值,先删除最大的bitmap对象)
•limitedagememorycache(当 bitmap加入缓存中的时间超过我们设定的值,将其删除)

3.只使用弱引用缓存

weakmemorycache(这个类缓存bitmap的总大小没有限制,唯一不足的地方就是不稳定,缓存的图片容易被回收掉)

我们直接选择uil中的默认配置缓存策略进行分析。

imageloaderconfiguration config = imageloaderconfiguration.createdefault(context);
imageloaderconfiguration.createdefault(…)这个方法最后是调用builder.build()方法创建默认的配置参数的。默认的内存缓存实现是lrumemorycache,磁盘缓存是unlimiteddisccache。

lrumemorycache解析

lrumemorycache:一种使用强引用来保存有数量限制的bitmap的cache(在空间有限的情况,保留最近使用过的bitmap)。每次bitmap被访问时,它就被移动到一个队列的头部。当bitmap被添加到一个空间已满的cache时,在队列末尾的bitmap会被挤出去并变成适合被gc回收的状态。
注意:这个cache只使用强引用来保存bitmap。

lrumemorycache实现memorycache,而memorycache继承自memorycacheaware。

public interface memorycache extends memorycacheaware<string, bitmap>

下面给出继承关系图

 

lrumemorycache.get(…)

我相信接下去你看到这段代码的时候会跟我一样惊讶于代码的简单,代码中除了异常判断,就是利用synchronized进行同步控制。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* returns the bitmap for {@code key} if it exists in the cache. if a bitmap was returned, it is moved to the head
* of the queue. this returns null if a bitmap is not cached.
*/
@override
public final bitmap get(string key) {
if (key == null) {
throw new nullpointerexception("key == null");
}
synchronized (this) {
return map.get(key);
}
}

我们会好奇,这不是就简简单单将bitmap从map中取出来吗?但lrumemorycache声称保留在空间有限的情况下保留最近使用过的bitmap。不急,让我们细细观察一下map。他是一个linkedhashmap<string, bitmap>型的对象。

linkedhashmap中的get()方法不仅返回所匹配的值,并且在返回前还会将所匹配的key对应的entry调整在列表中的顺序(linkedhashmap使用双链表来保存数据),让它处于列表的最后。当然,这种情况必须是在linkedhashmap中accessorder==true的情况下才生效的,反之就是get()方法不会改变被匹配的key对应的entry在列表中的位置。

?
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
@override public v get(object key) {
 
 
/*
* this method is overridden to eliminate the need for a polymorphic
* invocation in superclass at the expense of code duplication.
*/
if (key == null) {
hashmapentry<k, v> e = entryfornullkey;
if (e == null)
return null;
if (accessorder)
maketail((linkedentry<k, v>) e);
return e.value;
}
// replace with collections.secondaryhash when the vm is fast enough (http://b/).
int hash = secondaryhash(key);
hashmapentry<k, v>[] tab = table;
for (hashmapentry<k, v> e = tab[hash & (tab.length - )];
e != null; e = e.next) {
k ekey = e.key;
if (ekey == key || (e.hash == hash && key.equals(ekey))) {
if (accessorder)
maketail((linkedentry<k, v>) e);
return e.value;
}
}
return null;
}

代码第11行的maketail()就是调整entry在列表中的位置,其实就是双向链表的调整。它判断accessorder。到现在我们就清楚lrumemorycache使用linkedhashmap来缓存数据,在linkedhashmap.get()方法执行后,linkedhashmap中entry的顺序会得到调整。那么我们怎么保证最近使用的项不会被剔除呢?接下去,让我们看看lrumemorycache.put(...)。

lrumemorycache.put(...)

注意到代码第8行中的size+= sizeof(key, value),这个size是什么呢?我们注意到在第19行有一个trimtosize(maxsize),trimtosize(...)这个函数就是用来限定lrumemorycache的大小不要超过用户限定的大小,cache的大小由用户在lrumemorycache刚开始初始化的时候限定。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@override
public final boolean put(string key, bitmap value) {
if (key == null || value == null) {
throw new nullpointerexception("key == null || value == null");
}
synchronized (this) {
size += sizeof(key, value);
//map.put()的返回值如果不为空,说明存在跟key对应的entry,put操作只是更新原有key对应的entry
bitmap previous = map.put(key, value);
if (previous != null) {
size -= sizeof(key, previous);
}
}
trimtosize(maxsize);
return true;
}

其实不难想到,当bitmap缓存的大小超过原来设定的maxsize时应该是在trimtosize(...)这个函数中做到的。这个函数做的事情也简单,遍历map,将多余的项(代码中对应toevict)剔除掉,直到当前cache的大小等于或小于限定的大小。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void trimtosize(int maxsize) {
while (true) {
string key;
bitmap value;
synchronized (this) {
if (size < || (map.isempty() && size != )) {
throw new illegalstateexception(getclass().getname() + ".sizeof() is reporting inconsistent results!");
}
if (size <= maxsize || map.isempty()) {
break;
}
map.entry<string, bitmap> toevict = map.entryset().iterator().next();
if (toevict == null) {
break;
}
key = toevict.getkey();
value = toevict.getvalue();
map.remove(key);
size -= sizeof(key, value);
}
}
}

这时候我们会有一个以为,为什么遍历一下就可以将使用最少的bitmap缓存给剔除,不会误删到最近使用的bitmap缓存吗?首先,我们要清楚,lrumemorycache定义的最近使用是指最近用get或put方式操作到的bitmap缓存。其次,之前我们直到lrumemorycache的get操作其实是通过其内部字段linkedhashmap.get(...)实现的,当linkedhashmap的accessorder==true时,每一次get或put操作都会将所操作项(图中第3项)移动到链表的尾部(见下图,链表头被认为是最少使用的,链表尾被认为是最常使用的。),每一次操作到的项我们都认为它是最近使用过的,当内存不够的时候被剔除的优先级最低。需要注意的是一开始的linkedhashmap链表是按插入的顺序构成的,也就是第一个插入的项就在链表头,最后一个插入的就在链表尾。假设只要剔除图中的1,2项就能让lrumemorycache小于原先限定的大小,那么我们只要从链表头遍历下去(从1→最后一项)那么就可以剔除使用最少的项了。

从源代码分析Android Universal ImageLoader的缓存处理机制从源代码分析Android Universal ImageLoader的缓存处理机制

至此,我们就知道了lrumemorycache缓存的整个原理,包括他怎么put、get、剔除一个元素的的策略。接下去,我们要开始分析默认的磁盘缓存策略了。

uil中的磁盘缓存策略

像新浪微博、花瓣这种应用需要加载很多图片,本来图片的加载就慢了,如果下次打开的时候还需要再一次下载上次已经有过的图片,相信用户的流量会让他们的叫骂声很响亮。对于图片很多的应用,一个好的磁盘缓存直接决定了应用在用户手机的留存时间。我们自己实现磁盘缓存,要考虑的太多,幸好uil提供了几种常见的磁盘缓存策略,当然如果你觉得都不符合你的要求,你也可以自己去扩展

•filecountlimiteddisccache(可以设定缓存图片的个数,当超过设定值,删除掉最先加入到硬盘的文件)
•limitedagedisccache(设定文件存活的最长时间,当超过这个值,就删除该文件)
•totalsizelimiteddisccache(设定缓存bitmap的最大值,当超过这个值,删除最先加入到硬盘的文件)
•unlimiteddisccache(这个缓存类没有任何的限制)

在uil中有着比较完整的存储策略,根据预先指定的空间大小,使用频率(生命周期),文件个数的约束条件,都有着对应的实现策略。最基础的接口disccacheaware和抽象类basedisccache

unlimiteddisccache解析

unlimiteddisccache实现disk cache接口,是imageloaderconfiguration中默认的磁盘缓存处理。用它的时候,磁盘缓存的大小是不受限的。

接下来我们来看看实现unlimiteddisccache的源代码,通过源代码我们发现他其实就是继承了basedisccache,这个类内部没有实现自己独特的方法,也没有重写什么,那么我们就直接看basedisccache这个类。在分析这个类之前,我们先想想自己实现一个磁盘缓存需要做多少麻烦的事情:

1、图片的命名会不会重。你没有办法知道用户下载的图片原始的文件名是怎么样的,因此很可能因为文件重名将有用的图片给覆盖掉了。

2、当应用卡顿或网络延迟的时候,同一张图片反复被下载。

3、处理图片写入磁盘可能遇到的延迟和同步问题。

basedisccache构造函数

首先,我们看一下basedisccache的构造函数:

cachedir:文件缓存目录

reservecachedir:备用的文件缓存目录,可以为null。它只有当cachedir不能用的时候才有用。
filenamegenerator:文件名生成器。为缓存的文件生成文件名。

?
1
2
3
4
5
6
7
8
9
10
11
public basedisccache(file cachedir, file reservecachedir, filenamegenerator filenamegenerator) {
if (cachedir == null) {
throw new illegalargumentexception("cachedir" + error_arg_null);
}
if (filenamegenerator == null) {
throw new illegalargumentexception("filenamegenerator" + error_arg_null);
}
this.cachedir = cachedir;
this.reservecachedir = reservecachedir;
this.filenamegenerator = filenamegenerator;
}

我们可以看到一个filenamegenerator,接下来我们来了解uil具体是怎么生成不重复的文件名的。uil中有3种文件命名策略,这里我们只对默认的文件名策略进行分析。默认的文件命名策略在defaultconfigurationfactory.createfilenamegenerator()。它是一个hashcodefilenamegenerator。真的是你意想不到的简单,就是运用string.hashcode()进行文件名的生成。

?
1
2
3
4
5
6
public class hashcodefilenamegenerator implements filenamegenerator {
@override
public string generate(string imageuri) {
return string.valueof(imageuri.hashcode());
}
}

basedisccache.save()

分析完了命名策略,再看一下basedisccache.save(...)方法。注意到第2行有一个getfile()函数,它主要用于生成一个指向缓存目录中的文件,在这个函数里面调用了刚刚介绍过的filenamegenerator来生成文件名。注意第3行的tmpfile,它是用来写入bitmap的临时文件(见第8行),然后就把这个文件给删除了。大家可能会困惑,为什么在save()函数里面没有判断要写入的bitmap文件是否存在的判断,我们不由得要看看uil中是否有对它进行判断。还记得我们在《从代码分析android-universal-image-loader的图片加载、显示流程》介绍的,uil加载图片的一般流程是先判断内存中是否有对应的bitmap,再判断磁盘(disk)中是否有,如果没有就从网络中加载。最后根据原先在uil中的配置判断是否需要缓存bitmap到内存或磁盘中。也就是说,当需要调用basedisccache.save(...)之前,其实已经判断过这个文件不在磁盘中。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public boolean save(string imageuri, inputstream imagestream, ioutils.copylistener listener) throws ioexception {
file imagefile = getfile(imageuri);
file tmpfile = new file(imagefile.getabsolutepath() + temp_image_postfix);
boolean loaded = false;
try {
outputstream os = new bufferedoutputstream(new fileoutputstream(tmpfile), buffersize);
try {
loaded = ioutils.copystream(imagestream, os, listener, buffersize);
} finally {
ioutils.closesilently(os);
}
} finally {
ioutils.closesilently(imagestream);
if (loaded && !tmpfile.renameto(imagefile)) {
loaded = false;
}
if (!loaded) {
tmpfile.delete();
}
}
return loaded;
}

basedisccache.get()

basedisccache.get()方法内部调用了basedisccache.getfile(...)方法,让我们来分析一下这个在之前碰过的函数。 第2行就是利用filenamegenerator生成一个唯一的文件名。第3~8行是指定缓存目录,这时候你就可以清楚地看到cachedir和reservecachedir之间的关系了,当cachedir不可用的时候,就是用reservecachedir作为缓存目录了。

最后返回一个指向文件的对象,但是要注意当file类型的对象指向的文件不存在时,file会为null,而不是报错。

?
1
2
3
4
5
6
7
8
9
10
protected file getfile(string imageuri) {
string filename = filenamegenerator.generate(imageuri);
file dir = cachedir;
if (!cachedir.exists() && !cachedir.mkdirs()) {
if (reservecachedir != null && (reservecachedir.exists() || reservecachedir.mkdirs())) {
dir = reservecachedir;
}
}
return new file(dir, filename);
}

总结

现在,我们已经分析了uil的缓存机制。其实从uil的缓存机制的实现并不是很复杂,虽然有各种缓存机制,但是简单地说:内存缓存其实就是利用map接口的对象在内存中进行缓存,可能有不同的存储机制。磁盘缓存其实就是将文件写入磁盘。

延伸 · 阅读

精彩推荐