Android-Universal-Image-Loader
这个框架是我接触的第一个Android图片加载框架,有种亲切感,因此选择这个作为第一篇源码解析的框架。
一、基本用法
1.在Application中进行init操作
/** * 初始化Universal-Image-Loader */ private void initImageLoader() { DisplayImageOptions defaultOptions = new DisplayImageOptions.Builder() .showImageForEmptyUri(R.mipmap.default_image) .cacheInMemory(true) .cacheOnDisk(true) // ...其他显示配置 .build(); ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).defaultDisplayImageOptions(defaultOptions) .diskCacheFileCount(100) /*.writeDebugLogs()*/ .build(); ImageLoader.getInstance().init(config); }
2.显示图片
ImageLoader.getInstance().displayImage(url, imageView);这里只是举了个最简单的栗子,更多的操作请查看api或者本文下面的介绍。
注意,需要网络和磁盘写入权限。
二、源码分析
1.加载流程
首先放一张作者在git上给出的加载与现实流程图:
这张图基本讲述了整个loader的加载流程,带着图来看就会相对容易很多。
2.库结构
看看其库的结构,如下图,
命名很清晰,基本上可以看出其分为cache、core和utils,cache分为磁盘缓存(disk)和内存缓存(memory),util为一些工具类,其核心就是core。
core包含:
ImageDecoder:图片解码器,将InputStream转换为Bitmap;
BitmapDisplayer:图片显示器,显示图片到对应ImageAware;
ImageDownloader:图片下载器,负责加载各种来源的图片;
ImageAware:需要图片的对象,库中已经对ImageView进行包装;
ImageLoadingListener:图片加载监听器,用于定义图片加载过程中的回调;
BitmapProcessor:图片处理器,对图片进行预处理等;
ImageLoaderEngine:负责将图片加载任务分发到各个线程执行。
当然还有一些加载配置相关类和任务类。下面进行一一分析。
3.核心类
ImageLoader.java
(接触最多的也就是这个类,就先从这个类来好了)
ImageLoader采用单例模式,主要用于向用户提供API,主要是用于向用户提供图片加载相关配置初始化,显示与加载图片,加载任务的取消等操作接口。
1)getInstance():采用双重检查加锁方式(提高性能),实现单例模式。
2)init(ImageLoaderConfiguration):初始化加载相关配置;参数不能为空,否则抛出异常;如果已经初始化过,则需要先destroy再init,否则会log出警告;
3)displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener):这个方法是该类的核心方法,我们可以看到所有的displayImage、loadImage、loadImageSync方法都直接或者间接的调用了该方法。因此只详细分析该方法。
参数:
uri:图片uri,可以支持http,https,file,content,assets,drawable图片加载显示,但是作者并不建议把所有的图片加载都用该框架,比如我们drawable中的图片,推荐肯定是使用Android原生的方法调用;
imageAware:该参数是指需要加载图片的类的包装,其实大多数情况下就是ImageView,但是其提供了加载处理需要的一些参数,当然也是从包装的View中获得的;
options:解码和显示图片时相关配置,如果为null,则会使用默认的参数;
targetSize:需要的图片尺寸,如果为空,则依赖于ImageAware中的view尺寸,其实所有displayImage方法中,都是传递null,都是依赖于需要显示的view,而在loadImage中,因为不知道用途,所以需要传递尺寸,才会需要该值;
listener:图片加载过程回调,如果是在UI线程则会直接出发回调,如果不是,则使用handler;
progressListener:图片加载进度回调,如果在UI线程会直接回调,不是则用handler;在磁盘上缓存时,如果回调,还要在options中设置。
代码就不上了,根据其方法实现,绘制了一张流程图:
首先判断是否是空的地址,如果是就显示空占位图,如果不是,就从内存缓存取,有图片就用,没有则到磁盘或者网络上取。思路很清晰,就不做过多的解释了;
4)getMemoryCache()、clearMemoryCache()、getDiskCahce()、clearDiskCache():顾名思义,磁盘和内存缓存的获取与清除;
5)还有就是加载任务引擎的暂停pause(),启动resume();
6)destroy():该方法主要是当默认的加载配置(ImageLoaderConfiguration)需要变化时,要先destroy清除配置,再重新的进行初始化,否则不会有效果。
ImageLoaderConfiguration.java与DisplayImageOptions.java
这两个类主要是进行一些相关配置,Configuration是加载的配置,而Options是显示的配置。具体说一下有哪些相关配置。
ImageLoaderConfiguration包含:
maxImageWidthForMemoryCahce、maxImageHeightForMemmoryCache、maxImageWidthForDiskCache、maxImageHeightForDiskCache(对应缓存中尺寸最值),processorForDiskCache(磁盘缓存前的预处理器);
taskExecutor(自定义的执行执行图片加载任务的executor)、taskExecutorForCachedImages(执行从磁盘获取缓存任务的executor)、customExecutor(是否自定义上述的Executor,默认false)、customExecutorForCachedImages(是否自定义上述的Executor,默认false);
threadPoolSize(线程池大小)、threadPriority(线程优先级)、denyCacheImageMultipleSizeInMemerory(拒绝缓存同一图片的不同尺寸图片,默认是false)、tasksProcesssingType(加载与显示的队列处理类型);
memoryCacheSize(内存缓存大小)、diskCacheSize(磁盘缓存大小)、diskCacheFileCount(最多缓存图片数量)、memoryCache(内存缓存)、DiskCache(磁盘缓存)、diskCacheFileNameGenerator(磁盘缓存文件名称生成器);
downLoader(图片下载器)、decoder(图片解码器);
defaultDisplayImageOptions(默认的显示配置,这里是全局设置的一个默认配置,同时也可以在displayImage...方法中使用临时的配置);
writeLogs(是否写log日志这个一般要在证实版本中置为false);
DisplayImageOptions包含:
imageResOnLoading,imageOnLoading、imageResForEmptyUti,imageFroEmptyUri、imageResOnFail,imageOnFail(正在加载、空uri、加载失败显示的占位图,前面的Res优先级高于后者);
resetViewBeforeLoading(ImageWare是否被重置);
cacheInMemory,cacheOnDisk(是否缓存在指定缓存);
imageSacleType(图片解码缩放类型)、decodingOptions(BitmapFactory的解码选项)、delayBeforeLoading(加载图片前延迟时间)、considerExifParams(是否考虑JPEG图片的EXIF参数);
preProcessor(内存缓存前预处理);postProcessor(图片显示前预处理);displayer(自定义图片加载显示器);
handler(这个不用说了吧);
isSyncLoading(是否同步加载);
这两个类都采用构建者模式来实现参数配置,内部都有一个静态Builder类。Options类中提供一个cloneFrom(DisplayImageOptions options)方法,方便创克隆建临时的显示参数,但其build方法并没有给出有些未配置的参数的默认实现。Configuration类则在build方法中调用initEmptyFiledsWidthDefaultValues()方法,用以提供一些未配置的选项的默认实现。其大部分都是通过DefaultConfigurationFactory这个工厂类来创建的。
DefaultConfigurationFactory.java
该类主要用于生成一些默认的参数,主要方法包括:
生成任务执行池(executor:ThreadPoolExecutor)、任务分发器(taskDistributor:CachedThreadPool);
文件命名生成器(fileNameGenerator:HashCodeFielNameGenerator)、磁盘缓存(diskCache:如果设置了最大缓存大小或数量,则为LruDiskCache,否则是UnLimitedDiskCache)、内存缓存(memoryCache:LruMemoryCache);
图片加载器(imageDownloader:BaseImageDownLoader)、图片解码器(imageDecoder:BaseImageDecoder)、图片显示器(bitmapDisplay:SimpleBitmapDisplayer)以及一个默认的ThreadFactory。
具体方法名我就不写出来了。
ImageLoaderEngine.java
该类主要用于显示任务的执行。其中包括:
ImageLoaderConfiguration(配置信息);
taskExecutor(执行从源获取图片任务的executor)、taskExecutorForCachedImages(执行从缓存获取图片任务的executor)、taskDistributor(任务分发池,分发LoadAndDisplayImageTask和ProcessAndDisplayImageTask);
cacheKeysForImageAwares(一个缓存ImageAware任务的map,其中key为ImageAware中包装的View的Id,value为内存缓存中ImageAware要下载的uri生成的key,这里缓存key使得之前的执行任务都取消,在显示时可以比对id与uri的对应的值,防止了图片显示错位的问题(比如ListView中),和保存tag防止错位是一样的原理);
uriLocks(图片正在加载的重入锁map)、paused(是否暂停执行任务)、networkDenied(是否拒绝网络访问)、slowNetwork(是否是慢网络情况)、pauseLock(暂停等待锁)。
主要方法:
submit(LoadAndDisplayImageTask):提交LoadAndDisplayImageTask任务,实现中,如果磁盘包含该文件,则使用taskExecutorForCachedImages执行否则使用taskExecutor执行。
submit(ProcessAndDisplayImageTask):直接交由taskExecutorForCachedImages执行。
prepareDisplayTaskFor(ImageAware)与cancelDisplayTaskFor(ImageAware):向cacheKeysForImageAwares添加与删除对应的ImageAware。
其他就不赘述了,其核心就在于几个任务执行器Executor以及这两个submit方法。
接下来看一下几个Task的执行过程,进一步了解图片的加载,解码与处理操作。我们从Engine的两个submit开始。所谓Task,就是实现了Runnable接口的类,这样接可以直接交由Executor去执行。这里涉及到命令模式,想了解请自行去网上荡。
ProcessAndDisplayTask.java
先看一下ProcessAndDisplayTask,由于该Task不需要加载图片(已经从内存缓存中取出),所以可以直接交给taskExecutorForCachedImages直接进行execut。在该Task的run方法中,先用BitmapProcessor对Bitmap进行处理,之后又创建了一个DisplayBitmapTask,再调用LoadAndDisplayImageTask的runTask静态方法进行显示。
先看一下这个static方法:
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); } }这个方法中我们可以看到,先判断是否是同步进行,同步则直接调用run方法在调用该方法的线程中执行,如果handler为空,则交由engine的分发器taskDistributor将该方法分发出去,否则直接调用hander的post方法,交由创建该Handler的线程去处理。查看配置handler的代码,如果该handler没有指定,即为null时,如果是在主线程中,则handler即为主线程的handler,则可以直接处理view相关的代码,一般情况下我们都是不指定handler且不同步,即默认在主线程调用。
再看一下这个DisplayBitmapTask的run方法
@Override 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); } } /** Checks whether memory cache key (image URI) for current ImageAware is actual */ private boolean isViewWasReused() { String currentCacheKey = engine.getLoadingUriForView(imageAware); return !memoryCacheKey.equals(currentCacheKey); }
这个DisplayBitmapTask,顾名思义,就是显示图片的task。在其run方法中可以看出,首先判断ImageAware是否被GC回收了,如果回收了则回调加载取消函数,在isViewWasReused()方法中看到从engine中取出保存的ImageAware对应的key也就是uri生成的值,如果imageAware要显示的uri已经发生了变化,就不再显示,而是回调加载取消。这里与ListView中使用tag来标记加载的图片,防止因为View复用图片加载不正确的方式有异曲同工之妙。最后才调用BitmapDisplay来显示图片到ImageAware中,然后清理掉imageAware对应的显示任务,并回调加载完成。
LoadAndDisplayImageTask.java
1)run方法如下:
@Override 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(); 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(); } DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom); runTask(displayBitmapTask, syncLoading, handler, engine); }来张流程图:
2)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; 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()); } } checkTaskNotActual(); 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; }这个就不画图了,直接解释一下,首先从磁盘缓存中取出图片,如果ImageAware没被回收,且uri正确,则对其进行解码获取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; } private boolean downloadImage() throws IOException { 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); } } } /** Decodes image file into Bitmap, resize it and save it back */ private boolean resizeAndSaveImage(int maxWidth, int maxHeight) throws IOException { // Decode image file, compress and re-save it boolean saved = false; File targetFile = configuration.diskCache.get(uri); if (targetFile != null && targetFile.exists()) { ImageSize targetImageSize = new ImageSize(maxWidth, maxHeight); DisplayImageOptions specialOptions = new DisplayImageOptions.Builder().cloneFrom(options) .imageScaleType(ImageScaleType.IN_SAMPLE_INT).build(); ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, Scheme.FILE.wrap(targetFile.getAbsolutePath()), uri, targetImageSize, ViewScaleType.FIT_INSIDE, getDownloader(), specialOptions); Bitmap bmp = decoder.decode(decodingInfo); if (bmp != null && configuration.processorForDiskCache != null) { L.d(LOG_PROCESS_IMAGE_BEFORE_CACHE_ON_DISK, memoryCacheKey); bmp = configuration.processorForDiskCache.process(bmp); if (bmp == null) { L.e(ERROR_PROCESSOR_FOR_DISK_CACHE_NULL, memoryCacheKey); } } if (bmp != null) { saved = configuration.diskCache.save(uri, bmp); bmp.recycle(); } } return saved; }
如上代码所示,先从下载器中下载,之后将输入流直接保存到磁盘。如果下载的图片尺寸过大,则根据需求将磁盘中的图片加载出来解码再重新保存起来。
核心的内容基本解析完。
4.其他辅助类
ImageDecoder
解码器,主要就是BaseImageDecoder类,其decode()方法中对流进行按尺寸比例压缩与旋转操作。
ImageDecodingInfo
该类对解码的相关信息进行封装。
BitmapDisplayer
显示器,包括CircleBitmapDisplayer(圆形图片)、FadeInBitmapDisplayer(淡入动画图片)、RoundedBitmapDisplayer(圆角图片)、RoundedVignetteBitmapDisplayer(圆角晕影图片)、SimpleBitmapDisplayer(正常显示的图片)。
ImageDownLoader
下载器,核心类为BaseImageDownLoader,其getStream()方法中分别支持对http、https、file、content、assets、drawable来源的输入流的获取。
ImageAware
主要是对图片要显示的对象的封装,其主要包含ViewAware抽象类与NonViewAware类,ImageViewAware是ViewAware的子类,一般我们显示在ImageView时,都会在ImageLoader中对其进行新建ImageAware的操作。ViewAware中对View采用Reference防止内存泄漏,并实现获取其宽高,缩放类型,以及是否回收掉,id(其实为hashcode),设置图片等方法,且提供了设置图片到View的抽象方法,用于让实现类具体实现其设置图片的方法。
ImageLoadingListener
提供加载各个生命周期的回调接口。用户可以在监听回调周期时使用SimpleImageLoadingListener类,该类提供了ImageLoadingListener接口,并为每个方法提供了空实现,我们在使用时,可以只重写其部分需要的方法即可。
ImageLoadingProgressListener
该接口提供了对加载的进度的回调,参数包括加载的uri地址,加载的对象view,以及目前已加载大小与总大小。
PauseOnScrollListener
该类实现了Android的OnScrollListener接口,主要用于在滚动时停止加载停止时继续加载的操作。同时为了不影响开发者的OnScrollListener接口的操作,在其中又提供了OnScrollListener的设置与调用。
BitmapProcessor
图片处理器,该接口提供process方法,用于实现对bitmap的预处理操作,并没有默认的实现类。
ImageLoadingInfo
该类主要是封装了要加载图片的uri,memoryCacheKey,ImageAware,ImageSize,DisplayImageOptions,ImageLoadingListener,ImageLoadingProgressListener,以及加载锁。
cache就不多说了,一般用的也就是LruMemoryCache以及DiskLruCache类。工具类和assist辅助类也不多说了。
三、总结
总结一下加载流程:当加载任务下来,调用displayImage方法时,判断是否是空的uri,是则显示空占位图。不为空则从内存缓存查找,如果有缓存且需要预处理则创建ProcessAndDisplayTask,并提交到线程池处理,不要预处理则直接显示。如果不存在缓存或已经被回收,则会创建LoadAndDisplayImageTask并提交到线程池执行。ProcessAndDisplayTask会先预处理图片,然后再显示。而LoadAndDisplayImageTask会先从磁盘中查找图片缓存,存在缓存则加载并预处理,并存储到内存缓存,之后需要则进行显示前预处理,再显示到界面,如果磁盘缓存中没有,则会到指定位置下载,下载之后对其进行解码操作(压缩,旋转等),之后再保存到磁盘与内存缓存中,最后显示出来。其中我们可以指定缓存的实现、解码器、显示器(圆角、圆形等图片)等配置。
第一次写解析源码的博客,也是第一次做源码分析。琐事较多,花了将近5天的空闲时间,不过收获还是很大的。后面也会继续有源码的分析博客出。
Android-Universal-Image-loader git地址:https://github.com/nostra13/Android-Universal-Image-Loader