Universal Image Loader源码分析
基本组件
UIL是一个强大的,高度定制的图片加载缓存器,支持:
- 支持任务线程池、下载器、解码器、内存及磁盘缓存、显示选项等等的配置。
- 包含内存缓存和磁盘缓存两级缓存。
- 支持多线程,支持异步和同步加载。
- 支持多种缓存算法、下载进度监听、ListView图片错乱解决等
每一部分都可以从中学到很多内容,本文仅从UIL架构、基本组件、重点知识点等方面出发分析,结构设计图如下:
整个库主要组件包括:
- ImageLoader 图片加载器,对外的主要API,采用单例模式,用于图片的加载和显示
- ImageLoaderEngine LoadAndDisplayImageTask和ProcessAndDisplayImageTask任务分发器,负责分发任务给具体的线程池
- ImageLoaderConfiguration ImageLoader参数配置项,包括图片最大尺寸、线程池、缓存、下载器、解码器等等
- Cache(MemoryCache & DiskCache) 详见上两篇文章
- ImageDownloader 图片下载接口
- ImageDecoder 将图片转换成Bitmap的接口
- BitmapProcessor 图片处理接口,可用于对图片预处理和后处理,比如加个水印,Bitmap process(Bitmap bitmap);
- BitmapDisplayer 在ImageAware中显示bitmap对象的接口,有圆角、动画效果的实现,默认实现SimpleBitmapDisplayer{imageAware.setImageBitmap(bitmap)}
- DisplayImageOptions 图片显示的配置项。比如加载前、加载中、加载失败应该显示的占位图片,图片是否需要在磁盘缓存,是否需要在 memory 缓存等
- ImageAware 需要显示图片的对象的接口,可包装View表示某个需要显示图片的View
- LoadAndDisplayImageTask 处理并显示图片的Task,实现了Runnable接口,用于从网络、文件系统或内存获取图片并解析,然后调用DisplayBitmapTask在ImageAware中显示图片
- ProcessAndDisplayImageTask 处理并显示图片的Task,实现了Runnable接口
- DisplayBitmapTask 显示图片的Task,实现了Runnable接口,必须在主线程调用
ImageLoader
主要函数:
getInstance()
获得ImageLoader的单例,int(ImageLoaderConfiguration configuration)
初始化UIL配置参数,并初始化ImageLoaderEngine引擎,最主要的调用函数displayImage(String uri, ImageAware imageAware, DisplayImageOptions options, ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener)
表示异步加载显示,loadImageSync()
表示同步加载
displayImage()函数流程图如下:
ImageLoaderConfiguration
ImageLoader Configuration is global for application. You should set it once. 内部有个Builder静态类,用于构建参数。
返回默认配置函数:
public static ImageLoaderConfiguration createDefault(Context context) {
return new Builder(context).build();
}
最终调用的Custom配置build()函数:
public ImageLoaderCofiguration build() {
//初始化空参数
initEmptyFieldsWithDefaultValues();
return new ImagerLoaderConfiguration(this);
}
常规配置项如下:
File cacheDir = StorageUtils.getCacheDirectory(context);
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.memoryCacheExtraOptions(480, 800) // default = device screen dimensions
.diskCacheExtraOptions(480, 800, null)
.taskExecutor(...)
.taskExecutorForCachedImages(...)
.threadPoolSize(3) // default
.threadPriority(Thread.NORM_PRIORITY - 2) // default
.tasksProcessingOrder(QueueProcessingType.FIFO) // default
.denyCacheImageMultipleSizesInMemory()
.memoryCache(new LruMemoryCache(2 * 1024 * 1024))
.memoryCacheSize(2 * 1024 * 1024)
.memoryCacheSizePercentage(13) // default
.diskCache(new UnlimitedDiskCache(cacheDir)) // default
.diskCacheSize(50 * 1024 * 1024)
.diskCacheFileCount(100)
.diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) // default
.imageDownloader(new BaseImageDownloader(context)) // default
.imageDecoder(new BaseImageDecoder()) // default
.defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default
.writeDebugLogs()
.build();
ImageLoaderEngine
ImageLoaderEngine主要负责分发任务,定义了三个Executor:
- taskExecutor 执行从源获取图片任务的Executor
- taskExecutorForCachedImages 执行从缓存获取图片任务的Executor
- taskDistributor 任务分发线程池,任务指LoadAndDisplayImageTask和ProcessAndDisplayImageTask,因为只需要分发给上面的两个Executor去执行任务,不存在较耗时或阻塞操作,所以用无并发数(Int最大值)限制的线程池即可
任务分发到线程池的函数:
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);
}
}
});
}
task executor和task distributor的创建线程池函数来自DefaultConfigurationFactory类:
/** 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-"));
}
/** Creates default implementation of task distributor */
public static Executor createTaskDistributor() {
return Executors.newCachedThreadPool(createThreadFactory(Thread.NORM_PRIORITY, "uil-pool-d-"));
}
JAVA线程池
Executor接口规定了线程池的接口,内部只有一个execute(Runnable r)方法,ExecutorService接口继承自Executor,包含了线程池逻辑操作的方法,ThreadPoolExecutor其实是实现了ExecutorService接口的实体类,线程池实际表现为ExecutorService类的一個实例。创建一个ThreadPoolExecutor需要以下参数:
public ThreadPoolExecutor(
int corePoolSize, //线程池的基本大小,即使有空闲线程也会创建新线程
int maximumPoolSize, //线程池允许的最大线程数
long keepAliveTime, //是指线程池中的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率
TimeUnit unit, //keepAliveTime保持时间的单位
BlockingQueue<Runnable> workQueue, //任务队列,the queue to use for holding tasks before they are executed. This queue will hold only the runnable tasks submitted by the execute() method,有ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue,PriorityBlockingQueue几种
ThreadFactory threadFactory, //用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字
RejectedExecutionHandler handler //饱和策略, 当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常
){}
上面的createTaskDistributor()是通过Executors静态工厂创建线程池,它也是一个ThreadPoolExecutor对象,Executors工厂提供了几种常用的线程池场景供使用:
- newFixedThreadPool:创建一个定长的线程池。达到最大线程数后,线程数不再增长。如果一个线程由于非预期Exception而结束,线程池会补充一个新的线程
- newCachedThreadPool:创建一个可缓存的线程池。当池长度超过处理需求时,可以回收空闲的线程
- newSingleThreadPool:创建一个单线程executor
- newScheduledThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的任务执行。类似于Timer。但是,Timer是基于绝对时间,对系统时钟的改变是敏感的,而ScheduledThreadPoolExecutor只支持相对时间
合理配置线程池,首先要分析任务特性,包括CPU密集型任务、IO密集型任务和混合型任务,任务优先级有高有低,任务执行时间有长有短,任务是否存在依赖性等,CPU密集型任务配置尽可能小的线程,如配置Ncpu+1个线程的线程池。IO密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
在UIL中,taskDistributor线程池每执行一个新线程时都会读取下磁盘,属于IO操作,另外图片缓存加载时会大量创建线程,这些线程执行时间很短,存活时间也不必太长,所以设计该线程池为normal优先级的无并发大小限制的线程池,适合many short-lived asynchronous tasks; taskExecutor和taskExecutorForCachedImages涉及网络和磁盘的读取和写入,比较耗时,这里线程数设为3,线程优先级设为4
ImageDownloader
图片下载接口,内部只有一个InputStream getStream(String imageUri, Object extra)函数,和内部定义的枚举Scheme,定义UIL支持的图片来源。BaseImageDownloader是ImageDownloader的具体实现类,得到各种Scheme的InputStream。其中最主要的getStreamFromNetwork()实现如下:
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
HttpURLConnection conn = createConnection(imageUri, extra);
...
InputStream imageStream;
try {
imageStream = conn.getInputStream;
} catch (IOExcption e) {
IoUtils.readAndCloseStream(conn.getErrorStream);
throw e;
}
...
}
protected HttpURLConnection createConnection(String url, Object extra) throws IOException {
String encodeUrl = Uri.encode(url, ALLOWED_URI_CHARS);
HttpURLConnection conn = (HttpURLConnection) new URL(encodeUrl).openConnection();
conn.setConnectTimeout(connectTimeout);
connect.setReadTimeout(readTimeout);
return conn;
}
ImageDecoder
将图片转换成Bitmap的接口,内部只有抽象函数Bitmap decode(ImageDecodingInfo decodingInfo) throw IOException; BaseImageDecoder实现了ImageDecoder
public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
Bitmap decodedBitmap;
ImageFileInfo imageInfo;
InputStream imageStream = getImageStream(decodingInfo);
...
try {
imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
imageStream = resetStream(imageStream, decodingInfo);
Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
} finally {
IoUtils.closeSilently(imageStream);
}
...
}
DisplayImageOptions
同ImageLoaderConfiguration,通过Builder静态内部类完成配置,Display Options(DisplayImageOptions) are local for every display task (ImageLoader.displayImage(…) call)
DisplayImageOptions options = new DisplayImageOptions.Builder()
.showImageOnLoading(R.drawable.ic_stub) // resource or drawable
.showImageForEmptyUri(R.drawable.ic_empty) // resource or drawable
.showImageOnFail(R.drawable.ic_error) // resource or drawable
.resetViewBeforeLoading(false) // default
.delayBeforeLoading(1000)
.cacheInMemory(false) // default
.cacheOnDisk(false) // default
.preProcessor(...)
.postProcessor(...)
.extraForDownloader(...)
.considerExifParams(false) // default
.imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // default
.bitmapConfig(Bitmap.Config.ARGB_8888) // default
.decodingOptions(...)
.displayer(new SimpleBitmapDisplayer()) // default
.handler(new Handler()) // default
.build();
ImageAware
接口主要定义了包装View的函数,ViewAware实现了该接口,利用WeakReference来Wrap View防止内存泄露,Reference viewRef = new WeakReference(view); ImageViewAware继承ViewAware,封装了Android ImageView来显示图片
一个对象被回收要满足两个条件:没有任何应用指向它、GC被运行,Java中引入weak reference,相对于普通的stong reference,针对cache中的reference回收问题
Object car = new Car(); //只要c还指向car object,c就会被GC
WeakReference weakCar = new WeakReference(car); //声明一个weak reference
当要获得weak reference引用的object时,首先需要判断它是否已经被回收weakCar.get();
WeakReference的一个特点是它何时被回收是不可确定的,因为这是由GC运行的不确定性所确定的。所以,一般用weak reference引用的对象是有价值被cache,而且很容易被重新被构建,且很消耗内存的对象
LoadAndDisplayImageTask
run函数,获取图片并显示:
bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null || bmp.isRecycled()) {
bmp = tryLoadBitmap();
...
...
...
if (bmp != null && options.isCacheInMemory()) {
L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
configuration.memoryCache.put(memoryCacheKey, bmp);
}
}
……
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
流程图如下:
tryLoadBitmap()函数:
File imageFile = configuration.diskCache.get(uri);
if (imageFile != null && imageFile.exists()) {
...
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
}
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
...
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);
...
}
tryCacheImageOnDisk()函数下载图片并存储在磁盘内,根据磁盘缓存图片最长宽高的配置处理图片,loaded = downloadImage();
downloadImage()函数:
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);
}
}
}
ProcessAndDisplayImageTask
public void run() {
...
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);
}
DisplayBitmapTask
public void run() {
//首先判断imageAware是否被 GC 回收,如果是直接调用取消加载回调接口
if (imageAware.isCollected()) {
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
//否则判断imageAware是否被复用,如果是直接调用取消加载回调接口
} else if (isViewWasReused()) {
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
//否则调用displayer显示图片,并将imageAware从正在加载的 map 中移除。调用加载成功回调接口
} 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);
}
对于 ListView 或是 GridView 这类会缓存 Item 的 View 来说,单个 Item 中如果含有 ImageView,在滑动过程中可能因为异步加载及 View 复用导致图片错乱,这里对imageAware是否被复用的判断就能很好的解决这个问题。原因类似:Android ListView 滑动过程中图片显示重复错位闪烁问题原因及解决方案
Listener
- ImageLoadingListener 图片加载各种时刻的回调接口
- ImageLoadingProgressListener Image加载进度的回调接口
- PauseOnScrollListener 可在View滚动过程中暂停图片加载的Listener,实现了OnScrollListener
参考链接:
1. http://www.cnblogs.com/kissazi2/p/3966023.html
2. http://www.tuicool.com/articles/imyueq
3. http://a.codekk.com/detail/Android/huxian99/Android%20Universal%20Image%20Loader%20%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90