Android-Universal-Image-Loader源码解析

时间:2022-09-09 18:05:07

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上给出的加载与现实流程图:

Android-Universal-Image-Loader源码解析

这张图基本讲述了整个loader的加载流程,带着图来看就会相对容易很多。

2.库结构

看看其库的结构,如下图,

Android-Universal-Image-Loader源码解析

命名很清晰,基本上可以看出其分为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中设置。

代码就不上了,根据其方法实现,绘制了一张流程图:

Android-Universal-Image-Loader源码解析

首先判断是否是空的地址,如果是就显示空占位图,如果不是,就从内存缓存取,有图片就用,没有则到磁盘或者网络上取。思路很清晰,就不做过多的解释了;

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);
}
来张流程图:

Android-Universal-Image-Loader源码解析

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

参考Android Universal Image Loader 源码分析