Android-UIL图片缓存框架 源码解析

Android-Universal-Image-Loader 是 github上一个开源的图片缓存框架 ,提供图片MemoryCache和DiskCache的功能,并支持加载网络、本地、contentProvider图片的功能

Acceptable URIs examples

"http://site.com/image.png" // from Web
"file:///mnt/sdcard/image.png" // from SD card
"file:///mnt/sdcard/video.mp4" // from SD card (video thumbnail)
"content://media/external/images/media/13" // from content provider
"content://media/external/video/media/13" // from content provider (video thumbnail)
"assets://image.png" // from assets
"drawable://" + R.drawable.img // from drawables (non-9patch images) //通常不用。

NOTE: Use drawable:// only if you really need it! Always consider the native way to
load drawables -ImageView.setImageResource(...) instead of using of ImageLoader.

下面我来从源码的角度分析一下这个开源项目的流程:

首先 先写一个简单的例子:

 ImageLoader imageLoader = ImageLoader.getInstance();
        ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).build();

        imageLoader.init(config);
        imageLoader.displayImage("http://pic32.nipic.com/20130829/12906030_124355855000_2.png", image);

第一行 要先实例化ImageLoader  采用了单例模式实例化

然后需要给imageLoader 初始化配置信息,也就是ImageLoaderConfiguration 这个类  如果不初始化 会报异常

接下来我们来看看这个类中都可以初始化哪些变量:

final Resources resources; //用于加载app中资源文件

	final int maxImageWidthForMemoryCache;  //内存缓存的图片宽度最大值  默认为屏幕宽度
	final int maxImageHeightForMemoryCache;  //同上
	final int maxImageWidthForDiskCache;  //磁盘缓存宽度  默认无限制
	final int maxImageHeightForDiskCache;  //同上
	final BitmapProcessor processorForDiskCache;  //位图处理器  磁盘缓存 处理器

	final Executor taskExecutor;   //任务执行者
	final Executor taskExecutorForCachedImages;   //缓存图片任务执行者
	final boolean customExecutor;   //自定义的任务执行者
	final boolean customExecutorForCachedImages;   //自定义的缓存图片任务执行者

	final int threadPoolSize;   //线程池 大小  默认为3
	final int threadPriority;  //线程优先级
	final QueueProcessingType tasksProcessingType;   //队列的类型 可以选择 FIFO(先进先出)LIFO(后进先出)

	final MemoryCache memoryCache;   //内存缓存
	final DiskCache diskCache;   //磁盘缓存
	final ImageDownloader downloader;  //图片下载器
	final ImageDecoder decoder;    //图片解码器
	final DisplayImageOptions defaultDisplayImageOptions;  //图片展示选项

	final ImageDownloader networkDeniedDownloader;   //离线图片下载器
	final ImageDownloader slowNetworkDownloader;   //网速慢图片下载器

在这个配置类中可以初始化以上内容  下面是一些默认的初始化

