Android 图片加载之深入 Universal Image Loader 源码探究

文章目录
  1. 1. displayImage()
  2. 2. 缓存策略
  3. 3. 参考文献

用勇气改变可以改变的事情,用胸怀接受不能改变的事情,用智慧分辨两者的不同。

文首分享的是一句我已用了 4 年多的 QQ 签名,回首经历的过往,总是自觉字字饶有趣味。好,上期 Android 图片加载之浅谈 Universal Image Loader 知道了 UIL 的简单使用,本期就从源码角度重点探究下这一经典的图片加载框架。

displayImage()

结合上期文章,其实,大部分时候都是调用该方法加载图片,进去看下:

1
2
3
public void displayImage(String uri, ImageView imageView, DisplayImageOptions options) {
displayImage(uri, new ImageViewAware(imageView), options, null, null);
}

将 ImageView 转换成 ImageViewAware,进去细看该类,是对 ImageView 的包装,将 ImageView 的强引用转换成弱引用,内存不足时,则回收 ImageView 对象,即持有 ImageView 的弱引用以防止内存泄漏。此外,该类中做了获取 ImageView 宽度和高度的处理,再对图片作适度的裁剪,以节省内存。

流程框图如下:

进入 displayImage() 方法细节中,分段来看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 检查 ImageLoader 的配置是否初始化
checkConfiguration();
if (imageAware == null) {
throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
}
if (listener == null) {
listener = defaultListener;
}
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}

if (TextUtils.isEmpty(uri)) {
// 取消加载和展示 imageAware
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
if (options.shouldShowImageForEmptyUri()) {
imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
} else {
imageAware.setImageDrawable(null);
}
listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
return;
}

engine 即 ImageLoaderEngine,为任务分发器,负责分发LoadAndDisplayImageTaskProcessAndDisplayImageTask给具体的线程池执行,其有一个 HashMap,记录正在加载的任务。加载图片时,将给定 ImageView 的 id、图片的 uri 和尺寸放置到 HashMap 中,加载完成后 remove 掉,而后将相应的图片资源设置给 ImageView 显示,最后回调 onLoadingComplete() 方法告知完成任务。看下一段:

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
if (targetSize == null) {
// ImageSize 内部封装好 ImageView 的宽高,指定的 size 为目标参数、配置参数或设备显示尺寸
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}

String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);

listener.onLoadingStarted(uri, imageAware.getWrappedView());

// 从内存缓存中获取 Bitmap 对象,默认使用 LruMemoryCache
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
// postProcessor 不为 null 时为 true,其表现是一个 BitmapProcessor 接口,其实现
// 是线程安全的,可以对原始的 Bitmap 作一些改造
if (options.shouldPostProcess()) {
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
} else {
// getDisplayer() 得到的是一个 BitmapDisplayer 接口,其几个实现类有着不同的功能
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
}

重点地方作了注释,看最后一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (options.shouldShowImageOnLoading()) {
imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
} else if (options.isResetViewBeforeLoading()) {
imageAware.setImageDrawable(null);
}

ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}

此段代码针对 Bitmap 不在内存中缓存,从网络中或文件系统里获取 Bitmap 对象,实例化 LoadAndDisplayImageTask。若 isSyncLoading 为 true,则执行 run() 方法,否则提交 displayTask 进执行池。进入 run() 方法里面,分段来看:

1
2
if (waitIfPaused()) return;
if (delayIfNeed()) return;

开头处两句判断,进入 waitIfPaused() 里面看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private boolean waitIfPaused() {
AtomicBoolean pause = engine.getPause();
if (pause.get()) {
synchronized (engine.getPauseLock()) {
if (pause.get()) {
L.d(LOG_WAITING_FOR_RESUME, memoryCacheKey);
try {
engine.getPauseLock().wait();
} catch (InterruptedException e) {
L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);
return true;
}
L.d(LOG_RESUME_AFTER_PAUSE, memoryCacheKey);
}
}
}
return isTaskNotActual();
}

此处主要用在列表视图中加载图片时,为了流畅地滑动,设定手指在滑动或猛地滑动时,不去加载图片。再看 isTaskNotActual() 方法:

1
2
3
private boolean isTaskNotActual() {
return isViewCollected() || isViewReused();
}

