源码解析 Universal Image Loader
2015-12-05 14:48
239 查看
在大多数的项目中我们都会用到从网络加载图片显示的功能,在Google发布的Volley框架中也提供了类似的功能,但是个人还是比较习惯Universal Image Loader。在今天的文章中从源码角度简单的分析一下Universal Image Loader的实现流程。
在这里就不去介绍Universal Image Loader简单的用法了,如果还没有用过的朋友可以直接看一下该框架中自带的Demo,没有太多复杂的地方。
在使用UIL的时候(Universal Image Loader下文中都将写为UIL),我们需要先在Application中配置一下,以下的内容是自带的Demo中的配置信息:
我们最开始接触的就是ImageLoaderConfiguration类,就先从它开始看起。可以看到这个类非常的长,但是仔细观察会发现主题的代码却没有多少:
在上面代码的注释中已经说明了各个属性的作用,里面涉及到的类在接下来的分析中如果有涉及将会解释。在这个类的构造方法中使用了构造者模式,在参数较多的情况下保证了良好的可读性。其内部类Builder(也就是构造者)中的属性和ImageLoaderConfiguration中大致相同,其采用了链表的形式,即在保存了一个传递参数后返回自身的引用,如下所示:
在这里只列举出两个举例。我们可以使用.的方式来进行构建,如下所示:
其中重点看一下build方法:
再看一下initEmptyFieldsWithDefaultValues方法:
在其中给一些用户没有手动设置的属性进行了初始化,然后在build方法中返回了一个ImageLoaderConfiguration类的实例。
到这里为止我们就将ImageLoaderConfiguration这个类简单的看了一遍,如果有兴趣可以深入的去读一下代码。
由于在Application中写了这样一行代码:ImageLoader.getInstance().init(config.build()); 我们来看一下整个框架中最主要的一个类ImageLoader。
首先是获取实例的方法,采用了单例模式来实现。
使用了双重检查来提高性能,并且这种写法需要将实例对象volatile保证可见性。
而后是init方法:
简单的保存了一下ImageLoaderConfiguration,然后用它创建出了一个叫engin的对象,定义如下:
它的作用是一个分发器,将任务分发到线程池中来执行,在后面会详细的对它进行分析。
这样我们的配置工作就结束了,但是光有配置也没用,UIL给我们提供了几种加载图片的方法:
一共有三种加载图片的方法,看名字也大概可以猜出含义,第一种是简单的加载图片,第二种是显示图片,第三种是同步加载图片。
先来看第一种最终调用的方法:
可以看到这个方法就简单的检查了一下configuration后创建出了一个ViewAware包装类(后面会有介绍)后调用了displayImage的一个重载方法。
再来看一下第三种最终调用的方法:
在这个方法中最终调用的是loadImage的一个重载方法,那么它最终也会调用到displayImage方法。不同的就是由于这个方法是同步的,所以它可以直接返回加载出来的Bitmap。
那么现在来看一下第二种最终调用的方法,所有的方法也都会调用到它身上:
在上面的代码中给出了整体流程的注释,下载来看一下其中涉及到的细枝末节的类与方法。
就从这个方法的参数开始说起吧,首先ImageAware到底是个什么,可以看到它是个接口,以下是接口的定义:
在这里因为篇幅的原因把注释都删掉了,大致的意思就是包装了一下显示图片控件的宽高,图片的缩放模式,控件的ID,展示的图片啊这些的内容,如果有兴趣也可以读一下注释,写的非常清楚。在正常情况下我们都使用ImageView来实现图片的展示,那么ImageView是如何转换为ImageAware的呢,可以随便拿一个displayImage方法来看一下:
使用了一个ImageViewAware来包装了ImageView。这里就不再深入代码细节了,但是有一点需要注意,就是UIL的这种模式是对扩展开放的,如果我们有一个自定义的控件,只需要为它写一个适配器(Adapter模式)ImageAware就可以去使用了,非常的灵活!
再来看一下刚分析的displayImage方法中的参数DisplayImageOptions,从名字上可以看出来它是展示图片的配置信息,同样使用构造者模式来进行组装,如果有兴趣可以看一下代码,属性的名字也非常直观,这里就不一一列举说明了。
还有一个没有用到的参数ImageLoadingProgressListener,看一下定义:
可以看到它回调了图片加载的进度。
下面接着说displayImage方法的主逻辑,该方法中有多处使用了ImageLoaderEngine类的对象,显然要了解整个方法,我们必须将这个类看一下,首先是构造方法:
前几句只是简单的将configuration的值赋给了ImageLoaderEngine中的属性,这个就不再说了,如果忘记了是什么意思可以看一下前面。最后一行创建出了一个分发任务的线程池。
在这个类中还有一些属性是很重要的,来看一下:
类中的方法比较简单,在这里只看一下我们用到的,如果有兴趣可以读一下:
如果还有印象的话可以知道这个方法的调用是在内存缓存不存在或者被回收的时候。首先查看一下当前磁盘上是否有这个Uri的缓存内容,然后调用initExecutorsIfNeed在线程池不存在的时候初始化一下线程池,如果当前在磁盘上有缓存的话,将其交给taskExecutorForCachedImages执行,如果没有缓存需要从网络拉取的话,交给taskExecutor来执行。
调用这个方法的前提是在内存里有对应的缓存并且没有被回收,那么直接交给taskExecutorForCachedImages来执行。
这两个方法也是刚才用到的,分别为准备分发任务和取消任务,对应的仅仅是将显示图片控件id为key的实例从map中移除了。
在displayImage中我们还剩下三个类没有去看,分别是ImageLoadingInfo,ProcessAndDisplayImageTask和LoadAndDisplayImageTask。先来看ImageLoadingInfo:
这个类非常的简单,包装了要加载图片的相关信息,并且将属性设置为final类型,保证我们不能去修改。这个类中属性的含义前面都有提及,这里就不多加赘述。
第二个是ProcessAndDisplayImageTask类,这是一个Runnable,来直接看一下run方法:
可以看到在经过后处理以后,调用了LoadAndDisplayImageTask类的runTask方法,由于在LoadAndDisplayImageTask的run方法中最后也将调用runTask,所以我们先来看一下LoadAndDisplayImageTask的run方法,然后一起来看runTask:
public void run() {
if (waitIfPaused()) return;
if (delayIfNeed()) return;
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 {
checkTaskNotActual();
//从内存中去读取bitmap
bmp = configuration.memoryCache.get(memoryCacheKey);
//当bitmap为空 或者已经被回收了的时候
if (bmp == null || bmp.isRecycled()) {
//尝试着去加载bitmap
bmp = tryLoadBitmap();
if (bmp == null) return; // listener callback already was fired
checkTaskNotActual();
checkTaskInterrupted();
//当需要对bitmap进行处理的时候进行处理
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);
//当需要在内存中进行缓存的话 直接将memoryKey放入到map中进行缓存
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);
}
方法中还是先判断了一下在内存中是否存在bitmap实例,如果存在的话打一个标记,过一会在构建展示图片任务的时候将会用到。如果不存在的话调用tryLoadBitmap方法去加载图片,方法体如下:
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
try {
//从磁盘中获取相应的uri
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 = LoadedFrom.DISC_CACHE;
checkTaskNotActual();
//从磁盘的缓存中读取相应的Bitmap文件
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());
}
}
checkTaskNotActual();
//解码出bitmap信息
bitmap = decodeImage(imageUriForDecoding);
//当bitmap为空的时候标记为失败
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;
}
方法注释写的比较详细了,这里简单的在看一下,首先判断磁盘上的缓存是否存在,如果存在就直接用缓存了,如果不存在的话判断一下图片加载的时候是否是允许磁盘缓存的,若允许,调用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一个方法,再看一下这个方法:
private boolean downloadImage() throws IOException {
//调用getDownloader去下载图片
InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());
if (is == null) {
L.e(ERROR_NO_IMAGE_STREAM, memoryCacheKey);
return false;
} else {
try {
return configuration.diskCache.save(uri, is, this);
} finally {
IoUtils.closeSilently(is);
}
}
}
这里调用了getDownloader得到与当前状态相匹配的加载器去加载流文件。到这里tryLoadBitmap大体上看完了。回头继续来走run中的方法逻辑,在加载了图片后判断一下是否需要对图片进行预处理,如果需要则调用相应处理器处理。然后看一下图片是否需要在内存缓存,如果需要将其放入代表内存缓存的map中。而后看一下图片是否需要后处理,如果需要则处理。而后创建出了一个DisplayBitmapTask类的对象,并且调用了runTask方法,我们先来看一下runTask方法:
static void runTask(Runnable r, boolean sync, Handler handler, ImageLoaderEngine engine) {
if (sync) {
r.run();
} else if (handler == null) {
engine.fireCallback(r);
} else {
handler.post(r);
}
}
可以看到它的作用不管是提交也好,运行也好,目的都是让Runnable去执行,那么这个Runnable是什么呢,没错就是刚刚创建出的DisplayBitmapTask类的对象。如果细心的话可能会发现在ProcessAndDisplayImageTask我们也创建了这个类的对象。既然这个类是一个Runnable,那我们就看一下它的run方法:
public void run() {
//判断当前的imageAware是否被GC回收 如果被回收 直接调用取消下载的接口
if (imageAware.isCollected()) {
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else if (isViewWasReused()) {//判断imageAware是否被复用 如果被复用 取消下载接口
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else {
//调用displayer展示图片
L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);
displayer.display(bitmap, imageAware, loadedFrom);
//将imageWare从正在下载的map中移除
engine.cancelDisplayTaskFor(imageAware);
//调用下载完成的回调
listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
}
}
注释已经写出了这个方法的作用,我们主要关注的应该是它展示图片的时候,所以这里看一下display方法:
void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom);
它是接口中声明的一个方法,我们找一下接口的实现类,这里就看一下SimpleBitmapDisplayer
public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) {
imageAware.setImageBitmap(bitmap);
}
可以看到这里简单的给ImageAware设置了一个图片。到这里为止run方法就已经结束了,我们也看到了图片时怎么从加载到展示的过程。这里没有去仔细分析Downloader和一些类的代码细节,如果有兴趣可以去深入一下代码细节和设计上的细节~
在这里就不去介绍Universal Image Loader简单的用法了,如果还没有用过的朋友可以直接看一下该框架中自带的Demo,没有太多复杂的地方。
在使用UIL的时候(Universal Image Loader下文中都将写为UIL),我们需要先在Application中配置一下,以下的内容是自带的Demo中的配置信息:
ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context); config.threadPriority(Thread.NORM_PRIORITY - 2);//在这里设置了正常的配置线程 为3个线程共同工作 config.denyCacheImageMultipleSizesInMemory(); config.diskCacheFileNameGenerator(new Md5FileNameGenerator());//设置缓存文件的名字为MD5写入到文件中 config.diskCacheSize(50 * 1024 * 1024); // 50 MiB config.tasksProcessingOrder(QueueProcessingType.LIFO); config.writeDebugLogs 4000 (); // Remove for release app // Initialize ImageLoader with configuration. ImageLoader.getInstance().init(config.build());
我们最开始接触的就是ImageLoaderConfiguration类,就先从它开始看起。可以看到这个类非常的长,但是仔细观察会发现主题的代码却没有多少:
public final class ImageLoaderConfiguration { // 在获取图片最大尺寸时会用到 final Resources resources; // 内存缓存图片的最大宽度。 final int maxImageWidthForMemoryCache; // 内存缓存图片的最大高度。 final int maxImageHeightForMemoryCache; // 磁盘缓存图片的最大宽度。 final int maxImageWidthForDiskCache; // 磁盘缓存图片的最大高度。 final int maxImageHeightForDiskCache; // 处理磁盘缓存图片的处理器 final BitmapProcessor processorForDiskCache; // 执行从网络(或者其他地方)获取图片任务的线程池 final Executor taskExecutor; // 执行从缓存中获取图片任务的线程池 final Executor taskExecutorForCachedImages; // 用户是否自定义了taskExecutor。 final boolean customExecutor; // 用户是否自定义了taskExecutorForCachedImages。 final boolean customExecutorForCachedImages; // 上述线程池的核心线程数 final int threadPoolSize; // 线程池中线程的优先级 final int threadPriority; // 线程池中队列的类型 final QueueProcessingType tasksProcessingType; // 内存缓存接口。 final MemoryCache memoryCache; // 磁盘缓存接口 final DiskCache diskCache; // 图片下载器 final ImageDownloader downloader; // 图片解码器 final ImageDecoder decoder; // 显示图片的配置 final DisplayImageOptions defaultDisplayImageOptions; // 不允许访问网络的图片下载器 final ImageDownloader networkDeniedDownloader; // 慢网络情况下的图片下载器。 final ImageDownloader slowNetworkDownloader; 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); } public static ImageLoaderConfiguration createDefault(Context context) { return new Builder(context).build(); } ImageSize getMaxImageSize() { DisplayMetrics displayMetrics = resources.getDisplayMetrics(); int width = maxImageWidthForMemoryCache; if (width <= 0) { width = displayMetrics.widthPixels; } int height = maxImageHeightForMemoryCache; if (height <= 0) { height = displayMetrics.heightPixels; } return new ImageSize(width, height); } }
在上面代码的注释中已经说明了各个属性的作用,里面涉及到的类在接下来的分析中如果有涉及将会解释。在这个类的构造方法中使用了构造者模式,在参数较多的情况下保证了良好的可读性。其内部类Builder(也就是构造者)中的属性和ImageLoaderConfiguration中大致相同,其采用了链表的形式,即在保存了一个传递参数后返回自身的引用,如下所示:
public Builder imageDownloader(ImageDownloader imageDownloader) { this.downloader = imageDownloader; return this; } public Builder imageDecoder(ImageDecoder imageDecoder) { this.decoder = imageDecoder; return this; }
在这里只列举出两个举例。我们可以使用.的方式来进行构建,如下所示:
config.threadPriority(Thread.NORM_PRIORITY - 2). denyCacheImageMultipleSizesInMemory(). diskCacheFileNameGenerator(new Md5FileNameGenerator()). diskCacheSize(50 * 1024 * 1024). build();
其中重点看一下build方法:
public ImageLoaderConfiguration build() { initEmptyFieldsWithDefaultValues(); return new ImageLoaderConfiguration(this); }
再看一下initEmptyFieldsWithDefaultValues方法:
private void initEmptyFieldsWithDefaultValues() { if (taskExecutor == null) { taskExecutor = DefaultConfigurationFactory.createExecutor( threadPoolSize, threadPriority, tasksProcessingType); } else { customExecutor = true; } if (taskExecutorForCachedImages == null) { taskExecutorForCachedImages = DefaultConfigurationFactory .createExecutor(threadPoolSize, threadPriority, tasksProcessingType); } else { customExecutorForCachedImages = true; } if (diskCache == null) { if (diskCacheFileNameGenerator == null) { diskCacheFileNameGenerator = DefaultConfigurationFactory .createFileNameGenerator(); } diskCache = DefaultConfigurationFactory.createDiskCache( context, diskCacheFileNameGenerator, diskCacheSize, diskCacheFileCount); } if (memoryCache == null) { memoryCache = DefaultConfigurationFactory.createMemoryCache( context, memoryCacheSize); } if (denyCacheImageMultipleSizesInMemory) { memoryCache = new FuzzyKeyMemoryCache(memoryCache, MemoryCacheUtils.createFuzzyKeyComparator()); } if (downloader == null) { downloader = DefaultConfigurationFactory .createImageDownloader(context); } if (decoder == null) { decoder = DefaultConfigurationFactory .createImageDecoder(writeLogs); } if (defaultDisplayImageOptions == null) { defaultDisplayImageOptions = DisplayImageOptions.createSimple(); } }
在其中给一些用户没有手动设置的属性进行了初始化,然后在build方法中返回了一个ImageLoaderConfiguration类的实例。
到这里为止我们就将ImageLoaderConfiguration这个类简单的看了一遍,如果有兴趣可以深入的去读一下代码。
由于在Application中写了这样一行代码:ImageLoader.getInstance().init(config.build()); 我们来看一下整个框架中最主要的一个类ImageLoader。
首先是获取实例的方法,采用了单例模式来实现。
private volatile static ImageLoader instance; /** Returns singleton class instance */ public static ImageLoader getInstance() { if (instance == null) { synchronized (ImageLoader.class) { if (instance == null) { instance = new ImageLoader(); } } } return instance; }
使用了双重检查来提高性能,并且这种写法需要将实例对象volatile保证可见性。
而后是init方法:
public synchronized void init(ImageLoaderConfiguration configuration) { if (configuration == null) { throw new IllegalArgumentException(ERROR_INIT_CONFIG_WITH_NULL); } if (this.configuration == null) { L.d(LOG_INIT_CONFIG);//"Initialize ImageLoader with configuration" engine = new ImageLoaderEngine(configuration); this.configuration = configuration; } else {//"Try to initialize ImageLoader which had already been initialized before. " + "To re-init ImageLoader with new confi // guration call ImageLoader.destroy() at first." L.w(WARNING_RE_INIT_CONFIG); } }
简单的保存了一下ImageLoaderConfiguration,然后用它创建出了一个叫engin的对象,定义如下:
private ImageLoaderEngine engine;
它的作用是一个分发器,将任务分发到线程池中来执行,在后面会详细的对它进行分析。
这样我们的配置工作就结束了,但是光有配置也没用,UIL给我们提供了几种加载图片的方法:
public void loadImage(String uri, ImageLoadingListener listener) { loadImage(uri, null, null, listener, null); } public void loadImage(String uri, ImageSize targetImageSize, ImageLoadingListener listener) { loadImage(uri, targetImageSize, null, listener, null); } public void loadImage(String uri, DisplayImageOptions options, ImageLoadingListener listener) { loadImage(uri, null, options, listener, null); } public void loadImage(String uri, ImageSize targetImageSize, DisplayImageOptions options, ImageLoadingListener listener) { loadImage(uri, targetImageSize, options, listener, null); } public void displayImage(String uri, ImageAware imageAware) { displayImage(uri, imageAware, null, null, null); } public void displayImage(String uri, ImageAware imageAware, ImageLoadingListener listener) { displayImage(uri, imageAware, null, listener, null); } public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options) { displayImage(uri, imageAware, options, null, null); } public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options, ImageLoadingListener listener) { displayImage(uri, imageAware, options, listener, null); } public void displayImage(String uri, ImageView imageView) { displayImage(uri, new ImageViewAware(imageView), null, null, null); } public void displayImage(String uri, ImageView imageView, DisplayImageOptions options) { displayImage(uri, new ImageViewAware(imageView), options, null, null); } public void displayImage(String uri, ImageView imageView, ImageLoadingListener listener) { displayImage(uri, new ImageViewAware(imageView), null, listener, null); } public void displayImage(String uri, ImageView imageView, DisplayImageOptions options, ImageLoadingListener listener) { displayImage(uri, imageView, options, listener, null); } public void displayImage(String uri, ImageView imageView, DisplayImageOptions options, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) { displayImage(uri, new ImageViewAware(imageView), options, listener, progressListener); } public Bitmap loadImageSync(String uri) { return loadImageSync(uri, null, null); } public Bitmap loadImageSync(String uri, DisplayImageOptions options) { return loadImageSync(uri, null, options); } public Bitmap loadImageSync(String uri, ImageSize targetImageSize) { return loadImageSync(uri, targetImageSize, null); }
一共有三种加载图片的方法,看名字也大概可以猜出含义,第一种是简单的加载图片,第二种是显示图片,第三种是同步加载图片。
先来看第一种最终调用的方法:
public void loadImage(String uri, ImageSize targetImageSize, DisplayImageOptions options, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) { checkConfiguration(); //如果当前的目标大小用户没有传入的话 直接利用configuration里面默认的最大的ImageSize信息 if (targetImageSize == null) { targetImageSize = configuration.getMaxImageSize(); } if (options == null) { options = configuration.defaultDisplayImageOptions; } NonViewAware imageAware = new NonViewAware(uri, targetImageSize, ViewScaleType.CROP); displayImage(uri, imageAware, options, listener, progressListener); }
可以看到这个方法就简单的检查了一下configuration后创建出了一个ViewAware包装类(后面会有介绍)后调用了displayImage的一个重载方法。
再来看一下第三种最终调用的方法:
public Bitmap loadImageSync(String uri, ImageSize targetImageSize, DisplayImageOptions options) { if (options == null) { options = configuration.defaultDisplayImageOptions; } options = new DisplayImageOptions.Builder().cloneFrom(options).syncLoading(true).build(); SyncImageLoadingListener listener = new SyncImageLoadingListener(); loadImage(uri, targetImageSize, options, listener); return listener.getLoadedBitmap(); }
在这个方法中最终调用的是loadImage的一个重载方法,那么它最终也会调用到displayImage方法。不同的就是由于这个方法是同步的,所以它可以直接返回加载出来的Bitmap。
那么现在来看一下第二种最终调用的方法,所有的方法也都会调用到它身上:
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) { //检查configuration是否为空,如果为空,抛异常 checkConfiguration(); //当图片展示的包装类为空的时候抛异常,这也就是为什么loadImage的时候仍然需要传入一个包装类 if (imageAware == null) { throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS); } //加载中监听器为空创建一个默认的 if (listener == null) { listener = defaultListener; } //展示图片的配置选项为空时创建一个默认的 if (options == null) { options = configuration.defaultDisplayImageOptions; } //uri为空时取消ImageLoaderEngine对任务的分发,而后判断是否有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; } //根据要显示图片的控件和最大的图片大小来生成一个图片的尺寸 ImageSize targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize()); //以当前Uri和尺寸为关键字来生成缓存的key String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize); //ImageLoaderEngine完成加载前的准备工作,主要是将缓存key和加载图片控件的ID关联起来放到一个map中 engine.prepareDisplayTaskFor(imageAware, memoryCacheKey); //回调listener中的方法 listener.onLoadingStarted(uri, imageAware.getWrappedView()); //从configuration保存的缓存实例对象中使用当前的缓存 f59c key尝试着拿一下缓存 Bitmap bmp = configuration.memoryCache.get(memoryCacheKey); //当这个缓存的Bitmap存在并且还没有被回收 if (bmp != null && !bmp.isRecycled()) { L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey); //如果当前的图片展示选项中需要对图片进行后续处理 if (options.shouldPostProcess()) { //封装一个ImageLoadingInfo对象 ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, progressListener, engine.getLockForUri(uri)); //创建出处理并且展示图片的任务,本质是一个Runnable ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo, defineHandler(options)); //在同步的情况下直接运行 if (options.isSyncLoading()) { displayTask.run(); } else {//在异步的情况下使用ImageLoaderEngine将其分发到线程池中去执行 engine.submit(displayTask); } } else {//不需要对图片进行后续处理 //直接对图片进行展示 options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE); //回调监听 listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp); } } else {//当前缓存的Bitmap不存在或者已经被回收,需要重新从磁盘或者网络加载 //看一下是否需要展示一个代表加载中的图片 if (options.shouldShowImageOnLoading()) { imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources)); } else if (options.isResetViewBeforeLoading()) { imageAware.setImageDrawable(null); } //使用一个ImageLoadingInfo对象包装加载信息 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); } } }
在上面的代码中给出了整体流程的注释,下载来看一下其中涉及到的细枝末节的类与方法。
就从这个方法的参数开始说起吧,首先ImageAware到底是个什么,可以看到它是个接口,以下是接口的定义:
public interface ImageAware { int getWidth(); int getHeight(); ViewScaleType getScaleType(); View getWrappedView(); boolean isCollected(); int getId(); boolean setImageDrawable(Drawable drawable); boolean setImageBitmap(Bitmap bitmap); <span style="font-size:18px;">}</span>
在这里因为篇幅的原因把注释都删掉了,大致的意思就是包装了一下显示图片控件的宽高,图片的缩放模式,控件的ID,展示的图片啊这些的内容,如果有兴趣也可以读一下注释,写的非常清楚。在正常情况下我们都使用ImageView来实现图片的展示,那么ImageView是如何转换为ImageAware的呢,可以随便拿一个displayImage方法来看一下:
public void displayImage(String uri, ImageView imageView, DisplayImageOptions options) { displayImage(uri, new ImageViewAware(imageView), options, null, null); }
使用了一个ImageViewAware来包装了ImageView。这里就不再深入代码细节了,但是有一点需要注意,就是UIL的这种模式是对扩展开放的,如果我们有一个自定义的控件,只需要为它写一个适配器(Adapter模式)ImageAware就可以去使用了,非常的灵活!
再来看一下刚分析的displayImage方法中的参数DisplayImageOptions,从名字上可以看出来它是展示图片的配置信息,同样使用构造者模式来进行组装,如果有兴趣可以看一下代码,属性的名字也非常直观,这里就不一一列举说明了。
还有一个没有用到的参数ImageLoadingProgressListener,看一下定义:
public interface ImageLoadingProgressListener { void onProgressUpdate(String imageUri, View view, int current, int total); }
可以看到它回调了图片加载的进度。
下面接着说displayImage方法的主逻辑,该方法中有多处使用了ImageLoaderEngine类的对象,显然要了解整个方法,我们必须将这个类看一下,首先是构造方法:
ImageLoaderEngine(ImageLoaderConfiguration configuration) { this.configuration = configuration; taskExecutor = configuration.taskExecutor; taskExecutorForCachedImages = configuration.taskExecutorForCachedImages; taskDistributor = DefaultConfigurationFactory.createTaskDistributor(); }
前几句只是简单的将configuration的值赋给了ImageLoaderEngine中的属性,这个就不再说了,如果忘记了是什么意思可以看一下前面。最后一行创建出了一个分发任务的线程池。
在这个类中还有一些属性是很重要的,来看一下:
//key为ImageAware的id,value为该控件显示的图片的Uri private final Map<Integer, String> cacheKeysForImageAwares = Collections .synchronizedMap(new HashMap<Integer, String>()); //图片正在加载的重入锁 map,key 为图片的 uri,value 为标识其正在加载的重入锁。 private final Map<String, ReentrantLock> uriLocks = new WeakHashMap<String, ReentrantLock>(); //当前加载图片是否被暂停。为true则所有加载图片的事件都会暂停。 private final AtomicBoolean paused = new AtomicBoolean(false); //是否禁止网络访问,为true则网络访问失效 private final AtomicBoolean networkDenied = new AtomicBoolean(false); //当前是否为慢网络情况,如果为true则调用SlowNetworkImageDownloader加载图片 private final AtomicBoolean slowNetwork = new AtomicBoolean(false); //在engine被暂停后调用这个锁等待。 private final Object pauseLock = new Object();
类中的方法比较简单,在这里只看一下我们用到的,如果有兴趣可以读一下:
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); } } }); }
如果还有印象的话可以知道这个方法的调用是在内存缓存不存在或者被回收的时候。首先查看一下当前磁盘上是否有这个Uri的缓存内容,然后调用initExecutorsIfNeed在线程池不存在的时候初始化一下线程池,如果当前在磁盘上有缓存的话,将其交给taskExecutorForCachedImages执行,如果没有缓存需要从网络拉取的话,交给taskExecutor来执行。
void submit(ProcessAndDisplayImageTask task) { initExecutorsIfNeed(); taskExecutorForCachedImages.execute(task); }
调用这个方法的前提是在内存里有对应的缓存并且没有被回收,那么直接交给taskExecutorForCachedImages来执行。
void prepareDisplayTaskFor(ImageAware imageAware, String memoryCacheKey) { //将控件中所加载的URI传入相应的map中来进行保存 cacheKeysForImageAwares.put(imageAware.getId(), memoryCacheKey); } void cancelDisplayTaskFor(ImageAware imageAware) { //取消对map中数据的保存 cacheKeysForImageAwares.remove(imageAware.getId()); }
这两个方法也是刚才用到的,分别为准备分发任务和取消任务,对应的仅仅是将显示图片控件id为key的实例从map中移除了。
在displayImage中我们还剩下三个类没有去看,分别是ImageLoadingInfo,ProcessAndDisplayImageTask和LoadAndDisplayImageTask。先来看ImageLoadingInfo:
final class ImageLoadingInfo { final String uri; final String memoryCacheKey; final ImageAware imageAware; final ImageSize targetSize; final DisplayImageOptions options; final ImageLoadingListener listener; final ImageLoadingProgressListener progressListener; final ReentrantLock loadFromUriLock; public ImageLoadingInfo(String uri, ImageAware imageAware, ImageSize targetSize, String memoryCacheKey, DisplayImageOptions options, ImageLoadingListener listener, ImageLoadingProgressListener progressListener, ReentrantLock loadFromUriLock) { this.uri = uri; this.imageAware = imageAware; this.targetSize = targetSize; this.options = options; this.listener = listener; this.progressListener = progressListener; this.loadFromUriLock = loadFromUriLock; this.memoryCacheKey = memoryCacheKey; } }
这个类非常的简单,包装了要加载图片的相关信息,并且将属性设置为final类型,保证我们不能去修改。这个类中属性的含义前面都有提及,这里就不多加赘述。
第二个是ProcessAndDisplayImageTask类,这是一个Runnable,来直接看一下run方法:
@Override public void run() { L.d(LOG_POSTPROCESS_IMAGE, imageLoadingInfo.memoryCacheKey); BitmapProcessor processor = imageLoadingInfo.options.getPostProcessor(); Bitmap processedBitmap = processor.process(bitmap); DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(processedBitmap, imageLoadingInfo, engine, LoadedFrom.MEMORY_CACHE); LoadAndDisplayImageTask.runTask(displayBitmapTask, imageLoadingInfo.options.isSyncLoading(), handler, engine); }
可以看到在经过后处理以后,调用了LoadAndDisplayImageTask类的runTask方法,由于在LoadAndDisplayImageTask的run方法中最后也将调用runTask,所以我们先来看一下LoadAndDisplayImageTask的run方法,然后一起来看runTask:
public void run() {
if (waitIfPaused()) return;
if (delayIfNeed()) return;
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 {
checkTaskNotActual();
//从内存中去读取bitmap
bmp = configuration.memoryCache.get(memoryCacheKey);
//当bitmap为空 或者已经被回收了的时候
if (bmp == null || bmp.isRecycled()) {
//尝试着去加载bitmap
bmp = tryLoadBitmap();
if (bmp == null) return; // listener callback already was fired
checkTaskNotActual();
checkTaskInterrupted();
//当需要对bitmap进行处理的时候进行处理
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);
//当需要在内存中进行缓存的话 直接将memoryKey放入到map中进行缓存
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);
}
方法中还是先判断了一下在内存中是否存在bitmap实例,如果存在的话打一个标记,过一会在构建展示图片任务的时候将会用到。如果不存在的话调用tryLoadBitmap方法去加载图片,方法体如下:
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
try {
//从磁盘中获取相应的uri
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 = LoadedFrom.DISC_CACHE;
checkTaskNotActual();
//从磁盘的缓存中读取相应的Bitmap文件
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());
}
}
checkTaskNotActual();
//解码出bitmap信息
bitmap = decodeImage(imageUriForDecoding);
//当bitmap为空的时候标记为失败
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;
}
方法注释写的比较详细了,这里简单的在看一下,首先判断磁盘上的缓存是否存在,如果存在就直接用缓存了,如果不存在的话判断一下图片加载的时候是否是允许磁盘缓存的,若允许,调用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一个方法,再看一下这个方法:
private boolean downloadImage() throws IOException {
//调用getDownloader去下载图片
InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());
if (is == null) {
L.e(ERROR_NO_IMAGE_STREAM, memoryCacheKey);
return false;
} else {
try {
return configuration.diskCache.save(uri, is, this);
} finally {
IoUtils.closeSilently(is);
}
}
}
这里调用了getDownloader得到与当前状态相匹配的加载器去加载流文件。到这里tryLoadBitmap大体上看完了。回头继续来走run中的方法逻辑,在加载了图片后判断一下是否需要对图片进行预处理,如果需要则调用相应处理器处理。然后看一下图片是否需要在内存缓存,如果需要将其放入代表内存缓存的map中。而后看一下图片是否需要后处理,如果需要则处理。而后创建出了一个DisplayBitmapTask类的对象,并且调用了runTask方法,我们先来看一下runTask方法:
static void runTask(Runnable r, boolean sync, Handler handler, ImageLoaderEngine engine) {
if (sync) {
r.run();
} else if (handler == null) {
engine.fireCallback(r);
} else {
handler.post(r);
}
}
可以看到它的作用不管是提交也好,运行也好,目的都是让Runnable去执行,那么这个Runnable是什么呢,没错就是刚刚创建出的DisplayBitmapTask类的对象。如果细心的话可能会发现在ProcessAndDisplayImageTask我们也创建了这个类的对象。既然这个类是一个Runnable,那我们就看一下它的run方法:
public void run() {
//判断当前的imageAware是否被GC回收 如果被回收 直接调用取消下载的接口
if (imageAware.isCollected()) {
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else if (isViewWasReused()) {//判断imageAware是否被复用 如果被复用 取消下载接口
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else {
//调用displayer展示图片
L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);
displayer.display(bitmap, imageAware, loadedFrom);
//将imageWare从正在下载的map中移除
engine.cancelDisplayTaskFor(imageAware);
//调用下载完成的回调
listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
}
}
注释已经写出了这个方法的作用,我们主要关注的应该是它展示图片的时候,所以这里看一下display方法:
void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom);
它是接口中声明的一个方法,我们找一下接口的实现类,这里就看一下SimpleBitmapDisplayer
public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) {
imageAware.setImageBitmap(bitmap);
}
可以看到这里简单的给ImageAware设置了一个图片。到这里为止run方法就已经结束了,我们也看到了图片时怎么从加载到展示的过程。这里没有去仔细分析Downloader和一些类的代码细节,如果有兴趣可以去深入一下代码细节和设计上的细节~
相关文章推荐
- ACM比赛的技巧
- HTML应用程序(HTML App)
- 154.Oracle数据库SQL开发之 JAVA——使用JDBC包
- 转发,Servlet异常
- 我的少女时代
- process launch failed: Security
- MFC主线程使用WaitForSingleObject阻塞的问题 http://blog.csdn.net/sysprogram/article/details/17383455
- iOS 8 Auto Layout界面自动布局系列2-使用Xcode的Interface Builder添加布局约束
- MySql中存储过程的用法
- 递归问题
- [TwistedFate]图片异步加载,KVO
- pycharm 中文
- mac vim 使用记录
- 教你如何配置Ubuntu用于高效、高质量的发送邮件
- IOS 高效利用Xcode
- Spark核心概念
- Servlet生命周期和JSP
- cookie 和session 的区别详解
- 重定向和MySql插入中文
- dede 两个网站共用一个数据库图片路径问题