File cacheDir = StorageUtils.getCacheDirectory(context);
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
        .memoryCacheExtraOptions(480, 800) // default = device screen dimensions
        .diskCacheExtraOptions(480, 800, CompressFormat.JPEG, 75, null)
        .taskExecutor(...)
        .taskExecutorForCachedImages(...)
        .threadPoolSize(3) // default
        .threadPriority(Thread.NORM_PRIORITY - 1) // default
        .tasksProcessingOrder(QueueProcessingType.FIFO) // default
        .denyCacheImageMultipleSizesInMemory()
        .memoryCache(new LruMemoryCache(2 * 1024 * 1024))
        .memoryCacheSize(2 * 1024 * 1024)
        .memoryCacheSizePercentage(13) // default
        .diskCache(new UnlimitedDiscCache(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();  

可以根据自己的需要选择需要使用的disk和memory缓存策略

接下来我们继续往下看:

public synchronized void init(ImageLoaderConfiguration configuration) {
		if (configuration == null) {
			throw new IllegalArgumentException(ERROR_INIT_CONFIG_WITH_NULL);
		}
		if (this.configuration == null) {
			L.d(LOG_INIT_CONFIG);
			engine = new ImageLoaderEngine(configuration);
			this.configuration = configuration;
		} else {
			L.w(WARNING_RE_INIT_CONFIG);
		}
	}

init方法  传入配置信息  并根据配置信息初始化 ImageLoaderEngine引擎类  (主要是 初始化其中的TaskExecutor)

之后 便是 displayImage方法了

下面我们来看 displayImage这个方法

这个方法  参数最多的重载是 :

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,

ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener)

参数包括  图片uri   图片控件  展示图片的选项、图像的大小 、图像加载的监听、图像加载的进度条监听等  其中 options中还可以设置更多的选项

下面正式开始看 displayImage方法的源码  (由于太长 一步步来看):

<span style="white-space:pre">		</span>checkConfiguration();
		if (imageAware == null) {
			throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
		}
		if (listener == null) {
			listener = defaultListener;
		}
		if (options == null) {
			options = configuration.defaultDisplayImageOptions;
		}

第一行 很简单  检查 是否有配置文件

private void checkConfiguration() {

if (configuration == null) {

throw new IllegalStateException(ERROR_NOT_INIT);

}

}

下面几行 也是类似  如果所判断的变量为空则初始化一个

<span style="white-space:pre">		</span>if (TextUtils.isEmpty(uri)) {
			engine.cancelDisplayTaskFor(imageAware);
			listener.onLoadingStarted(uri, imageAware.getWrappedView());
			if (options.shouldShowImageForEmptyUri()) {
				imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
			} else {
				imageAware.setImageDrawable(null);
			}
			listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
			return;
		}

		if (targetSize == null) {
			targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
		}

继续,第一行  判断uri是否是空  如果是空 直接取消engine中的任务 (此处我觉得也还没在engine中添加任务呀,为什么要remove 但是remove肯定没错 那么就先remove着吧。)

然后调用listener的start 之后由于 uri为空  如果设置了需要设置空的图像那么直接设置 图像是  空的时候需要设置的图像即可 如果没设置,直接不显示就好

之后调用 complete 回调  返回   这是uri为空的情况  不需要做太多操作 也不需要缓存

如果 图像的大小 设置是空 那么根据控件设置的大小  设置 要展示图片的大小

public static ImageSize defineTargetSizeForView(ImageAware imageAware, ImageSize maxImageSize) {

int width = imageAware.getWidth();

if (width <= 0) width = maxImageSize.getWidth();

int height = imageAware.getHeight();

if (height <= 0) height = maxImageSize.getHeight();

return new ImageSize(width, height);

}

String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
		engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);

		listener.onLoadingStarted(uri, imageAware.getWrappedView());

之后 根据 uri和目标的大小  生成一个key 并把 这个任务放入 engine 的集合中

回调 started方法

<span style="white-space:pre">		</span>Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);  //从内存缓存取
		if (bmp != null && !bmp.isRecycled()) {  //如果存在 并且没被回收
			L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);

				if (options.shouldPostProcess()) { //如果设置了 postProcess 执行  默认没设置  设置这个可以提前对图片进行某些处理
				ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
						options, listener, progressListener, engine.getLockForUri(uri));
				ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
						defineHandler(options));
				if (options.isSyncLoading()) {
					displayTask.run();
				} else {
					engine.submit(displayTask);
				}
			} else {
				options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
				listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
			}
		}

接下来是重要的部分

首先第一行 从内存缓存中根据key取bitmap

第二行 判断 内存中有没有和 有没有被内存回收   如果存在切没被回收 那么就比较简单了

先对图片进行一些处理  然后把图片展示出来即可

其中 上述的那几行  task 代码  的主要目的就是 封装了一些在展示图片之前的一些对图片的处理 然后再展示图片

倒数第五行的else 语句  是在  不需要 在展示图片之前处理图片时,那么就直接使用 displaywe 对 图片进行 展示  并回调complete函数

其中 这个displayer可以设置 fadeIn(透明度) 和 Circle  displayer(圆角)   看自己需要了

public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) {

imageAware.setImageBitmap(bitmap);

}

普通的displayer   display非常简单 见上面代码