isViewCollected(),即判断 target ImageAware 是否被垃圾回收器收集;isViewReused(),判断当前的 ImageAware 是否在显示 image 时被重用。两者结合起来,判断是否是实际的 task。

同理,再看 delayIfNeed(),判断 task 是否应该被 interrupted。再看下面的大段:

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
// loadFromUriLock 是一个锁
ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
if (loadFromUriLock.isLocked()) {
L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
}

loadFromUriLock.lock();
Bitmap bmp;

try {
// 检查是否是实际的 task
checkTaskNotActual();
// 从缓存中获取
bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null || bmp.isRecycled()) {
bmp = tryLoadBitmap();
if (bmp == null) return; // listener callback already was fired

checkTaskNotActual();
checkTaskInterrupted();
// 判断是否应该预处理
if (options.shouldPreProcess()) {
L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
bmp = options.getPreProcessor().process(bmp);
if (bmp == null) {
L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
}
}

if (bmp != null && options.isCacheInMemory()) {
L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
// 将图片保存至内存缓存中
configuration.memoryCache.put(memoryCacheKey, bmp);
}
} else {
loadedFrom = LoadedFrom.MEMORY_CACHE;
L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey);
}

if (bmp != null && options.shouldPostProcess()) {
L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey);
bmp = options.getPostProcessor().process(bmp);
if (bmp == null) {
L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
}
}
checkTaskNotActual();
checkTaskInterrupted();
} catch (TaskCancelledException e) {
fireCancelEvent();
return;
} finally {
loadFromUriLock.unlock();
}

锁对象与图片的 uri 是对应上的。举例来说,视图列表中,item 获取网络图片,短时间内滚动很频繁时,这里,就会由图片的 uri 去对应一个 ReentrantLock 对象,具有相同 uri 的请求就在loadFromUriLock.lock()处锁住并等待,当图片加载完成后, ReentrantLock 对象会被释放,之前相同 uri 的请求就会继续执行 loadFromUriLock.lock() 之后的代码。总而言之,ReentrantLock 能很好地避免去重复请求图片。好,重点看下上述代码中 tryLoadBitmap() 方法,分段来看:

1
2
3
4
5
6
7
8
File imageFile = configuration.diskCache.get(uri);
if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
loadedFrom = LoadedFrom.DISC_CACHE;

checkTaskNotActual();
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
}

先判断文件缓存中是否有该文件,若有的话,则调用 decodeImage() 方法去解码图片,此方法里又调用到了 decode() 方法,根据 target 的宽高和 ScaleType 去裁剪图片。再看 tryLoadBitmap() 方法中的另一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
loadedFrom = LoadedFrom.NETWORK;

String imageUriForDecoding = uri;
// 判断是否配置了 isCacheOnDisk()
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
imageFile = configuration.diskCache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}

checkTaskNotActual();
bitmap = decodeImage(imageUriForDecoding);

if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
fireFailEvent(FailType.DECODING_ERROR, null);
}
}

着重进入 tryCacheImageOnDisk() 中看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private boolean tryCacheImageOnDisk() throws TaskCancelledException {
L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);

boolean loaded;
try {
// 下载图片,将其保持到文件缓存中
loaded = downloadImage();
if (loaded) {
// 获取配置保存在文件系统中的图片大小
int width = configuration.maxImageWidthForDiskCache;
int height = configuration.maxImageHeightForDiskCache;
if (width > 0 || height > 0) {
L.d(LOG_RESIZE_CACHED_IMAGE_FILE, memoryCacheKey);
// 裁剪图片替换原图,再保存至文件系统中
resizeAndSaveImage(width, height); // TODO : process boolean result
}
}
} catch (IOException e) {
L.e(e);
loaded = false;
}
return loaded;
}

此方法主要从服务器上拉取图片,并保存到本地文件中。

总结 UIL 的设计图如下:

缓存策略

一般加载大批量图片时,都会做缓存处理,这里的图片缓存分为内存缓存和磁盘缓存,下面结合源码详细来看:

内存缓存

先来了解两个概念:强引用和弱引用。

强引用

创建一个对象,将这个对象赋给一个引用变量,引用变量指向时,则永远不会被 GC 掉,甚至内存不足、出现 OOM 时也不会。举例来说,开发中 new 的对象是强引用。

弱引用

通过 WeakReference 类实现,具有较强的不确定性,若垃圾回收器扫描到 WeakReference 的对象,会将其回收释放内存。

