Universal Image Loader源码分析

来源:互联网 发布:六皇后问题答案java 编辑:程序博客网 时间:2024/05/22 06:07

Universal Image Loader源码分析


基本组件

UIL是一个强大的,高度定制的图片加载缓存器,支持:

  • 支持任务线程池、下载器、解码器、内存及磁盘缓存、显示选项等等的配置。
  • 包含内存缓存和磁盘缓存两级缓存。
  • 支持多线程,支持异步和同步加载。
  • 支持多种缓存算法、下载进度监听、ListView图片错乱解决等

每一部分都可以从中学到很多内容,本文仅从UIL架构、基本组件、重点知识点等方面出发分析,结构设计图如下:
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()函数流程图如下:
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);

流程图如下:
LoadAndDisplayImageTask
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

0 0