else { //如果不存在内存缓存中 或者已经被回收了
			if (options.shouldShowImageOnLoading()) {
				imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
			} else if (options.isResetViewBeforeLoading()) {
				imageAware.setImageDrawable(null);
			}

			ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
					options, listener, progressListener, engine.getLockForUri(uri));
			LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
					defineHandler(options));
			if (options.isSyncLoading()) { //表示同步  直接执行
<span style="white-space:pre">				</span>displayTask.run();
<span style="white-space:pre">			</span>} else {  // 不同步 那么就交给线程池 对象执行  engine中 有  Executor  这其中有 线程池
<span style="white-space:pre">				</span>engine.submit(displayTask);
<span style="white-space:pre">			</span>}

		}

继续 源码   如果  不在内存缓存中 那么 就麻烦了  大体的操作步骤是 先从图片原始地加载图片,得到图片后放入硬盘和内存  然后展示

第二行  如果加载时需要显示图片  那么设置  否则 不设置图片

然后 设置正在加载时的信息  ImageLoadingInfo   和 任务LoadAndDisplayImageTask

之后根据是否同步  执行任务

接下来看  displayTask的run方法

<span style="white-space:pre">		</span>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();

前两行是

如果waitIfPaused(), delayIfNeed()返回true的话,直接从run()方法中返回了,不执行下面的逻辑,

这两个方法  主要是判断是否是被中断了任务 或者要延时任务的

继续看 第四行 获取了 一个锁  然后 给其 加锁  这是为了防止重复的加载

假如在一个ListView中,某个item正在获取图片的过程中,而此时我们将这个item滚出界面之后又将其滚进来,滚进来之后如果没有加锁,该item又会去加载一次图片,假设在很短的时间内滚动很频繁,那么就会出现多次去网络上面请求图片,所以这里根据图片的Url去对应一个ReentrantLock对象,让具有相同Url的请求就会在最后一行等待,等到这次图片加载完成之后,ReentrantLock就被释放,刚刚那些相同Url的请求就会继续执行下面的代码

<span style="white-space:pre">		</span>Bitmap bmp;
		try {
			checkTaskNotActual(); //检查任务是否还在 

			bmp = configuration.memoryCache.get(memoryCacheKey);  //从内存缓存获取bmp
			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 { //内存缓存中有  设置 from 为  内存缓存
				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();  //释放锁
		}
        //下面两行是显示图片的任务 上面是加载bitmap  现已加载好 并缓存到 内存和磁盘中  只需要显示即可
		//回调接口的  oncancle  和 oncomplete方法 在这里调用   进度条的 在 从网络获取的时候回调  onstart在最开始回调
		DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
		runTask(displayBitmapTask, syncLoading, handler, engine);

上面的代码 就是 缓存部分的了   首先从内存中根据key读取  如果内存中没有 或者说 已经被 回收了  那么就执行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()));  //直接解析出bitmap
			}
			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()) {  //tryCahcheImageDisk 方法 从网络下载 并缓存到硬盘
					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;
	}

其中tryCacheImageOnDisk这个方法的作用 是 在磁盘中未取到 时  从网络获取图片 并缓存到磁盘中去

/** @return <b>true</b> - if image was downloaded successfully; <b>false</b> - otherwise */
	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);
			}
		}
	}

这样 就完成了  图片的 缓存 与显示

时间: 2024-10-10 16:19:59

Android-UIL图片缓存框架 源码解析的相关文章

Android Small插件化框架源码分析

Android Small插件化框架源码分析 目录 概述 Small如何使用 插件加载流程 待改进的地方 一.概述 Small是一个写得非常简洁的插件化框架,工程源码位置:https://github.com/wequick/Small 插件化的方案,说到底要解决的核心问题只有三个: 1.1 插件类的加载 这个问题的解决和其它插件化框架的解决方法差不多.Android的类是由DexClassLoader加载的,通过反射可以将插件包动态加载进去.Small的gradle插件生成的是.so包,在初始

美女图片采集器 (源码+解析)