好,看看这里 UIL 的内存缓存策略:

  • 只使用强引用的缓存:

    LruMemoryCache,持有限定数量 Bitmap 的强引用,每当 Bitmap 进入,会移到队列的头部。当 Bitmap 添至的缓存满了,队列尾部的 Bitmap 被逐出,且适当地可被 GC 掉,为默认使用

  • 既使用强引用,又使用弱引用的的缓存:

    以下这些缓存

    共同点是:

    有限的缓存,提供 Bitmap 的存储,存储大小不会超过限定值。

    不同点是:

    UsingFreqLimitedMemoryCache,一旦超过,则最少使用的 Bitmap 会从缓存中删掉;

    LRULimitedMemoryCache,一旦超过,则最之前使用的 Bitmap 会从缓存中删掉;

    FIFOLimitedMemoryCache,一旦超过,则清除缓存遵循 FIFO 规则的处理,即先删除最先加入缓存的 Bitmap;

    LargestLimitedMemoryCache,一旦超过,则先删除最大的 Bitmap 对象;

    LimitedAgeMemoryCache,Bitmap 加入缓存的时间超过设定值,则删除。

  • 只使用弱引用的缓存:

    WeakMemoryCache,缓存 Bitmap 的总大小没有限制,但是不稳定,缓存的图片容易被回收。

    使用时,举例配置如下:

    1
    2
    3
    ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(this)
    .memoryCache(new WeakMemoryCache())
    .build();

重点看下 LruMemoryCache 类,

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
public class LruMemoryCache implements MemoryCache {

private final LinkedHashMap<String, Bitmap> map;

private final int maxSize;
// 缓存字节的大小
private int size;

// maxSize 即缓存图片的最大值
public LruMemoryCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
// 为 true 表示按访问顺序排序
this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
}

// 缓存中有,则返回相应的 Bitmap,其会移至队列的头部;Bitmap 未缓存,则返回 null
@Override
public final Bitmap get(String key) {
if (key == null) {
throw new NullPointerException("key == null");
}

synchronized (this) {
return map.get(key);
}
}

@Override
public final boolean put(String key, Bitmap value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}

synchronized (this) {
// sizeOf() 计算图片的字节大小,size 即记录当前缓存 Bitmap 的大小
size += sizeOf(key, value);
Bitmap previous = map.put(key, value);
if (previous != null) {
size -= sizeOf(key, previous);
}
}

trimToSize(maxSize);
return true;
}

// 移除存在时间最长的条目,直到剩下的所有条目不超过限定值
private void trimToSize(int maxSize) {
while (true) {
String key;
Bitmap 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<String, Bitmap> toEvict = map.entrySet().iterator().next();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
// 缓存的 Bitmap 总值大小大于 maxSize,则删除 LinkedHashMap 中第一个元素
map.remove(key);
// 相对应的,size 中减去 Bitmap 对应的字节数
size -= sizeOf(key, value);
}
}
}

@Override
public final Bitmap remove(String key) {
if (key == null) {
throw new NullPointerException("key == null");
}

synchronized (this) {
Bitmap previous = map.remove(key);
if (previous != null) {
size -= sizeOf(key, previous);
}
return previous;
}
}

@Override
public Collection<String> keys() {
synchronized (this) {
return new HashSet<String>(map.keySet());
}
}

@Override
public void clear() {
trimToSize(-1); // -1 will evict 0-sized elements
}

// 返回相应 Bitmap 的字节大小,缓存中 entry 的大小不会改变
private int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}

@Override
public synchronized final String toString() {
return String.format("LruCache[maxSize=%d]", maxSize);
}

}

对照注释,简单分析完了内存缓存,接下来再看看磁盘缓存。

磁盘缓存

同样提供了几种常见的缓存策略:

UnlimitedDiscCache,最快的缓存,不限制缓存大小,为默认使用

LruDiskCache,缓存限制于总的缓存大小或文件数量。若缓存大小超过指定值,则最之前使用的文件会被删除,基于 “Least-Recently Used” 准则;

LimitedAgeDiscCache,有限的文件存活时间,不限制缓存的大小。若缓存文件的时间超过限定值,则其会从缓存中被删除。

同样,使用时距离配置如下:

1
2
3
ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(this)
.diskCache(new LimitedAgeDiscCache())
.build();

