由于我当前的ImageLoader版本与读者们的版本可能不同,所以下面讲解的地方可能存在一些出入,但大体上的实现基本一致,请读者自己参照自己的imageloader源码来分析
一般在使用ImageLoader的时候都需要进行一些配置 如下
//显示图片的配置
DisplayImageOptions options = new DisplayImageOptions.Builder()
.showImageOnLoading(R.drawable.default)
.showImageOnFail(R.drawable.error)
.cacheInMemory(true)
.cacheOnDisk(true)
.bitmapConfig(Bitmap.Config.RGB_565)
.build();
然后会调用displayImage方法来加载图片
ImageLoader.getInstance().displayImage(imageUrl, mImageView, options);
我们看下一下displayImage的实现
public void displayImage(String uri, ImageView imageView, DisplayImageOptions options, ImageLoadingListener listener) {
this.displayImage(uri, (ImageAware)(new ImageViewAware(imageView)), options, listener);
}
将imageView转化成ImageViewAware,ImageViewAware实现了ImageAware接口,我们来看一下ImageViewAware 中的方法
首先是构造方法
public ImageViewAware(ImageView imageView) {
this(imageView, true);
}
public ImageViewAware(ImageView imageView, boolean checkActualViewSize) {
this.imageViewRef = new WeakReference(imageView);
this.checkActualViewSize = checkActualViewSize;
}
可以看到在第一个构造方法中调用了第二个构造方法,在第二个构造方法中使用了WeakReference,即将我们的imageView由强引用转化为弱引用,这样当内存不足的时候,可以更好的回收ImageView对象
接下来看一下displayImage的具体实现代码↓
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options, ImageLoadingListener listener) {
this.checkConfiguration();
if(imageAware == null) {
throw new IllegalArgumentException("Wrong arguments were passed to displayImage() method (ImageView reference must not be null)");
} else {
if(listener == null) {
listener = this.emptyListener;
}
if(options == null) {
options = this.configuration.defaultDisplayImageOptions;
}
if(TextUtils.isEmpty(uri)) {
this.engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
if(options.shouldShowImageForEmptyUri()) {
imageAware.setImageDrawable(options.getImageForEmptyUri(this.configuration.resources));
} else {
imageAware.setImageDrawable((Drawable)null);
}
listener.onLoadingComplete(uri, imageAware.getWrappedView(), (Bitmap)null);
} else {
ImageSize targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, this.configuration.getMaxImageSize());
String memoryCacheKey = MemoryCacheUtil.generateKey(uri, targetSize);
this.engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
Bitmap bmp = (Bitmap)this.configuration.memoryCache.get(memoryCacheKey);
ImageLoadingInfo imageLoadingInfo;
if(bmp != null && !bmp.isRecycled()) {
if(this.configuration.writeLogs) {
L.d("Load image from memory cache [%s]", new Object[]{memoryCacheKey});
}
if(options.shouldPostProcess()) {
imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, this.engine.getLockForUri(uri));
ProcessAndDisplayImageTask displayTask1 = new ProcessAndDisplayImageTask(this.engine, bmp, imageLoadingInfo, options.getHandler());
if(options.isSyncLoading()) {
displayTask1.run();
} else {
this.engine.submit(displayTask1);
}
} else {
bmp = options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
} else {
if(options.shouldShowImageOnLoading()) {
imageAware.setImageDrawable(options.getImageOnLoading(this.configuration.resources));
} else if(options.isResetViewBeforeLoading()) {
imageAware.setImageDrawable((Drawable)null);
}
imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, this.engine.getLockForUri(uri));
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(this.engine, imageLoadingInfo, options.getHandler());
if(options.isSyncLoading()) {
displayTask.run();
} else {
this.engine.submit(displayTask);
}
}
}
}
}
在displayImage方法具体的实现中,第一步调用了checkConfiguration()方法
private void checkConfiguration() {
if(this.configuration == null) {
throw new IllegalStateException("ImageLoader must be init with configuration before using");
}
}
当我们的配置是空时,则会抛出异常ImageLoader must be init with configuration before using,这个异常在新手使用时比较容易遇到,这是因为没有init我们的imageloader,下面是初始化的代码(下面这段初始化的代码只提供参考,可根据实际情况自己配置自己需要的参数)
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context).threadPriority(Thread.NORM_PRIORITY - 2).denyCacheImageMultipleSizesInMemory().
discCacheFileNameGenerator(new Md5FileNameGenerator()).tasksProcessingOrder(QueueProcessingType.LIFO).build();
ImageLoader.getInstance().init(config);
我们再来分析一下displayImage中的这段代码
if(TextUtils.isEmpty(uri)) {
this.engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
if(options.shouldShowImageForEmptyUri()) {
imageAware.setImageDrawable(options.getImageForEmptyUri(this.configuration.resources));
} else {
imageAware.setImageDrawable((Drawable)null);
}
listener.onLoadingComplete(uri, imageAware.getWrappedView(), (Bitmap)null);
} else {
....
}
在if语句中,处理的就是当我们传递进去的url为空的情况,我们看到this.engine.cancelDisplayTaskFor(imageAware);有这么一句,那么这一句是什么意思呢?
engine是一个ImageLoaderEngine对象,ImageLoaderEngine中存在一个HashMap,用来记录正在加载的任务,加载图片的时候会将ImageView的id和图片的url加上尺寸加入到HashMap中,加载完成之后会将其移除,我们可以看cancelDisplayTaskFor的具体试下,他将正在加载中的任务的当前iamgeAware给remove掉了
void cancelDisplayTaskFor(ImageAware imageAware) {
this.cacheKeysForImageAwares.remove(Integer.valueOf(imageAware.getId()));
}
然后将DisplayImageOptions的imageResForEmptyUri的图片设置给ImageView,最后回调给ImageLoadingListener接口告诉它这次任务完成了。
接下来我们就来分析一下在url不为空的情况下,这才是我们应该着重关注的部分
if(TextUtils.isEmpty(uri)) {
...
} else {
ImageSize targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, this.configuration.getMaxImageSize());
String memoryCacheKey = MemoryCacheUtil.generateKey(uri, targetSize);
this.engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
Bitmap bmp = (Bitmap)this.configuration.memoryCache.get(memoryCacheKey);
ImageLoadingInfo imageLoadingInfo;
if(bmp != null && !bmp.isRecycled()) {
if(this.configuration.writeLogs) {
L.d("Load image from memory cache [%s]", new Object[]{memoryCacheKey});
}
if(options.shouldPostProcess()) {
imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, this.engine.getLockForUri(uri));
ProcessAndDisplayImageTask displayTask1 = new ProcessAndDisplayImageTask(this.engine, bmp, imageLoadingInfo, options.getHandler());
if(options.isSyncLoading()) {
displayTask1.run();
} else {
this.engine.submit(displayTask1);
}
} else {
bmp = options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
} else {
if(options.shouldShowImageOnLoading()) {
imageAware.setImageDrawable(options.getImageOnLoading(this.configuration.resources));
} else if(options.isResetViewBeforeLoading()) {
imageAware.setImageDrawable((Drawable)null);
}
imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, this.engine.getLockForUri(uri));
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(this.engine, imageLoadingInfo, options.getHandler());
if(options.isSyncLoading()) {
displayTask.run();
} else {
this.engine.submit(displayTask);
}
}
}
首先它会调用ImageSizeUtils类的defineTargetSizeForView方法 将我们的imageAware封装为一个ImageSize对象 ,defineTargetSizeForView方法实现如下
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);
}
如果获取ImageView的宽高小于等于0,就会使用手机屏幕的宽高作为ImageView的宽高。
String memoryCacheKey = MemoryCacheUtil.generateKey(uri, targetSize);
这一句的作用是生成一个缓存时使用的key,再从缓存中取数据的时候通过该key值来获取generateKey方法如下,非常简单,大家看看就好哈,这里就不说了↓
public static String generateKey(String imageUri, ImageSize targetSize) {
return imageUri + "_" + targetSize.getWidth() + "x" + targetSize.getHeight();
}
this.engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
这一句就是将当前任务加入到haspmap中记录起来,cacheKeysForImageAwares就是一个haspMap 如下↓
void prepareDisplayTaskFor(ImageAware imageAware, String memoryCacheKey) {
this.cacheKeysForImageAwares.put(Integer.valueOf(imageAware.getId()), memoryCacheKey);
}
Bitmap bmp = (Bitmap)this.configuration.memoryCache.get(memoryCacheKey);
这一句代码从内存缓存中获取Bitmap对象,我们可以再ImageLoaderConfiguration中配置内存缓存逻辑,默认使用的是LruMemoryCache。
我们再来看接下来的这一段代码
if(options.shouldPostProcess()) {
imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, this.engine.getLockForUri(uri));
ProcessAndDisplayImageTask displayTask1 = new ProcessAndDisplayImageTask(this.engine, bmp, imageLoadingInfo, options.getHandler());
if(options.isSyncLoading()) {
displayTask1.run();
} else {
this.engine.submit(displayTask1);
}
} else {
bmp = options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
这一段代码是在if(bmp != null && !bmp.isRecycled())为true的情况下执行的,就是说是在缓存不为空且没有被回收的条件下执行的。我们如果在DisplayImageOptions中设置了postProcessor就进入true逻辑,不过默认postProcessor是为null的,BitmapProcessor接口主要是对Bitmap进行处理,这个框架并没有给出相对应的实现,如果我们有自己的需求的时候可以自己实现BitmapProcessor接口。
bmp = options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
这两行主要是将Bitmap设置到ImageView上面,这里我们可以在DisplayImageOptions中配置显示需求displayer,默认使用的是SimpleBitmapDisplayer,直接将Bitmap设置到ImageView上面,我们可以配置其他的显示逻辑, 他这里提供了FadeInBitmapDisplayer(透明度从0-1)RoundedBitmapDisplayer(4个角是圆弧)等, 然后回调ImageLoadingListener接口onLoadingComplete 加载完成。
if(options.shouldShowImageOnLoading()) {
imageAware.setImageDrawable(options.getImageOnLoading(this.configuration.resources));
} else if(options.isResetViewBeforeLoading()) {
imageAware.setImageDrawable((Drawable)null);
}
imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, this.engine.getLockForUri(uri));
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(this.engine, imageLoadingInfo, options.getHandler());
if(options.isSyncLoading()) {
displayTask.run();
} else {
this.engine.submit(displayTask);
}
这段代码主要是Bitmap不在内存缓存,从文件中或者网络里面获取bitmap对象,实例化一个LoadAndDisplayImageTask对象,LoadAndDisplayImageTask实现了Runnable,如果配置了isSyncLoading为true, 直接执行LoadAndDisplayImageTask的run方法,表示同步,默认是false,将LoadAndDisplayImageTask提交给线程池对象
我们来看一下LoadAndDisplayImageTask中的run方法如何实现的
public void run() {
if(!this.waitIfPaused()) {
if(!this.delayIfNeed()) {
...
}
}
}
当waitIfPaused()和delayIfNeed()方法返回true时,会直接结束run方法,我们先来看看这两个方法的实现
waitIfPaused()方法
private boolean waitIfPaused() {
AtomicBoolean pause = this.engine.getPause();
synchronized(pause) {
if(pause.get()) {
this.log("ImageLoader is paused. Waiting... [%s]");
try {
pause.wait();
} catch (InterruptedException var5) {
L.e("Task was interrupted [%s]", new Object[]{this.memoryCacheKey});
return true;
}
this.log(".. Resume loading [%s]");
}
}
return this.checkTaskIsNotActual();
}
这个方法是干嘛用呢,主要是我们在使用ListView,GridView去加载图片的时候,有时候为了滑动更加的流畅,我们会选择手指在滑动或者猛地一滑动的时候不去加载图片,所以才提出了这么一个方法,那么要怎么用呢? 这里用到了PauseOnScrollListener这个类,使用很简单ListView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling )), pauseOnScroll控制我们缓慢滑动ListView,GridView是否停止加载图片,pauseOnFling 控制猛的滑动ListView,GridView是否停止加载图片
除此之外,这个方法的返回值由isTaskNotActual()决定,我们接着看看checkTaskIsNotActual()的源码
private boolean checkTaskIsNotActual() {
return this.checkViewCollected() || this.checkViewReused();
}
checkViewCollected()是判断我们ImageView是否被垃圾回收器回收了,如果回收了,LoadAndDisplayImageTask方法的run()就直接返回了,checkViewReused()判断该ImageView是否被重用,被重用run()方法也直接返回,为什么要用checkViewReused()方法呢?主要是ListView,GridView我们会复用item对象,假如我们先去加载ListView,GridView第一页的图片的时候,第一页图片还没有全部加载完我们就快速的滚动,checkViewReused()方法就会避免这些不可见的item去加载图片,而直接加载当前界面的图片
delayIfNeed()方法与waitIfPaused() 一样,都是由checkTaskIsNotActual()来控制返回值,就不多说这个方法了。
然后我们来看看当这两个都返回false时,执行的代码
ReentrantLock loadFromUriLock = this.imageLoadingInfo.loadFromUriLock;
this.log("Start display image task [%s]");
if(loadFromUriLock.isLocked()) {
this.log("Image already is loading. Waiting... [%s]");
}
loadFromUriLock.lock();
Bitmap bmp;
try {
if(this.checkTaskIsNotActual()) {
return;
}
bmp = (Bitmap)this.configuration.memoryCache.get(this.memoryCacheKey);
if(bmp == null) {
bmp = this.tryLoadBitmap();
if(this.imageAwareCollected) {
return;
}
if(bmp == null) {
return;
}
if(this.checkTaskIsNotActual() || this.checkTaskIsInterrupted()) {
return;
}
if(this.options.shouldPreProcess()) {
this.log("PreProcess image before caching in memory [%s]");
bmp = this.options.getPreProcessor().process(bmp);
if(bmp == null) {
L.e("Pre-processor returned null [%s]", new Object[0]);
}
}
if(bmp != null && this.options.isCacheInMemory()) {
this.log("Cache image in memory [%s]");
this.configuration.memoryCache.put(this.memoryCacheKey, bmp);
}
} else {
this.loadedFrom = LoadedFrom.MEMORY_CACHE;
this.log("...Get cached bitmap from memory after waiting. [%s]");
}
if(bmp != null && this.options.shouldPostProcess()) {
this.log("PostProcess image before displaying [%s]");
bmp = this.options.getPostProcessor().process(bmp);
if(bmp == null) {
L.e("Pre-processor returned null [%s]", new Object[]{this.memoryCacheKey});
}
}
} finally {
loadFromUriLock.unlock();
}
if(!this.checkTaskIsNotActual() && !this.checkTaskIsInterrupted()) {
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, this.imageLoadingInfo, this.engine, this.loadedFrom);
displayBitmapTask.setLoggingEnabled(this.writeLogs);
if(this.options.isSyncLoading()) {
displayBitmapTask.run();
} else {
this.handler.post(displayBitmapTask);
}
}
我们可以看到在第一行中有一个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的请求就会在等待,等到这次图片加载完成之后,ReentrantLock就被释放,刚刚那些相同Url的请求才会继续执行下面的代码
接下来又会执行bmp = (Bitmap)this.configuration.memoryCache.get(this.memoryCacheKey);这一句代码,先从内存缓存中获取一遍,如果内存缓存中没有在去执行下面的逻辑,所以ReentrantLock的作用就是避免这种情况下重复的去从网络上面请求图片。
当内存中没有缓存该图片时 会执行一个tryLoadBitmap()方法,
private Bitmap tryLoadBitmap() {
File imageFile = this.getImageFileInDiscCache();
Bitmap bitmap = null;
try {
if(imageFile.exists()) {
this.log("Load image from disc cache [%s]");
this.loadedFrom = LoadedFrom.DISC_CACHE;
bitmap = this.decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
if(this.imageAwareCollected) {
return null;
}
}
if(bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
this.log("Load image from network [%s]");
this.loadedFrom = LoadedFrom.NETWORK;
String e = this.options.isCacheOnDisc()?this.tryCacheImageOnDisc(imageFile):this.uri;
if(!this.checkTaskIsNotActual()) {
bitmap = this.decodeImage(e);
if(this.imageAwareCollected) {
return null;
}
if(bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
this.fireFailEvent(FailType.DECODING_ERROR, (Throwable)null);
}
}
}
} catch (IllegalStateException var4) {
this.fireFailEvent(FailType.NETWORK_DENIED, (Throwable)null);
} catch (IOException var5) {
L.e(var5);
this.fireFailEvent(FailType.IO_ERROR, var5);
if(imageFile.exists()) {
imageFile.delete();
}
} catch (OutOfMemoryError var6) {
L.e(var6);
this.fireFailEvent(FailType.OUT_OF_MEMORY, var6);
} catch (Throwable var7) {
L.e(var7);
this.fireFailEvent(FailType.UNKNOWN, var7);
}
return bitmap;
}
这里面的逻辑是先从文件缓存中获取有没有Bitmap对象,如果没有在去从网络中获取,然后将bitmap保存在文件系统中,我们来看一下它从网络获取图片后是如何进行缓存的
String e = this.options.isCacheOnDisc()?this.tryCacheImageOnDisc(imageFile):this.uri;
先检查是否配置了DisplayImageOptions的isCacheOnDisk,表示是否需要将Bitmap对象保存在文件系统中,一般我们需要配置为true,当为true时就会调用tryCacheImageOnDisc()这个方法了
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());
return configuration.diskCache.save(uri, is, this);
}
downloadImage()方法是负责下载图片,并将其保持到文件缓存中,将下载保存Bitmap的进度回调到IoUtils.CopyListener接口的onBytesCopied(int current, int total)方法中,所以我们可以设置ImageLoadingProgressListener接口来获取图片下载保存的进度,这里保存在文件系统中的图片是原图
int width = configuration.maxImageWidthForDiskCache;
int height = configuration.maxImageHeightForDiskCache;
获取ImageLoaderConfiguration是否设置保存在文件系统中的图片大小,如果设置了maxImageWidthForDiskCache和maxImageHeightForDiskCache,会调用resizeAndSaveImage()方法对图片进行裁剪然后在替换之前的原图,保存裁剪后的图片到文件系统的,所以我们只要在Application中实例化ImageLoaderConfiguration的时候设置maxImageWidthForDiskCache和maxImageHeightForDiskCache就可以保存缩略图了
然后我们再回到run方法中,执行完tryLoadBitmap()后会执行下面这段代码,将图片保存到内存缓存中去
if(bmp != null && this.options.isCacheInMemory()) {
this.log("Cache image in memory [%s]");
this.configuration.memoryCache.put(this.memoryCacheKey, bmp);
}
最后这一段代码就是一个显示的过程
if(!this.checkTaskIsNotActual() && !this.checkTaskIsInterrupted()) {
DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, this.imageLoadingInfo, this.engine, this.loadedFrom);
displayBitmapTask.setLoggingEnabled(this.writeLogs);
if(this.options.isSyncLoading()) {
displayBitmapTask.run();
} else {
this.handler.post(displayBitmapTask);
}
}
我们直接看一下displayBitmapTask.run();方法
public void run() {
if(this.imageAware.isCollected()) {
if(this.loggingEnabled) {
L.d("ImageAware was collected by GC. Task is cancelled. [%s]", new Object[]{this.memoryCacheKey});
}
this.listener.onLoadingCancelled(this.imageUri, this.imageAware.getWrappedView());
} else if(this.isViewWasReused()) {
if(this.loggingEnabled) {
L.d("ImageAware is reused for another image. Task is cancelled. [%s]", new Object[]{this.memoryCacheKey});
}
this.listener.onLoadingCancelled(this.imageUri, this.imageAware.getWrappedView());
} else {
if(this.loggingEnabled) {
L.d("Display image in ImageAware (loaded from %1$s) [%2$s]", new Object[]{this.loadedFrom, this.memoryCacheKey});
}
Bitmap displayedBitmap = this.displayer.display(this.bitmap, this.imageAware, this.loadedFrom);
this.listener.onLoadingComplete(this.imageUri, this.imageAware.getWrappedView(), displayedBitmap);
this.engine.cancelDisplayTaskFor(this.imageAware);
}
}
假如ImageView被回收了或者被重用了,就回调ImageLoadingListener接口的onLoadingCancelled方法,否则就调用BitmapDisplayer去显示Bitmap。
到此整个加载和缓存的过程就讲完了,里面有很多讲得不好的地方 欢迎大家一起讨论。