Universal Image Loader源码分析
来源:互联网 发布:六皇后问题答案java 编辑:程序博客网 时间:2024/05/22 06:07
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
- Universal Image Loader源码分析
- Android Universal Image Loader 源码分析
- Android Universal Image Loader 源码分析
- Android Universal Image Loader 源码分析
- Android Universal Image Loader 源码分析
- Android Universal Image Loader 源码分析
- Android Universal Image Loader 源码分析
- Android Universal Image Loader 源码分析
- Android Universal Image Loader 源码分析
- Android Universal Image Loader 源码分析
- Android Universal Image Loader 源码分析
- Android Universal Image Loader 源码分析
- Universal-Image-Loader系列2-源码分析
- Android Universal Image Loader 源码分析
- Android Universal Image Loader 源码分析
- Android Universal Image Loader 源码分析
- Android Universal Image Loader 源码分析
- Android Universal Image Loader 源码分析
- c语言入门之项目1.0——正差值
- java 多线程操作
- android学习攻略
- POJ 1064 (二分)
- uva1645
- Universal Image Loader源码分析
- N层楼梯上楼方式总数
- oracle使用exp导出并备份上传shell
- 当一页的布局很多条目都相同如何防止代码冗余
- Python--内置二分法查找/插入模块(源码)
- Android之Handler用法总结
- 第一行代码笔记之二Activity
- poj 1611The Suspects
- poj3686(KM)