重点看下 LruDiskCache 类,

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
public class LruDiskCache implements DiskCache {

// ...

protected DiskLruCache cache;
private File reserveCacheDir;
// 生成文件名
protected final FileNameGenerator fileNameGenerator;

protected int bufferSize = DEFAULT_BUFFER_SIZE;

// 指定 Bitmap 能被压缩的格式
protected Bitmap.CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
protected int compressQuality = DEFAULT_COMPRESS_QUALITY;

public LruDiskCache(File cacheDir, FileNameGenerator fileNameGenerator, long cacheMaxSize) throws IOException {
this(cacheDir, null, fileNameGenerator, cacheMaxSize, 0);
}

// reserveCacheDir 为保存文件缓存目录
public LruDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator, long cacheMaxSize,
int cacheMaxFileCount) throws IOException
{

if (cacheDir == null) {
throw new IllegalArgumentException("cacheDir" + ERROR_ARG_NULL);
}

// ...

this.reserveCacheDir = reserveCacheDir;
this.fileNameGenerator = fileNameGenerator;
initCache(cacheDir, reserveCacheDir, cacheMaxSize, cacheMaxFileCount);
}

private void initCache(File cacheDir, File reserveCacheDir, long cacheMaxSize, int cacheMaxFileCount)
throws IOException {

try {
cache = DiskLruCache.open(cacheDir, 1, 1, cacheMaxSize, cacheMaxFileCount);
} catch (IOException e) {
L.e(e);
if (reserveCacheDir != null) {
initCache(reserveCacheDir, null, cacheMaxSize, cacheMaxFileCount);
}
if (cache == null) {
throw e; //new RuntimeException("Can't initialize disk cache", e);
}
}
}

@Override
public File getDirectory() {
return cache.getDirectory();
}

@Override
public File get(String imageUri) {
DiskLruCache.Snapshot snapshot = null;
try {
snapshot = cache.get(getKey(imageUri));
return snapshot == null ? null : snapshot.getFile(0);
} catch (IOException e) {
L.e(e);
return null;
} finally {
if (snapshot != null) {
snapshot.close();
}
}
}

@Override
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
if (editor == null) {
return false;
}

OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
boolean copied = false;
try {
copied = IoUtils.copyStream(imageStream, os, listener, bufferSize);
} finally {
IoUtils.closeSilently(os);
if (copied) {
// 提交编辑,以对 readers 可见,这会释放编辑锁,让另一次编辑在同样的键值上开始
editor.commit();
} else {
// 放弃编辑,同样,这会释放编辑锁,让另一次编辑在同样的键值上开始
editor.abort();
}
}
return copied;
}

@Override
public boolean save(String imageUri, Bitmap bitmap) throws IOException {
DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
if (editor == null) {
return false;
}

OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
boolean savedSuccessfully = false;
try {
savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
} finally {
IoUtils.closeSilently(os);
}
if (savedSuccessfully) {
editor.commit();
} else {
editor.abort();
}
return savedSuccessfully;
}

@Override
public boolean remove(String imageUri) {
try {
return cache.remove(getKey(imageUri));
} catch (IOException e) {
L.e(e);
return false;
}
}

@Override
public void close() {
try {
// 关闭缓存,存储值会保留在文件系统中
cache.close();
} catch (IOException e) {
L.e(e);
}
cache = null;
}

@Override
public void clear() {
try {
// 关闭缓存,删除其所有的存储值
cache.delete();
} catch (IOException e) {
L.e(e);
}
try {
initCache(cache.getDirectory(), reserveCacheDir, cache.getMaxSize(), cache.getMaxFileCount());
} catch (IOException e) {
L.e(e);
}
}

}

对照注释,简单理了下磁盘缓存。

分析完内存缓存和磁盘缓存,且看下面一张加载并展示任务流程图:

至此,深入 UIL 源码探究完毕。

本人才疏学浅,如有疏漏错误之处,望读者中有识之士不吝赐教,谢谢。

1
Email: [email protected] / WeChat: Wolverine623

您也可以关注我个人的微信公众号码农六哥第一时间获得博客的更新通知,或后台留言与我交流

参考文献

1.https://github.com/nostra13/Android-Universal-Image-Loader

2.http://blog.csdn.net/xiaanming/article/details/39057201