前言: 有一段时间没写博客了, "持之以恒"徽章都暗了, 实在不该. 前一段确实比较忙, ...小小地给自己的懒找个借口吧. 大二即将结束, 学习iOS也有一段时间了.今天抽点时间, 开源一个前几天刚上传的App里面的一个功能, RT, 美女图片采集器.   美女.. 相信没有人不喜欢吧, 基于此, 这个小Demo应运而生. 效果演示: 看到这里, 如果还有兴趣学习的话, 可以先到我的git中下载源码, 然后配合着源码看我下面的解析.相信, 会让你有所收获的. git下载链接: Bea

神经网络caffe框架源码解析--data_layer.cpp类代码研究

dataLayer作为整个网络的输入层, 数据从leveldb中取.leveldb的数据是通过图片转换过来的. 网络建立的时候, datalayer主要是负责设置一些参数,比如batchsize,channels,height,width等. 这次会通过读leveldb一个数据块来获取这些信息. 然后启动一个线程来预先从leveldb拉取一批数据,这些数据是图像数据和图像标签. 正向传播的时候, datalayer就把预先拉取好数据拷贝到指定的cpu或者gpu的内存. 然后启动新线程再预先拉取数

神经网络caffe框架源码解析--softmax_layer.cpp类代码研究

// Copyright 2013 Yangqing Jia // #include <algorithm> #include <vector> #include "caffe/layer.hpp" #include "caffe/vision_layers.hpp" #include "caffe/util/math_functions.hpp" using std::max; namespace caffe { /**

JDK源码及其他框架源码解析随笔地址导航

置顶一篇文章,主要是整理一下写过的JDK中各个类的源码解析以及其他框架源码解析的文章,方便自己随时阅读也方便网友朋友们阅读及指正 基础篇 从为什么String=String谈到StringBuilder和StringBuffer Java语法糖1:可变长度参数以及foreach循环原理 Java语法糖2:自动装箱和自动拆箱 集合篇 图解集合1:ArrayList 图解集合2:LinkedList 图解集合3:CopyOnWriteArrayList 图解集合4:HashMap 图解集合5:不正确

Android FM模块学习之四源码解析(二)

上一章我们了解了FM主activity:FMRadio.java,若没查看的,请打开链接Android FM模块学习之四源码解析(一) 查看fmradio.java源码注释.接下来我们来看看FM重要的一个类:FMRadioService.java 由上一章我们已经知道,打开FM时,在OnStart函数中会bindToService来开启服务, public boolean bindToService(Context context, ServiceConnection callback) { L

Android EventBus3.0使用及源码解析

叨了个叨 最近因为换工作的一些琐事搞的我一个头两个大,也没怎么去学新东西,实在是有些愧疚.新项目用到了EventBus3.0,原来只是听说EventBus的鼎鼎大名,一直没仔细研究过.趁着周末有些时间,研究下代码,也算没有虚度光阴. EventBus GitHub : https://github.com/greenrobot/EventBus EventBus3.0简介 EventBus是greenrobot出品的一个用于Android中事件发布/订阅的库.以前传递对象可能通过接口.广播.文件

【Android应用开发】EasyDialog 源码解析

示例源码下载 : EasyDialog 简介 : -- 作用 : 用于在界面进行一些介绍, 说明; -- 效果图 : 一. EasyDialog 源码解析 1. 实现原理 实现原理 : -- EasyDialog 效果 : 在点击后, 会从屏幕外飞入对话框, 飞入恰好能够正好处于特定 View 组件的上方 或者下方; -- 本质 : 点击按钮弹出的对话框会填充整个屏幕, 背景设置成透明的, 然后会计算组件坐标, 记录坐标位置, 再在弹出的整个对话框中 绘制一个 带小三角对话框的布局, 并让其执行

iOS开发之Masonry框架源码解析

Masonry是iOS在控件布局中经常使用的一个轻量级框架,Masonry让NSLayoutConstraint使用起来更为简洁.Masonry简化了NSLayoutConstraint的使用方式,让我们可以以链式的方式为我们的控件指定约束.本篇博客的主题不是教你如何去使用Masonry框架的,而是对Masonry框架的源码进行解析,让你明白Masonry是如何对NSLayoutConstraint进行封装的,以及Masonry框架中的各个部分所扮演的角色是什么样的.在Masonry框架中,仔细