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); } } }
这样 就完成了 图片的 缓存 与显示