彻底掌握如何有效处理高清大图:Android-Universal-Image-Loader框架解析(一)基本使用
彻底掌握如何有效处理高清大图:Android-Universal-Image-Loader框架解析(二)缓存
彻底掌握如何有效处理高清大图:Android-Universal-Image-Loader框架解析(三)下载模块,解码模块和显示模块
前面三篇文章系统的介绍了UIL框架使用的核心模块,通过这些模块的了解,其实我们对UIL的使用已经是比较清晰的了,这里将对UIL框架的整个流程做一次梳理
UIL的暴露给上层开发者的主要类为ImageLoader,他作为一个单例用于显示图片到imageview上
- ImageLoader的核心方法
(1)构建ImageLoder的单例模式
private volatile static ImageLoader instance;
public static ImageLoader getInstance() {
if (instance == null) {
synchronized (ImageLoader.class) {
if (instance == null) {
instance = new ImageLoader();
}
}
}
return instance;
}
可以看出ImageLoader的构建方式是双重检查锁构建的饿汉式
第4行,是为了不让线程等待,这样就多线程判断此处不为null的时候,就可以直接返回了
第6行,是为了多线程创建多个实例
第7行,instance = new ImageLoader();这段代码实际上jvm分三步完成如下内容
给 instance分配内存
调用ImageLoader的构造函数来初始化成员变量,形成实例
将instance 对象指向分配的内存空间(执行完这步 instance 才是非 null )
但是第二步和第三步有可能会因为指令重排原因导致步骤三先被执行,此时instance已经非null,如果此时有线程过来,则就会返回一个没有初始化的install,为了解决这个问题,所以给instance加上了volatile关键字,volatile用来确保将变量的更新操作通知到其他线程,即所有线程所用的该变量值都是一样的
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,volatile 是一种比sychronized关键字更轻量级的同步
单例模式还有饿汉式,饱汉式;
饿汉式:顾名思义就是在jvm启动的时候,就构建这个对象,及时你在app运行的过程中,不使用该对象
public class DesignPatternsLeanrning {
private static DesignPatternsLeanrning hungryInstance = new DesignPatternsLeanrning();
private void DesignPatternsLeanrning() {
}
public static DesignPatternsLeanrning getHungryInstance() {
return hungryInstance;
}
}
饱汉式:顾名思义作为全局的单例实例在第一次被使用时构建
private static DesignPatternsLeanrning lazyInstance;
public static synchronized DesignPatternsLeanrning getLazyInstance() {
if (lazyInstance == null) {
lazyInstance = new DesignPatternsLeanrning();
}
return lazyInstance;
}
在饱汉式单例中,加入了synchronized关键字,是为了防止线程不安全,因为如果两个线程争抢DesignPatternsLeanrning实例,在没有synchronized关键字的情况下,就会创建多个实例了;
但是加入synchronized之后,其他线程就会等待,在执行效率上便会有了一定的影响了
在UIL框架中,ImageLoader通过volatile 和双重检查锁机制完美的实现了单例模式。
(2)ImageLoder加载图片和显示图片
在UIL框架中,用于加载图片并显示图片的API主要有两个
.displayImage 该方法其中的一个参数是imageview,作用是加载图片并显示到imageview中
.loadImage 该方法的作用是加载图片,并在加载结束的回调中返回一个bitmap,此时开发者可以在这个回调中显示这个图片,显然在使用上displayImage更便捷一些
(3)ImageLoder其他关键方法
.pause暂停加载图片,解码图片和显示图片到imageview上,即暂停LoadAndDisplayImageTask任务
.loadImageSync 采用同步的方式加载一个图片
- ImageLoader核心方法displayImage分析
displayImage方法最终调用的方法如下
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
/*省略部分代码*/
if (targetSize == null) {
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
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 {
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
} 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()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}
}
第7行,根据imageview的宽,高,创建ImageSize实例,ImageSize其实就是维护了imageview的宽和高属性
第12行,回调加载监听,表示开始加载
第14行,根据传入的memoryCacheKey在内存中查找是否有这张图片
第15行,如果有这张图片并且图片没有被内存回收
第18行,如果在显示这张图片之前,开发者有设定针对图片进行操作的Process
第19~26行,构建ImageLoadingInfo和ProcessAndDisplayImageTask任务,然后根据任务是否需要异步,开始执行ProcessAndDisplayImageTask任务
ImageLoadingInfo:该类将会被ProcessAndDisplayImageTask使用,描述的是显示图片任务的相关信息,如图片的url,图片的宽和高(被封装在ImageSize中)
ProcessAndDisplayImageTask:利用DisplayBitmapTask这个实现了Runnable接口的线程来来显示图片到imageview上
第32行,如果在内存中没有这张图片或者这张图片被回收了
第33行,如果设置了加载开始时显示图片的配置
第39~47行,构建ImageLoadingInfo和LoadAndDisplayImageTask任务,然后根据任务是否需要异步,开始执行LoadAndDisplayImageTask任务
总结: displayImage方法的核心如下
1). 先从内存中根据图片url计算出来的key,查找是否有这张图片
2).如果内存中有这张图片并且图片没有被回收,则通过ProcessAndDisplayImageTask任务完成图片显示到imageview上
3).如果内存中没有这张图片或者图片已经被回收了,则通过LoadAndDisplayImageTask从硬盘,网络或者文件系统中下载图片并显示到imageview上
- 一个异步的LoadAndDisplayImageTask是如何执行的
LoadAndDisplayImageTask实现了Runnable接口,他会被提交到Executor taskDistributor的线程池中作从而异步执行
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);
}
}
});
}
第5行,根据图片URL计算出一个key,从硬盘缓存中查找这个文件
第8行,如果硬盘缓存存在这个图片
第9行,则将该任务提交到taskExecutorForCachedImages线程池中执行
第12行,则将该任务提交到taskExecutor线程池中执行
其实不管是哪个线程池,这里我们应该明确一个设计思想,任务分工原则,不同的任务不能交叉干扰,所以这里会有三个线程池,但是真正的任务只有一个即LoadAndDisplayImageTask
- LoadAndDisplayImageTask是如何运行的
@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);
}
第3行,如果开发者调用了ImageLoader的pause方法,则此处返回为true
这里我们可以看到让一个线程停止的方法是采用了Object的wait方法,在Object类中还有notify(),notifyAll(),wait(long millis),wait(long millis, int nanos)方法
这些方法都是final native的,无法被重写
调用某个对象的wait()方法能让当前线程阻塞,并且当前线程必须拥有此对象的monitor(即锁)
调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;
调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程;
备注:因为wait可以让当前线程获得此对象的锁,顾调用上述方法必然在包含了synchronized方法中;
调用某个对象的wait()方法,相当于让当前线程交出此对象的monitor,然后进入等待状态,等待后续再次获得此对象的锁(Thread类中的sleep方法使当前线程暂停执行一段时间,从而让其他线程有机会继续执行,但它并不释放对象锁)
wait()和wait(long millis)对比
1). wait()迫使当前线程交出此对象的锁,进行等待状态,直到另外一个线程调用notify()或者notifyAll()让其在运行之前再次获得对象锁,同时我们应该知道一个在等待中的线程,如果被interrupt()调用,则会提前结束等待状态
2). wait(long millis) 也是迫使当前线程交出此对象的锁,进行等待状态,直到另外一个线程调用notify()或者notifyAll()让其在运行之前再次获得对象锁,当然出入超过long millis最大等待时间之后,他也会让该线程在运行前自动获得对象锁,从而结束等待状态
返回到LoadAndDisplayImageTask中
第17行,从内存中尝试获得缓存的图片
第18行,如果没有缓存或者图片被回收
第19行,调用tryLoadBitmap获得这张图片
第25行,如果开发人员设置了Pre-Process
第27行,在缓存图片之前对这张图片进行一次处理
第33行,如果开发人员在Option中设置可以缓存到内存
第35行,则将这张图片提交到内存缓存中
第42行,如果开发人员设置了Post-Process
第44行,在显示图片之前对这张图片进行一次处理
第58行,通过DisplayBitmapTask 来显示这张图片
由此可见LoadAndDisplayImageTask判断如果内存没有这张图片,其核心是通过tryLoadBitmap方法去硬盘,网络,或者文件系统加载图片;然后根据开发者是否设置了Post-Process,Pre-Process对图片做一次处理,然后进行显示或者放入缓存
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);
}
}
}
/*省略部分代码*/
return bitmap;
}
第4行,从硬盘缓存中尝试获取缓存文件
第10行,如果硬盘中存在这个缓存文件,则通过decodeImage方法完成对应文件的解码
decodeImage方法其实就是利用BaseImageDecoder的decode方法完成对对应流的解码,详见彻底掌握如何有效处理高清大图:Android-Universal-Image-Loader框架解析(三)下载模块,解码模块和显示模块
第12行,如果此时无法解码出图片,则说明没有对应的硬盘缓存,那我们自然需要从网络上下载该图片了,也是通过decodeImage完成下载的
至此UIL框架就完成全部的流程了;
UIL框架优点个人觉得有如下几点
1: 可配置度高
2:支持三级缓存
3:支持下载进度监听和支持滑动取消加载
4:异步的方式去加载显示图片
5:在缓存和显示图片的时候还提供了对应的process来进行操作,真人才