上一篇我们讲解了当加载大量图片时如何使用异步机制以及如何使用多缓存,解决了图片错乱,OOM等问题。其实Android早就提供了强大的图片加载框架来实现上述功能,而且远比我们上一篇实现的功能强大,其中比较著名的就是Universal-Image-Loader,相信很多朋友都听过或者使用过它,这篇文章就是对这个框架进行解析。
UIL(Universal-Image-Loader)旨在提供一个强大的、灵活的和高度可定制的工具图像加载、缓存和显示功能。并且提供了许多配置选项和良好控制图像加载和缓存的过程。
UIL框架特点:
简单描述一下UIL的结构:每一个图片的加载和显示任务都运行在独立的线程中,除非这个图片缓存在内存中,这种情况下图片会立即显示。如果需要的图片缓存在本地,他们会开启一个独立的线程队列。如果在缓存中没有正确的图片,任务线程会从线程池中获取,因此,快速显示缓存图片时不会有明显的障碍。
UIL加载图片基本流程如下所示:
GITHUB上的下载路径为:https://github.com/nostra13/Android-Universal-Image-Loader ,下载最新的库文件,并且导入到项目的Lib下便可以使用。
因为UIL加载图片涉及到网络和SD卡存储(如果有SD),需要添加以下权限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
ImageLoaderConfiguration是针对图片缓存的全局配置,主要有线程类、缓存大小、磁盘大小、图片下载与解析、日志方面的配置。
UIL使用之前需要进行初始化,其实就是加载ImageLoaderConfiguration,我们一般把初始化动作放在Application中进行,初始化配置参数也有两种方式:使用默认配置、自己手动配置。(只能配置一次,如多次配置,则默认第一次的配置参数)
(1)默认配置:
public class AppApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
//创建默认的ImageLoader配置参数
ImageLoaderConfiguration configuration = ImageLoaderConfiguration.createDefault(this);
//初始化ImageLoader
ImageLoader.getInstance().init(configuration);
}
}
(2)手动配置:
public void initImageLoader(Context context) {
File cacheDir = StorageUtils.getOwnCacheDirectory(context, PATH_CACHE); //缓存文件夹路径
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.memoryCacheExtraOptions(480, 800) //default = device screen dimensions 内存缓存文件的最大长宽
.diskCacheExtraOptions(480, 800, null) //本地缓存的详细信息(缓存的最大长宽),最好不要设置这个
.taskExecutor(...) //添加个线程池进行下载,如果进行了这个设置,那么threadPoolSize(int),threadPriority(int),tasksProcessingOrder(QueueProcessingType)将不会起作用
.taskExecutorForCachedImages(...) //下载缓存图片
.threadPoolSize(3) // default 线程池内加载的数量
.threadPriority(Thread.NORM_PRIORITY - 1) // default 设置当前线程的优先级
.tasksProcessingOrder(QueueProcessingType.FIFO) // 设置图片下载和显示的工作队列排序
.denyCacheImageMultipleSizesInMemory() //当同一个Uri获取不同大小的图片,缓存到内存时,只缓存一个。默认会缓存多个不同的大小的相同图片
.memoryCache(new LruMemoryCache(2 * 1024 * 1024)) //可以通过自己的内存缓存实现
.memoryCacheSize(2 * 1024 * 1024) // 内存缓存的最大值
.memoryCacheSizePercentage(13) // default
.diskCache(new UnlimitedDiskCache(cacheDir)) // default 可以自定义缓存路径
.diskCacheSize(50 * 1024 * 1024) // 50Mb sd卡(本地)缓存的最大值
.diskCacheFileCount(100) // 可以缓存的文件数量
.diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) // default为使用HASHCODE对UIL进行加密命名, 还可以用MD5(new Md5FileNameGenerator())加密
.imageDownloader(new BaseImageDownloader(context)) // default
.imageDecoder(new BaseImageDecoder(false)) // default
.defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default
.writeDebugLogs() // 打印debug log
.build(); //开始构建
ImageLoader.getInstance().init(config);
}
1. 现在我们来看看UIL提供了哪些内存缓存策略:
只使用的是强引用缓存
使用强引用和弱引用相结合的缓存有
只使用弱引用缓存
使用:ImageLoaderConfiguration.memoryCache(…)
2. 接下来就给大家分析分析硬盘缓存的策略,这个框架也提供了几种常见的缓存策略:
使用:ImageLoaderConfiguration.diskCache(…)
ImageLoaderConfiguration的主要职责就是记录相关的配置,它的内部其实就是一些字段的集合。它有一个builder的内部类,这个类中的字段跟ImageLoaderConfiguration中的字段完全一致,它有一些默认值,通过修改builder可以配置ImageLoaderConfiguration。
上面的这些就是所有的选项配置,我们在项目中不需要每一个都自己设置,一般使用createDefault()创建的ImageLoaderConfiguration就能使用,然后调用ImageLoader的init()方法将ImageLoaderConfiguration参数传递进去,ImageLoader使用单例模式。
注:
1. maxImageWidthForMemoryCache() 和maxImageHeightForMemoryCache()用于将图片解析成Bitmap对象。为了不储存整个图片,根据ImageView参数的值(要加载图片的那个)减少图片的大小。maxWidth和maxHeight(第一阶段),layout_width layout_height(第二阶段)。如果不定义这些参数(值为fill_parent和wrap_content被视为不确定的大小),然后尺寸的设定就会根据maxImageWidthForMemoryCache()和maxImageHeightForMemoryCache()的设置而定。原始图像的大小最大会缩小到2倍(适合用fast decoding),直到宽度或高度变得小于指定值;
2.调用denyCacheImageMultipleSizesInMemory()强制UIL在内存中不能存储内容相同但大小不同的图像。由于完整大小的图片会存储在硬盘缓存中,后面当图片加载进入内存,他们就会缩小到ImageView的大小(图片要显示的尺寸),然而在某些情况下,相同的图像第一次显示在一个小的View中,然后又需要在一个大的View中显示。同时,两个不同大小相同内容的图片就会被将被存储在内存中。这是默认的操作。denyCacheImageMultipleSizesInMemory()指令确保删除前一个加载的图像缓存的内存的大小。
3. 在设置中配置线程池的大小是非常明智的。一个大的线程池会允许多条线程同时工作,但是也会显著的影响到UI线程的速度。但是可以通过设置一个较低的优先级来解决:当ImageLoader在使用的时候,可以降低它的优先级,这样UI线程会更加流畅。在使用List的时候,UI 线程经常会不太流畅,所以在你的程序中最好设置threadPoolSize(…)和threadPriority(…)这两个参数来优化你的应用。
4. memoryCache(…)和memoryCacheSize(…)这两个参数会互相覆盖,所以在ImageLoaderConfiguration中使用一个就好了
5. diskCacheSize(…)、diskCache(…)和diskCacheFileCount(…)这三个参数会互相覆盖,只使用一个。这三个参数都已弃用,不建议使用了。
6. 清除缓存
ImageLoader.getInstance().clearMemoryCache(); // 清除内存缓存
ImageLoader.getInstance().clearDiskCache(); // 清除本地缓存
DisplayImageOptions用于指导每一个Imageloader根据网络图片的状态(空白、下载错误、正在下载)显示对应的图片,是否将缓存加载到磁盘上,下载完后对图片进行怎么样的处理等。
每一个ImageLoader.displayImage(…)都可以使用Display Options。如果没有使用,那么配置默认显示选项
(ImageLoaderConfiguration.defaultDisplayImageOptions(…))将被使用。
public static DisplayImageOptions getOptions() {
DisplayImageOptions options = new DisplayImageOptions.Builder()
.showImageOnLoading(R.drawable.pic_empty) // 设置图片下载期间显示的图片
.showImageForEmptyUri(R.drawable.pic_empty) // 设置图片Uri为空或是错误的时候显示的图片
.showImageOnFail(R.drawable.pic_empty) // 设置图片加载或解码过程中发生错误显示的图片
.resetViewBeforeLoading(false) // default 设置图片在加载前是否重置、复位
.delayBeforeLoading(1000) // 下载前的延迟时间
.cacheInMemory(false) // default 设置下载的图片是否缓存在内存中
.cacheOnDisk(false) // default 设置下载的图片是否缓存在SD卡中
.preProcessor(...) //设置图片加入缓存前,对bitmap进行设置
.postProcessor(...) //设置显示前的图片,显示后这个图片一直保留在缓存中
.extraForDownloader(...) //设置额外的内容给ImageDownloader
.considerExifParams(false) //是否考虑JPEG图像EXIF参数(旋转,翻转)
.imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // default 设置图片以如何的编码方式显示
.bitmapConfig(Bitmap.Config.ARGB_8888) // default 设置图片的解码类型
.decodingOptions(...) //设置图片的解码配置
.displayer(new SimpleBitmapDisplayer()) // default 还可以设置圆角图片new RoundedBitmapDisplayer(20)
.handler(new Handler()) // default
.build();
return options;
}
(1).imageScaleType(ImageScaleType imageScaleType) //设置图片的缩放方式
缩放类型imageScaleType:
(2).displayer(BitmapDisplayer displayer) //设置图片的显示方式
显示方式displayer:
和ImageLoaderConfiguration一样,DisplayImageOptions也是使用一个builder的内部类进行管理,同理在项目中不需要每一个参数都自己设置。
注:如果你的程序中使用displayImage()方法时传入的参数经常是一样的,那么一个合理的解决方法是,把这些选项配置在ImageLoader的设置中作为默认的选项(通过调用defaultDisplayImageOptions(…)方法)。之后调用displayImage(…)方法的时候就不必再指定这些选项了,如果这些选项没有明确的指定给defaultDisplayImageOptions(…)方法,那调用的时候将会调用UIL的默认设置。
displayImage一共有六个重载方法
void displayImage(String url, ImageView view)
void displayImage(String url, ImageView view, DisplayImageOptions options)
void displayImage(String url, ImageView view, ImageLoadingListener listener)
void displayImage(String url, ImageView view, DisplayImageOptions options, ImageLoadingListener listener)
void displayImage(String url, ImageView view, DisplayImageOptions options, ImageLoadingListener listener, ImageLoadingProgressListener listener)
void displayImage(String url, ImageView view, ImageSize targetImageSize)
(1)第一个重载方法
void displayImage(String url, ImageView view)
所有东西都很简单。url就是图片的下载地址,ImageView就是需要显示它的imageView控件。这个ViewOption(DisplayOptions)将使用默认配置option(defaultDisplayImageOptions(…))
(2)第二个重载方法
void displayImage(String url, ImageView view, DisplayImageOptions options)
我们可以针对特定的任务做一些特定的option。
(3)第三个重载方法
void displayImage(String url, ImageView view, ImageLoadingListener listener)
我们可以使用ImageLoadingListener接口监听图片的下载和显示过程:
public interface ImageLoadingListener {
void onLoadingStarted(String imageUri, View view);
void onLoadingFailed(String imageUri, View view, FailReason reason);
void onLoadingCancelled(String imageUri, View view);
void onLoadingComplete(String imageUri, View view, Bitmap bitmap);
}
当然,如果我们只想监听某个或某几个回调方法而不想重载接口所有的方法,我们可以使用SimpleImageLoadingListener子类来实现,SimpleImageLoadingListener类本身已经实现了这个接口的所有函数,只是里面全是空的而已:
public class SimpleImageLoadingListener {
void onLoadingStarted(String imageUri, View view){}
void onLoadingFailed(String imageUri, View view, FailReason reason){}
void onLoadingCancelled(String imageUri, View view){}
void onLoadingComplete(String imageUri, View view, Bitmap bitmap){}
}
(4)第四个重载方法
void displayImage(String url, ImageView view, DisplayImageOptions options, ImageLoadingListener listener)
同时实现定制option和监听过程。
(5)第五个重载方法
void displayImage(String url, ImageView view, DisplayImageOptions options, ImageLoadingListener listener, ImageLoadingProgressListener listener)
在第四个方法的基础上增加了图片加载进度监听。我们可以使用ImageLoadingProgressListener 接口监听图片的下载进度:
public interface ImageLoadingProgressListener {
void onProgressUpdate(String imageUri, View view, int current, int total);
}
(6)第六个重载方法
void displayImage(String url, ImageView view, ImageSize targetImageSize)
如果我们要指定图片的大小该怎么办呢,这也好办,初始化一个ImageSize对象,指定图片的宽和高即可。
ImageSize mImageSize = new ImageSize(100, 100);
ImageLoader.getInstance().displayImage(url, image, mImageSize);
注:
1. 使用displayImage()方法,最终图片是加载到ImageView的src中的。当然我们也可以在ImageLoadingListener接口的complete回调中获取图片,手动设置给background。
2.使用displayImage()方法,它会根据ImageView对象的测量值,或者Android:layout_width/android:layout_height设定的值,或者android:maxWidth/android:maxHeight设定的值以及ImageSize和imageScaleType来自动裁剪图片。
我们写个Demo证实一下,例子中图片1设置layout_width和layout_height都为50dp,图片2-6宽高都是150dp,但是图片6设置了ImageSize(宽高都为50dp),开启Log打印。
可以清楚看到图片1和6宽高已经被裁剪到指定值150px,其他图片仍为450px。
loadImage一共有五个重载方法
void loadImage(String url, ImageLoadingListener listener)
void loadImage(String url, DisplayImageOptions options, ImageLoadingListener listener)
void loadImage(String url, ImageSize targetImageSize, ImageLoadingListener listener)
void loadImage(String url, ImageSize targetImageSize, DisplayImageOptions options, ImageLoadingListener listener)
void loadImage(String url, ImageSize targetImageSize, DisplayImageOptions options, ImageLoadingListener listener, ImageLoadingProgressListener listener)
loadImage可以类比displayImage,这里就不对每个方法进行解析了,可以看到每个重载方法都添加了ImageLoadingListener接口,在它的onLoadingComplete方法中我们可以获取到下载完成的图片,可以对它进行处理,然后手动设置到ImageView上,既可以设置为src也可以是background,例如:
ImageLoader.getInstance().loadImage(url, options, new SimpleImageLoadingListener(){
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage){
if (loadedImage != null){
int width = loadedImage.getWidth();
int height = loadedImage.getHeight();
RelativeLayout.LayoutParams blp = (RelativeLayout.LayoutParams)videoImageID.getLayoutParams();
if (width > videoID.getWidth()) {
blp.width = videoID.getWidth();
blp.height = blp.width*height/width;
}
if (height > videoID.getHeight()) {
blp.height = videoID.getHeight();
blp.width = blp.height*width/height;
}
videoImageID.setLayoutParams(blp);
videoImageID.setBackgroundDrawable(new BitmapDrawable(loadedImage));
return;
}
}
});
注:DisplayImageOptions选项中有些选项对于loadImage()方法是无效的,比如showImageOnLoading, showImageForEmptyUri等。
使用UIL框架不仅可以加载网络图片,还可以加载sd卡、drawable、assert和Content provider等图片,使用也很简单,只是将图片的url稍加的改变下就行了:
String imageUri = "http://site.com/image.png"; // from Web
String imageUri = "file://mnt/sdcard/image.png"; // from SD card
String imageUri = "content://media/external/audio/albumart/13"; // from content provider
String imageUri = "assets://image.png"; // from assets
String imageUri = "drawable://" + R.drawable.image; // from drawables (only images, non-9patch)
注:对于drawable资源,不建议UIL加载,可以直接用setImageResource或setBackgroundResource。
1. 如果你经常出现OOM,你可以尝试:
1)禁用在内存中缓存cacheInMemory(false),如果OOM仍然发生那么似乎你的应用程序有内存泄漏,使用MemoryAnalyzer来检测它。否则尝试以下步骤(尝试所有或几个)
2)减少配置的线程池的大小(.threadPoolSize(…)),建议1~5
3)在显示选项中使用 .bitmapConfig(Bitmap.Config.RGB_565) . RGB_565模式消耗的内存比ARGB_8888模式少两倍.
4)配置中使用.diskCacheExtraOptions(480, 320, null)
5)配置中使用 .memoryCache(newWeakMemoryCache()) 或者完全禁用在内存中缓存(don’t call .cacheInMemory()).
6)在显示选项中使用.imageScaleType(ImageScaleType.EXACTLY) 或 .imageScaleType(ImageScaleType.IN_SAMPLE_INT)
7)避免使用 RoundedBitmapDisplayer. 调用的时候它使用ARGB-8888模式创建了一个新的Bitmap对象来显示,对于内存缓存模式 (ImageLoaderConfiguration.memoryCache(…)) 你可以使用已经实现好的方法.
2. 使用这个框架时尽量使用displayImage()去加载图片。displayImage()中,对ImageView对象使用的是弱引用,方便垃圾回收器回收ImageView对象,如果我们加载固定大小的图片时,使用loadImage()需要传递一个ImageSize对象,而displayImage()会根据ImageView对象的测量值,或者Android:layout_width和android:layout_height设定的值,或者android:maxWidth或android:maxHeight设定的值来裁剪图片。
3.还有一个常用的优化ListView或者GridView的监听:
/** Parameters: ** imageLoader ImageLoader instance for controlling ** pauseOnScroll Whether pause ImageLoader during scrolling ** pauseOnFling Whether pause ImageLoader during fling */
PauseOnScrollListener(ImageLoader imageLoader, boolean pauseOnScroll, boolean pauseOnFling)
listView.setOnScrollListener(new PauseOnScrollListener(imageLoader,true,true));
gridView.setOnScrollListener(new PauseOnScrollListener(imageLoader,true,true));
该方法用于设置滚动、滑动过程不加载图片,优化显示效果。
4.如果你设置了图片缓存到内存和硬盘,你还可以通过如下方式去获取、删除、清空图片缓存:
//获取内存缓存图片文件
Bitmap image = ImageLoader.getInstance().getMemoryCache().get(url);
//删除内存缓存图片文件
boolean success = ImageLoader.getInstance().getMemoryCache().remove(url);
//清空内存缓存
ImageLoader.getInstance().getMemoryCache().clear();
ImageLoader.getInstance().clearMemoryCache;
//获取硬盘缓存图片文件
File file = ImageLoader.getInstance().getDiscCache().get(url);
//删除硬盘缓存图片文件
boolean success = ImageLoader.getInstance().getDiscCache().remove(url);
//清空硬盘缓存
ImageLoader.getInstance().getDiscCache().clear();
ImageLoader.getInstance().clearDiscCache;
本文开头我贴出了一张UIL加载图片的流程图,从图中,我们可以看出,UIL加载图片的一般流程是先判断内存中是否有对应的Bitmap,再判断磁盘(disk)中是否有,如果没有就从网络中加载。最后根据原先在UIL中的配置判断是否需要缓存Bitmap到内存或磁盘中。Bitmap加载完后,就对它进行解析,然后显示到特定的ImageView中。
有了对UIL对图片加载和处理流程的初步认识之后,我们就可以着手分析它的源代码了。先从ImageLoader.displayImage(…)入手,毕竟一切都因它而始。
public void displayImage(String uri, ImageView imageView, DisplayImageOptions options) {
displayImage(uri, new ImageViewAware(imageView), options, null, null);
}
它会将ImageView转换成ImageViewAware, ImageViewAware主要是做什么的呢?该类主要是将ImageView进行一个包装,将ImageView的强引用变成弱引用,当内存不足的时候,可以更好的回收ImageView对象,还有就是获取ImageView的宽度和高度。这使得我们可以根据ImageView的宽高去对图片进行一个裁剪,减少内存的使用。
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
//检查ImageLoaderConfiguration是否初始化,这个初始化是在Application中进行的
checkConfiguration();
if (imageAware == null) {
throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
}
if (listener == null) {
listener = emptyListener;
}
if (options == null) {
options = configuration.defaultDisplayImageOptions;
}
//针对url为空的时候做的处理
if (TextUtils.isEmpty(uri)) {
//ImageLoaderEngine中存在一个HashMap,用来记录正在加载的任务,加载图片的时候会将ImageView的id和图片的url加上尺寸加入到HashMap中,加载完成之后会将其移除
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
if (options.shouldShowImageForEmptyUri()) {
//DisplayImageOptions的imageResForEmptyUri的图片设置给ImageView
imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
} else {
imageAware.setImageDrawable(null);
}
//回调给ImageLoadingListener接口告诉它这次任务完成了
listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
return;
}
//计算Bitmap的大小,以便后面解析图片时用
ImageSize targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
//内存图片key值是由url和图片大小共同生成
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
//从内存缓存中获取Bitmap对象,我们可以在ImageLoaderConfiguration中配置内存缓存逻辑,默认LruMemoryCache
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
//DisplayImageOptions中是否设置了postProcessor,默认postProcessor是为null
if (options.shouldPostProcess()) {
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 {
//显示图片,这里我们可以在DisplayImageOptions中配置显示需求displayer,默认使用的是SimpleBitmapDisplayer
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
//回调到ImageLoadingListener接口
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
//Bitmap不在内存缓存,从文件中或者网络里面获取bitmap对象
} else {
if (options.shouldShowImageOnLoading()) {
//DisplayImageOptions的imageResOnLoading的图片设置给ImageView
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对象, 启动一个线程,加载并显示图片
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo, defineHandler(options));
//如果配置了isSyncLoading为true, 直接执行LoadAndDisplayImageTask的run方法,表示同步,默认是false,将LoadAndDisplayImageTask提交给线程池对象
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}
}
跟进LoadAndDisplayImageTask.run()方法中:
// 如果waitIfPaused(), delayIfNeed()返回true的话,直接从run()方法中返回了,不执行下面的逻辑
if (waitIfPaused()) return;
if (delayIfNeed()) return;
private boolean waitIfPaused() {
AtomicBoolean pause = engine.getPause();
if (pause.get()) {
synchronized (engine.getPauseLock()) {
if (pause.get()) {
L.d(LOG_WAITING_FOR_RESUME, memoryCacheKey);
try {
engine.getPauseLock().wait();
} catch (InterruptedException e) {
L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);
return true;
}
L.d(LOG_RESUME_AFTER_PAUSE, memoryCacheKey);
}
}
}
return isTaskNotActual();
}
这个方法是干嘛用呢,主要是我们在使用ListView,GridView去加载图片的时候,有时候为了滑动更加的流畅,我们会选择手指在滑动或者猛地一滑动的时候不去加载图片,所以才提出了这么一个方法,那么要怎么用呢? 这里用到了PauseOnScrollListener这个类,使用很简单ListView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling )), pauseOnScroll控制我们缓慢滑动ListView,GridView是否停止加载图片,pauseOnFling 控制猛的滑动ListView,GridView是否停止加载图片
除此之外,这个方法的返回值由isTaskNotActual()决定,我们接着看看isTaskNotActual()的源码:
private boolean isTaskNotActual() {
return isViewCollected() || isViewReused();
}
isViewCollected()是判断我们ImageView是否被垃圾回收器回收了,如果回收了,LoadAndDisplayImageTask方法的run()就直接返回了,isViewReused()判断该ImageView是否被重用,被重用run()方法也直接返回,为什么要用isViewReused()方法呢?主要是ListView,GridView我们会复用item对象,假如我们先去加载ListView,GridView第一页的图片的时候,第一页图片还没有全部加载完我们就快速的滚动,isViewReused()方法就会避免这些不可见的item去加载图片,而直接加载当前界面的图片
继续LoadAndDisplayImageTask.run方法:
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();
}
第1行代码有一个loadFromUriLock,这个是一个锁,获取锁的方法在ImageLoaderEngine类的getLockForUri()方法中
ReentrantLock getLockForUri(String uri) {
ReentrantLock lock = uriLocks.get(uri);
if (lock == null) { lock = new ReentrantLock();
uriLocks.put(uri, lock);
}
return lock;
}
从上面可以看出,这个锁对象与图片的url是相互对应的,为什么要这么做?也行你还有点不理解,不知道大家有没有考虑过一个场景,假如在一个ListView中,某个item正在获取图片的过程中,而此时我们将这个item滚出界面之后又将其滚进来,滚进来之后如果没有加锁,该item又会去加载一次图片,假设在很短的时间内滚动很频繁,那么就会出现多次去网络上面请求图片,所以这里根据图片的Url去对应一个ReentrantLock对象,让具有相同Url的请求就会在第7行等待,等到这次图片加载完成之后,ReentrantLock就被释放,刚刚那些相同Url的请求就会继续执行第7行下面的代码。来到第12行,它们会先从内存缓存中获取一遍,如果内存缓存中没有在去执行下面的逻辑,所以ReentrantLock的作用就是避免这种情况下重复的去从网络上面请求图片。
进入第14行的方法tryLoadBitmap(),这个方法就是加载图片,我先告诉大家,这里面的逻辑是先从文件缓存中获取有没有Bitmap对象,如果没有在去从网络中获取,然后将bitmap保存在文件系统中,我们还是具体分析下
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
try {
//尝试从磁盘缓存中读取Bitmap
File imageFile = configuration.diskCache.get(uri);
if (imageFile != null && imageFile.exists()) {
L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
loadedFrom = LoadedFrom.DISC_CACHE;
checkTaskNotActual();
//硬盘缓存中有该文件,直接去调用decodeImage()方法去解码图片,该方法里面调用BaseImageDecoder类的decode()方法,根据ImageView的宽高,ScaleType去裁剪图片
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;
//是否需要将Bitmap对象保存在文件系统中,一般我们需要配置为true, 默认是false
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
imageFile = configuration.diskCache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
}
checkTaskNotActual();
//进入decodeImage()函数中,我们发现UIL调用Image Decoder进行图片的解析
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;
}
private boolean tryCacheImageOnDisk() throws TaskCancelledException {
L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);
boolean loaded;
try {
//downloadImage()方法是负责下载图片,并将其保持到文件缓存中
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);
//获取ImageLoaderConfiguration是否设置保存在文件系统中的图片大小,如果设置了maxImageWidthForDiskCache和maxImageHeightForDiskCache,会调用resizeAndSaveImage()方法对图片进行裁剪然后在替换之前的原图,保存裁剪后的图片到文件系统的,这个框架保存在文件系统的图片都是原图,那么怎么才能保存缩略图呢?只要在Application中实例化ImageLoaderConfiguration的时候设置maxImageWidthForDiskCache和maxImageHeightForDiskCache就行了
resizeAndSaveImage(width, height);
}
}
} catch (IOException e) {
L.e(e);
loaded = false;
}
return loaded;
}
private boolean downloadImage() throws IOException {
//获取一个实现Image Downloader的downloader
InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());
//将下载保存Bitmap的进度回调到IoUtils.CopyListener接口的onBytesCopied(int current, int total)方法中,所以我们可以设置ImageLoadingProgressListener接口来获取图片下载保存的进度,这里保存在文件系统中的图片是原图
return configuration.diskCache.save(uri, is, this);
}
继续看tryLoadBitmap,进入decodeImage()函数中
private Bitmap decodeImage(String imageUri) throws IOException {
ViewScaleType viewScaleType = imageAware.getScaleType();
ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType, getDownloader(), options);
return decoder.decode(decodingInfo);
}
decode()函数最终是调用BaseImageDecoder.decode()方法进行解析的,这个利用之前获得的inputStream,直接从它身上读取数据,然后进行解析,并对整个下载任务的网络接口进行重置。
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);
}
if (decodedBitmap == null) {
L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
} else {
decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation, imageInfo.exif.flipHorizontal);
}
return decodedBitmap;
}
接下来,有了解析好的Bitmap对象后,剩下的就是在Image View对象中显示它了。我们回到文章一开始介绍到的ImageLoader.displayImage(…)函数中
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
这两行代码就是一个显示任务,直接看DisplayBitmapTask类的run()方法
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);
}
}
假如ImageView被回收了或者被重用了,回调给ImageLoadingListener接口,否则就调用BitmapDisplayer去显示Bitmap,根据实现BitmapDisplayer接口的不同对象,还有SimpleBitmapDisplayer、FadeInBitmapDisplayer、RoundedBitmapDisplayer、RoundedVignetteBitmapDisplayer这5种对象。
最后,让我们用任务流图概况以上的处理流程中对应接口。
Demo下载地址