概要
ImageLoader
是最早的图片加载框架之一。虽然现在已经使用较少,但是ImageLoader
的结构清晰,功能丰富,源码很具有学习的价值。
使用方式
首先回顾一下使用ImageLoader
加载图片的代码片段
DisplayImageOptions options = new DisplayImageOptions.Builder()
.showImageOnLoading(R.drawable.ic_stub)
.showImageForEmptyUri(R.drawable.ic_empty)
.showImageOnFail(R.drawable.ic_error)
.cacheInMemory(true)
.cacheOnDisk(true)
.considerExifParams(true)
.displayer(new CircleBitmapDisplayer(Color.WHITE, 5))
.build();
ImageLoader.getInstance().displayImage(uri, imageView, options, loadListener);
源码分析
基于版本1.9.5
结构分析
类结构图
class_imageloader.png类结构特点
-
ImageLoader
是单例实现的门面类,使用者可以通过ImageLoader实现初始化配置,发起图片加载任务,取消任务,清理缓存等; - 每一个加载任务,会被分解为多个步骤,每个步骤对应一个小任务,由专门的类来处理,如
MemeryCache
负责内存缓存,ImageDownloader
负责下载,BitmapDisplayer
负责展示; -
ImageLoaderEngine
统一管理任务分发(包括异步任务入队); - 用户可以通过
ImageLoderConfiguration
或者DisplayImageOptions
,对各个步骤的具体策略进行配置, 总体思路为对所有图片任务生效的配置在ImageLoderConfiguration
中,不同图片任务有差异的配置在DisplayImageOptions
中。
主流程分析
加载过程图示如下:
image.png下面仅列举一些关键步骤,部分细节略过
ImageLoader-displayImage
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
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)) { // uri为null分支
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;
}
if (targetSize == null) { // 计算targetSize
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize); // 生成key
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
listener.onLoadingStarted(uri, imageAware.getWrappedView()); // load start
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey); // 从缓存取
if (bmp != null && !bmp.isRecycled()) {// 缓存可用
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
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 {
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
} else {// 缓存不可用
if (options.shouldShowImageOnLoading()) {// 设置loadingImage
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)); //构建ImageLoadingInfo
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));//构建loadAndDisplayTask
if (options.isSyncLoading()) {
displayTask.run();//同步加载
} else {
engine.submit(displayTask);//提交异步任务
}
}
}
- checkConfiguration(); - 保证已初始化完成
- 赋值listener和options
- 处理uri =null的分支(分支内逻辑略)
- 计算targetSize
- 生成memoryCacheKey
- 关联imageAware.id和memoryCacheKey
- listener.onLoadingStarted(uri, imageAware.view)
- 根据memoryCacheKey从内存中读取bitmap
8.1. 如果缓存bitmap有效 -> 9
8.2. 如果缓存bitmap无效 -> 10 - 提交ProcessAndDisplayImageTask
9.1. new ProcessAndDisplayImageTask()
9.2. engine.submit(displayTask); - 提交LoadAndDisplayImageTask
10.1. set loading image to view,
10.2. new ImageLoadingInfo()
10.3. new LoadAndDisplayImageTask(imageLoadingInfo)
10.4. engine.submit(task);
taskDistributor-execute-LoadAndDisplayImageTask
- load from diskCache
1.1 image on disk -> 入队taskExecutorForCachedImages
1.2 image not on disk -> 入队taskExecutor
LoadAndDisplayImageTask - run()
- wait if paused -> 可能导致线程wait
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();
}
- delay if need (if options.shouldDelayBeforeLoading())-> 可能导致线程sleep
- loadFromUriLock.lock(); -> 申请锁
- 根据memoryCacheKey从内存中读取bitmap
4.1. 如果bitmap有效 -> 6
4.2. 如果bitmap无效 -> 5 - bitmap = tryLoadBitmap()
- preProcessor.process(bitmap)
- memoryCache.put(memoryCacheKey, bmp);
- postProcessor.process(bitmap)
- loadFromUriLock.unlock();
- new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom)
- taskDistributor/handler.execute(task);
LoadAndDisplayImageTask - tryLoadBitmap()
- load from disk
1.1 image on disk -> decodeImage
1.2 image not on disk -> 2 - downloader.getStream(uri)
- notify progress if needed
- diskCache.save(uri, inputStream)
- resize and save image
- decode image
流程总结
由于图片框架的IO请求和网络请求操作较多,所以ImageLoader维护了多个线程池来实现三级缓存,并且对重复任务(同一份资源的多次下载请求)的考虑,再各个节点均有缓存校验。
线程使用
异步任务线程池
ImageLoderConfiguration
中有三个线程池
-
taskExecutor
: 执行下载图片的任务 -
taskExecutorForCachedImages
: 执行从磁盘读取缓存的任务 -
taskDistributor
: 负责分发任务,如果用户没有指定展示的handler,展示任务也会被分配到此线程
分发任务代码片段如下
void submit(final LoadAndDisplayImageTask task) {
taskDistributor.execute(new Runnable() {
@Override
public void run() {
File image = configuration.diskCache.get(task.getLoadingUri());
boolean isImageCachedOnDisk = image != null && image.exists();
initExecutorsIfNeed();
if (isImageCachedOnDisk) {
taskExecutorForCachedImages.execute(task);
} else {
taskExecutor.execute(task);
}
}
});
}
线程池的设定
-
taskExecutor
和taskExecutorForCachedImages
默认线程池大小: DEFAULT_THREAD_POOL_SIZE = 3,核心数和最大数均为3.
Queue支持 FIFO(first in first out),LIFO(last in first out)两种。
代码片段如下:
/** Creates default implementation of task executor */
public static Executor createExecutor(int threadPoolSize, int threadPriority,
QueueProcessingType tasksProcessingType) {
boolean lifo = tasksProcessingType == QueueProcessingType.LIFO;
BlockingQueue<Runnable> taskQueue =
lifo ? new LIFOLinkedBlockingDeque<Runnable>() : new LinkedBlockingQueue<Runnable>();
return new ThreadPoolExecutor(threadPoolSize, threadPoolSize, 0L, TimeUnit.MILLISECONDS, taskQueue,
createThreadFactory(threadPriority, "uil-pool-"));
}
-
taskDistributor
使用默认cacheThreadTool(核心数0,最大无上限)
代码片段如下
/** Creates default implementation of task distributor */
public static Executor createTaskDistributor() {
return Executors.newCachedThreadPool(createThreadFactory(Thread.NORM_PRIORITY, "uil-pool-d-"));
}
- 支持用户通过config自定义线程池
缓存算法
LruMemoryCache
LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
- 数据结构使用
LinkedHashMap
-
LinkedHashMap
内部对于get的实现如下:
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
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;
}
}
即每一次查找key都从链表尾节点查询
当调用get之后,会将这个节点放到链表尾节点上
-
LinkedHashMap
对于新put的对象,也会放到尾节点上 - 每一次执行put(String key, Bitmap value),都会检查size是否超过maxSize
BaseMemoryCache
缓存软引用,使用HashMap
private final Map<String, Reference<Bitmap>> softMap = Collections.synchronizedMap(new HashMap<String, Reference<Bitmap>>());
LimitedMemoryCache
继承BaseMemoryCache
,缓存使用软引用+强引用,其中强引用保存在列表LinkedList中,由子类决定removeNext具体的对象。
private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());
@Override
public boolean put(String key, Bitmap value) {
boolean putSuccessfully = false;
// Try to add value to hard cache
int valueSize = getSize(value);
int sizeLimit = getSizeLimit();
int curCacheSize = cacheSize.get();
if (valueSize < sizeLimit) {
while (curCacheSize + valueSize > sizeLimit) {
Bitmap removedValue = removeNext();
if (hardCache.remove(removedValue)) {
curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
}
}
hardCache.add(value);
cacheSize.addAndGet(valueSize);
putSuccessfully = true;
}
// Add value to soft cache
super.put(key, value);
return putSuccessfully;
}
protected abstract Bitmap removeNext();
...
FIFOLimitedMemoryCache
继承LimitedMemoryCache
,使用LinkedList
作为queue保存强引用,当缓存达到上限,移除第一张。
@Override
protected Bitmap removeNext() {
return queue.remove(0);
}
LargestLimitedMemoryCache
继承LimitedMemoryCache
,使用HashMap<Bitmap, Integer>
保存强引用,当缓存达到上限,移除size最大的一张。
@Override
protected Bitmap removeNext() {
Integer maxSize = null;
Bitmap largestValue = null;
Set<Entry<Bitmap, Integer>> entries = valueSizes.entrySet();
synchronized (valueSizes) {
for (Entry<Bitmap, Integer> entry : entries) {
if (largestValue == null) {
largestValue = entry.getKey();
maxSize = entry.getValue();
} else {
Integer size = entry.getValue();
if (size > maxSize) {
maxSize = size;
largestValue = entry.getKey();
}
}
}
}
valueSizes.remove(largestValue);
return largestValue;
}
IO
保存单文件 ( BaseDiskCache
)
为了避免下载不完整,读取过程命名加后缀.tmp
@Override
public boolean save(String imageUri, Bitmap bitmap) throws IOException {
File imageFile = getFile(imageUri);
File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
boolean savedSuccessfully = false;
try {
savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
} finally {
IoUtils.closeSilently(os);
if (savedSuccessfully && !tmpFile.renameTo(imageFile)) {
savedSuccessfully = false;
}
if (!savedSuccessfully) {
tmpFile.delete();
}
}
bitmap.recycle();
return savedSuccessfully;
}
LruDiskCache
LruDiskCache
非google开发,但已经作为图片磁盘缓存的一种解决方案,得到了官方的认可。
google源码地址:googlesource DiskLruCache.java
详细讲解
网友评论