Android 图片载入框架Universal-Image-Loader源代码解析
本篇文章已授权微信公众号 guolin_blog (郭霖)独家公布
Universal-Image-Loader(项目地址)能够说是安卓知名图片开源框架中最古老、使用率最高的一个了。一张图片的载入对于安卓应用的开发或许是件简单的事,可是假设要同一时候载入大量的图片,而且图片用于ListView、GridView、ViewPager等控件,怎样防止出现OOM、怎样防止图片错位(由于列表的View复用功能)、怎样更快地载入、怎样让client程序猿用最简单的操作完成本来十分复杂的图片载入工作。成了全世界安卓应用开发程序猿心头的一大难题。所幸有了Universal-Image-Loader,让这一切变得简单,从某种意义来讲,它延长了安卓开发人员的寿命~
针对上述几个问题,Universal-Image-Loader可谓是兵来将挡水来土掩,见招拆招:
怎样同一时候载入大量图片:採用线程池优化高并发
怎样提高载入速度:使用内存、磁盘缓存
怎样避免OOM:载入的图片进行压缩,内存缓存具有相关的淘汰算法
怎样避免ListView的图片错位:将要载入的图片和要显示的ImageView绑定, 在请求过程中推断该ImageView当前绑定的图片是否是当前请求的图片。不是则停止当前请求。
首先来看下Universal-Image-Loader总体流程图:
总的来说就是:下载图片——将图片缓存在磁盘中——解码图片成为Bitmap——Bitmap的预处理——缓存在Bitmap内存中——Bitmap的后期处理——显示Bitmap
基本用法:
想必非常多朋友也知道了。只是还是“抄袭”下Github说明里的用法:
**简单版本号:**
ImageLoader imageLoader = ImageLoader.getInstance(); // Get singleton instance
// Load image, decode it to Bitmap and display Bitmap in //ImageView (or any other view
// which implements ImageAware interface)
imageLoader.displayImage(imageUri, imageView);
// Load image, decode it to Bitmap and return Bitmap to callback
imageLoader.loadImage(imageUri, new SimpleImageLoadingListener() {
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
// Do whatever you want with Bitmap
}
});
// Load image, decode it to Bitmap and return Bitmap //synchronously
Bitmap bmp = imageLoader.loadImageSync(imageUri);
**复杂版本号**:
// Load image, decode it to Bitmap and display Bitmap in ImageView (or any other view
// which implements ImageAware interface)
imageLoader.displayImage(imageUri, imageView, options, new ImageLoadingListener() {
@Override
public void onLoadingStarted(String imageUri, View view) {
...
}
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
...
}
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
...
}
@Override
public void onLoadingCancelled(String imageUri, View view) {
...
}
}, new ImageLoadingProgressListener() {
@Override
public void onProgressUpdate(String imageUri, View view, int current, int total) {
...
}
});
// Load image, decode it to Bitmap and return Bitmap to callback
ImageSize targetSize = new ImageSize(80, 50);
// result Bitmap will be fit to this size
imageLoader.loadImage(imageUri, targetSize, options, new SimpleImageLoadingListener() {
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
// Do whatever you want with Bitmap
}
});
// Load image, decode it to Bitmap and return Bitmap
//synchronously
ImageSize targetSize = new ImageSize(80, 50);
// result Bitmap will be fit to this size
Bitmap bmp = imageLoader.loadImageSync(imageUri, targetSize, options);
相信各位经验丰富的安卓开发人员一看就心照不宣,就不必解释什么了。
和源代码见面之前。先和几个重要的类打招呼:
ImageLoaderEngine:任务分发器。负责分发LoadAndDisplayImageTask和ProcessAndDisplayImageTask给详细的线程池去运行。
ImageAware:显示图片的对象。能够是ImageView等。
ImageDownloader:图片下载器。负责从图片的各个来源获取输入流。
Cache:图片缓存,分为MemoryCache和DiskCache两部分。
MemoryCache:内存图片缓存。可向内存缓存缓存图片或从内存缓存读取图片。
DiskCache:本地图片缓存。可向本地磁盘缓存保存图片或从本地磁盘读取图片。
ImageDecoder:图片解码器。负责将图片输入流InputStream转换为Bitmap对象。
BitmapProcessor:图片处理器,负责从缓存读取或写入前对图片进行处理。
BitmapDisplayer:将Bitmap对象显示在相应的控件ImageAware上。
LoadAndDisplayImageTask:用于载入并显示图片的任务。
ProcessAndDisplayImageTask:用于处理并显示图片的任务。
DisplayBitmapTask:用于显示图片的任务。
当中有个全局图片载入配置类贯穿整个框架,ImageLoaderConfiguration。能够配置的东西实在有点多:
private ImageLoaderConfiguration(final Builder builder) {
resources = builder.context.getResources();
maxImageWidthForMemoryCache = builder.maxImageWidthForMemoryCache;
maxImageHeightForMemoryCache = builder.maxImageHeightForMemoryCache;
maxImageWidthForDiskCache = builder.maxImageWidthForDiskCache;
maxImageHeightForDiskCache = builder.maxImageHeightForDiskCache;
processorForDiskCache = builder.processorForDiskCache;
taskExecutor = builder.taskExecutor;
taskExecutorForCachedImages = builder.taskExecutorForCachedImages;
threadPoolSize = builder.threadPoolSize;
threadPriority = builder.threadPriority;
tasksProcessingType = builder.tasksProcessingType;
diskCache = builder.diskCache;
memoryCache = builder.memoryCache;
defaultDisplayImageOptions = builder.defaultDisplayImageOptions;
downloader = builder.downloader;
decoder = builder.decoder;
customExecutor = builder.customExecutor;
customExecutorForCachedImages = builder.customExecutorForCachedImages;
networkDeniedDownloader = new NetworkDeniedImageDownloader(downloader);
slowNetworkDownloader = new SlowNetworkImageDownloader(downloader);
L.writeDebugLogs(builder.writeLogs);
}
主要是图片最大尺寸、线程池、缓存、下载器、解码器等等。
经过前面那么多的铺垫,最终迎来了源代码~~
整个框架的源代码上万,全部讲完不可能,最好的方式还是依照载入流程走一遍,细枝末节各位能够自己慢慢研究,一旦总体把握好了,其它的一切就水到渠成。切勿仅仅见树木不见森林,迷失在各种代码细节中~~
好了。简单点,讲代码的方式简单点,从最简单的代码切入:
imageLoader.displayImage(imageUri, imageView);
进入方法:
public void displayImage(String uri, ImageView imageView) {
displayImage(uri, new ImageViewAware(imageView), null, null, null);
}
ImageAware是ImageView的包装类。持有ImageView对象的弱引用,防止ImageView出现内存泄漏发生。主要是提供了获取ImageView宽度高度和ScaleType等。
最终会运行这一个重载方法:
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);
}
//使用默认的Listener
if (listener == null) {
listener = defaultListener;
}
//使用默认的options
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}
//uri为空
if (TextUtils.isEmpty(uri)) {
//则移除当前请求
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 = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
//依据uri和图片尺寸生成缓存的key
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
//从内存缓存取出Bitmap
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
//命中内存缓存的相应的Bitmap
if (bmp != null && !bmp.isRecycled()) {
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
//进行Bitmap后期处理
if (options.shouldPostProcess()) {
//创建一个载入和显示图片任务须要的信息的对象
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
//将内存缓存中取出的Bitmap显示出来
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
defineHandler(options));
//是否同一时候载入。否则使用线程池载入
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
} else {
//不进行图片处理则直接显示在ImageView
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
} else {
//假设内存缓存拿不到图片
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);
}
}
}
这种方法基本描绘出了整个图片载入的流程。重要的地方已经加上凝视。
第8行的listener就是上面基本使用说明复杂版本号的进度回调接口ImageLoadingListener,大家看下就知道。假设没有配置的话设置为默认,而默认事实上啥都没做,方法都是空实现。
第12行也是相似地假设没有配置DisplayImageOptions。即图片显示的配置项。则取默认的。
看下这个图片显示配置类能够配置什么:
/** Sets all options equal to incoming options */
public Builder cloneFrom(DisplayImageOptions options) {
imageResOnLoading = options.imageResOnLoading;
imageResForEmptyUri = options.imageResForEmptyUri;
imageResOnFail = options.imageResOnFail;
imageOnLoading = options.imageOnLoading;
imageForEmptyUri = options.imageForEmptyUri;
imageOnFail = options.imageOnFail;
resetViewBeforeLoading = options.resetViewBeforeLoading;
cacheInMemory = options.cacheInMemory;
cacheOnDisk = options.cacheOnDisk;
imageScaleType = options.imageScaleType;
decodingOptions = options.decodingOptions;
delayBeforeLoading = options.delayBeforeLoading;
considerExifParams = options.considerExifParams;
extraForDownloader = options.extraForDownloader;
preProcessor = options.preProcessor;
postProcessor = options.postProcessor;
displayer = options.displayer;
handler = options.handler;
isSyncLoading = options.isSyncLoading;
return this;
}
配置是否内存磁盘缓存以及设置图片预处理和后期处理器(预处理和后期处理器默觉得null。留给client程序猿扩展)等。
displayImage方法第27行里。假设没有专门设置targetSize。即指定图片的尺寸,就取ImageAware的宽高。即包装在里面的ImageView的宽高,假设得到的宽高小于等于0,则设置为最大宽高,假设没有设置内存缓存的最大宽高(maxImageWidthForMemoryCache,maxImageHeightForMemoryCache),则为屏幕的宽高。
然后依据图片uri和图片的尺寸生成一个内存缓存的key。之所以使用图片尺寸是由于内存缓存的图片同一张图片拥有不同尺寸的版本号。
第33行中:
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
engine指的是任务分发器 ImageLoaderEngine。它持有一个HashMap。作为记录图片载入任务使用。一旦任务停止载入或者载入完成则会删除相应的任务引用。
prepareDisplayTaskFor方法正是将任务的引用加入到该HashMap中。这里以内存缓存的Key为键。
接下来假设拿到的图片须要做后期处理,则创建一个图片显示信息的对象,然后以之前创建的图片显示信息的对象为參数之中的一个创建一个处理显示任务对象ProcessAndDisplayImageTask(实现Runnable),假设是指定同步载入则直接调用它的run方法,不是则将其加入到任务分发器ImageLoaderEngine的线程池中异步运行。
若不须要对图片进行后期处理则调用Displayer去显示显示图片。默认的Displayer为SimpleBitmapDisplayer。仅仅是简单地显示图片到指定的ImageView中。
假设拿不到内存缓存的相应图片,则创建载入显示图片任务对象LoadAndDisplayImageTask。然后运行该任务去磁盘缓存或者网络载入图片。
接下来的关键就是看下LoadAndDisplayImageTask的run方法怎么运行的。就知道整个图片载入怎么运行的了。
public void run() {
//当前请求是否须要暂停等待。等待期间被中断则停止请求
if (waitIfPaused()) return;
//假设当前请求延时载入,延时期间被中断则停止请求
if (delayIfNeed()) return;
//得到与图片uri绑定的锁
ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
if (loadFromUriLock.isLocked()) {
L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
}
//锁与图片uri相应是为了防止同一时候载入同一张图片
loadFromUriLock.lock();
Bitmap bmp;
try {
//检查当前任务的相应Imageview是否被回收或者复用,是则抛TaskCancelledException结束当前任务
checkTaskNotActual();
//疑问:再一次尝试从内存缓存中获取是由于可能此时其它任务载入了同样的图片?
bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null || bmp.isRecycled()) {
//内存缓存获取不到,尝试到磁盘缓存或者网络获取
bmp = tryLoadBitmap();
if (bmp == null) return; // listener callback already was fired
checkTaskNotActual();
//检查任务是否被Interrupt
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();
}
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
}
run方法不长,就直接展示了。
这里第3、5行:
if (waitIfPaused()) return;
if (delayIfNeed()) return;
第1行中的waitIfPaused方法:
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();
}
这里假设engine的pause标志位被置位为true。则会调用engine.getPauseLock()对象(事实上就是一个普通的Object对象)的wait方法使当前线程暂停运行。为什么要这样呢?看下engine的pause标志位什么时候会被置位为true。最终在PauseOnScrollListener的重写方法onScrollStateChanged中找到:
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case OnScrollListener.SCROLL_STATE_IDLE:
imageLoader.resume();
break;
case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
if (pauseOnScroll) {
imageLoader.pause();
}
break;
case OnScrollListener.SCROLL_STATE_FLING:
if (pauseOnFling) {
imageLoader.pause();
}
break;
}
我们知道ListView滑动时候载入显示图片会使得滑动有卡顿现象。这就是在列表滑动时暂停载入的方式,仅仅要给ListView设置:
ListView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling ))
就能够通过在滑动时候暂停请求任务从而防止ListView滑动带来的卡顿。一旦ListView在滑动状态。全部新增的图片请求都会暂停等待列表滑动停止调用imageLoader.resume()方法唤醒暂停的线程继续载入请求。
waitIfPaused()在线程等待过程中被中断(线程池被停止运行)的时候会返回true,从而终止任务的run方法终止载入请求。
它的返回值还由isTaskNotActual方法决定,该方法主要推断显示图片的ImageView是否被回收以及所在的列表项是否被复用,是的话也是终止载入任务。
delayIfNeed方法则是在任务须要延迟的时候让当前线程sleep一会,同样也是遇到中断返回true终止任务。
接下来12行
loadFromUriLock.lock();
这里的loadFromUriLock是一个ReentrantLock对象,是和要载入的图片uri绑定一起的,所以同样uri的图片具有同一把锁,这也就意味着同样图片的请求同一时间仅仅有一个在网络或者磁盘载入。有效避免了反复的网络或者磁盘缓存载入。一旦载入完其余请求都会从内存缓存中载入图片。
然后尝试从内存缓存中取出图片。一旦成功得到图片,则经过请求是否有效的相关推断之后。创建一个DisplayBitmapTask将图片显示到相应的ImageView中。
关键是假设内存取不到图片的情况,这时候就得看run中的22行:
bmp = tryLoadBitmap();
该方法就是去磁盘缓存取图片。假设没有则取网络图片。
方法也不长。所以也直接拷贝过来:
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
try {
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;
//检查当前任务的相应Imageview是否被回收或者复用,是则抛TaskCancelledException结束当前任务
checkTaskNotActual();
//解码得到图片
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
}
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
loadedFrom = LoadedFrom.NETWORK;
String imageUriForDecoding = uri;
//磁盘缓存获取失败则从网络获取
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
imageFile = configuration.diskCache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}
//仍然检查当前任务的相应Imageview是否被回收或者复用,是则抛TaskCancelledException结束当前任务
checkTaskNotActual();
//tryCacheImageOnDisk将图片保存到文件系统。
//接下来将图片按需求裁剪等处理后载入到内存
//所以文件保存的是原图或者缩略图,内存保存的是依据ImageView大小、scaletype、方向处理过得图片
bitmap = decodeImage(imageUriForDecoding);
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
fireFailEvent(FailType.DECODING_ERROR, null);
}
}
} catch (IllegalStateException e) {
fireFailEvent(FailType.NETWORK_DENIED, null);
} catch (TaskCancelledException e) {
throw e;
} catch (IOException e) {
L.e(e);
fireFailEvent(FailType.IO_ERROR, e);
} catch (OutOfMemoryError e) {
L.e(e);
fireFailEvent(FailType.OUT_OF_MEMORY, e);
} catch (Throwable e) {
L.e(e);
fireFailEvent(FailType.UNKNOWN, e);
}
return bitmap;
}
首先第四行是尝试从磁盘文件系统中寻找相应的图片。假设有的话。调用:
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
进行解码得到相应的Bitmap对象,注意这里将文件路径使用Scheme.FILE进行包装。由于Universal-Image-Loader载入图片有多个来源,比方网络,文件系统,项目目录asset等。所以解码的时候进行一个包装。方便解码器BaseImageDecoder在解码的时候识别来源进行相应的获取流处理。
Scheme总共同拥有以下几个:
HTTP("http"), HTTPS("https"), FILE("file"), CONTENT("content"), ASSETS("assets"), DRAWABLE("drawable"), UNKNOWN("")
这里的decodeImage方法:
private Bitmap decodeImage(String imageUri) throws IOException {
ViewScaleType viewScaleType = imageAware.getScaleType();
ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
getDownloader(), options);
return decoder.decode(decodingInfo);
}
依据ImageView的ScaleType以及targetSize等生成一个ImageDecodingInfo对象,传入解码器decoder的decode方法中。
这里的解码器默觉得BaseImageDecoder,decode方法:
public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
Bitmap decodedBitmap;
ImageFileInfo imageInfo;
//获得图片相应的流
InputStream imageStream = getImageStream(decodingInfo);
if (imageStream == null) {
L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());
return null;
}
try {
//依据图片的属性生成一个ImageFileInfo对象,指定尺寸和旋转处理
imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
// 还原stream
// 为什么还原呢? 由于通过上面的操作,我们的stream游标可能
// 已经不在首部,这时再去读取会造成了信息不完整
// 所以这里须要reset一下
imageStream = resetStream(imageStream, decodingInfo);
Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
//解码获取Bitmap对象
decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
} finally {
IoUtils.closeSilently(imageStream);
}
if (decodedBitmap == null) {
L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
} else {
//对Bitmap进行裁剪和旋转等操作
decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
imageInfo.exif.flipHorizontal);
}
return decodedBitmap;
}
这里详细怎样裁剪和旋转的代码就不介绍了,有兴趣大家能够去源代码看下。
最后得到的图片就能够直接使用显示图片任务DisplayBitmapTask将图片显示在ImageView上了。
假如磁盘也获取不到图片,那就须要到网络载入了。
tryLoadBitmap方法的20行:
if (options.isCacheOnDisk() && tryCacheImageOnDisk())
推断是否须要磁盘缓存,须要的话,进入tryCacheImageOnDisk方法:
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;
}
主要就是通过downloadImage方法从网络载入图片,保存到文件系统中。然后推断是否配置了maxImageWidthForDiskCache和maxImageHeightForDiskCache选项,有的话对保存在文件系统的图片进行裁剪压缩得到缩略图。
然后在tryLoadBitmap方法第21行中。取出相应磁盘缓存的图片文件路径,然后使用解码器将图片解码文件为Bitmap。
以上讨论的是须要磁盘缓存的情况。假设不须要呢?看下tryLoadBitmap方法第31行:
bitmap = decodeImage(imageUriForDecoding);
imageUriForDecoding是图片的uri。该方法上面已经讲过,最终是通过流解码为一个Bitmap对象。也就是从网络载入图片到内存。而前面讲的是从文件系统载入图片到内存。
这里须要注意的是,磁盘缓存的是原图或者其缩略图。内存缓存的是依据ImageView裁剪的图片。
拿到Bitmap对象,然后就是对图片进行预处理了(假设配置为须要的话),处理完成后加入到内存缓存。然后进行后期处理(假设须要的话),最后将Bitmap交给显示任务DisplayBitmapTask处理。
DisplayBitmapTask的run就十分简单了:
public void run() {
if (imageAware.isCollected()) {
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else if (isViewWasReused()) {
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else {
L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);
displayer.display(bitmap, imageAware, loadedFrom);
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
}
}
在确保ImageView没有被回收和复用的情况下,交给显示器displayer处理,displayer默觉得SimpleBitmapDisplayer。SimpleBitmapDisplayer的display事实上就是直接调用ImageView的setBitmapImage方法将图片显示上去。然后将该ImageView的图片载入任务从任务分发器任务记录中移除。
DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
这里注意下runTask的參数handler。它的来源在ImageLoader类的defineHandler方法:
private static Handler defineHandler(DisplayImageOptions options) {
Handler handler = options.getHandler();
if (options.isSyncLoading()) {
handler = null;
} else if (handler == null && Looper.myLooper() == Looper.getMainLooper()) {
handler = new Handler();
}
return handler;
}
能够看到首先取显示配置中的Handler,默觉得null,所以默认会运行以下的推断,假设调用该方法是在主线程,则创建一个Handler对象,于是改Handler就和主线程绑定一起了(不熟悉Hanlder的朋友能够看下我的还有一篇博文:全面分析Handler消息机制)
最后会利用Handler的post方法将图片的显示传递到主线程调用~~
图片载入过程算是Over了~~
这里谈下一些注意要点:
1.关于线程池:
当载入显示任务要运行的时候,任务分发器的submit相应方法是:
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);
}
}
});
}
当中任务分发线程池taskDistributor为缓存线程池(CacheThreadPoll)。用于推断任务请求的图片是否缓存在磁盘中以及分发任务给运行任务线程池。两个实际运行任务的线程池taskExecutorForCachedImages和taskExecutor为可配置的线程池,默认核心和工作线程数都为3。默认配置採用的先进先出的队列,假设是列表图片建议配置为先进后出队列。
两个线程池能够依据任务性质自己定义配置扩展其它属性。
为什么要专门分为三个线程池而不是一个线程池运行全部任务呢?假设全部任务运行在一个线程池中,全部的任务就都仅仅能採取同一种任务优先级和运行策略。
显然果要有更好的性能。在线程数比較多而且线程承担的任务不同的情况下,App中不妨按任务的类别来划分线程池。
一般来说,任务分为CPU密集型任务,IO密集型任务和混合型任务。
CPU密集型任务配置尽可能小的线程,如配置Ncpu+1个线程的线程池。IO密集型任务则由于线程并非一直在运行任务,则配置尽可能多的线程。如2*Ncpu。详细可參见从源代码分析Universal-Image-Loader中的线程池
2.关于下载器:
在载入显示任务类LoadAndDisplayImageTask的获取下载器方法中:
private ImageDownloader getDownloader() {
ImageDownloader d;
if (engine.isNetworkDenied()) {
d = networkDeniedDownloader;
} else if (engine.isSlowNetwork()) {
d = slowNetworkDownloader;
} else {
d = downloader;
}
return d;
}
默认网络良好的情况下使用BaseImageDownloader。在无网络情况下使用NetworkDeniedImageDownloader。当推断到图片源为网络时抛出异常停止请求。
网络不佳情况下使用SlowNetworkDownloader,主要通过重写FilterInputStream的 skip(long n) 函数解决在慢网络情况下 decode image 异常的 Bug。(详细解决原理本人也不太清楚,求教。。)
另外关于缓存机制详细能够看下从源代码分析Android-Universal-Image-Loader的缓存处理机制
总结整个框架的特点:
1.支持内存和磁盘两级缓存。
2.配置高灵活性,包含缓存算法、线程池属性、显示选项等等。
3.支持同步异步载入
4.运用多种设计模式,比方模板方法、装饰者、建造者模式等等,具有非常高的可扩展性可复用性。
好了。这个开源框架就说到这里,希望对大家有帮助,个人水平有限,阅读开源框架源代码还不是非常多。不足之处请帮忙纠正~~
參考文章:
Android 开源框架Universal-Image-Loader全然解析(三)—源代码解读