Android中UniversalImageLoader面试问题讲解

1.UIL基本用法

DisplayImageOptions options = new DisplayImageOptions.Builder()
    .cacheInMemory(true)
    .cacheOnDisk(true)
    .bitmapConfig(Bitmap.Config.RGB_565)
    .build();
ImageLoader.getInstance().displayImage(imageUrl,mImageView,options);

我们可以看到,UniversalImageLoader的用法还是很简单的,还有更多的配置我这里就不赘述了。这个示例中用到的设置主要是cacheInMemory(true)允许使用内存缓存,cacheOnDisk(true)允许使用磁盘缓存。

2.displayImage()方法源码分析

我们可以看到,在使用UIL时,我们最终调用了ImageLoader中的displayImage方法。方法源码如下:

public void displayImage(String uri, ImageView imageView, DisplayImageOptions options) {
    displayImage(uri, new ImageViewAware(imageView), options, null, null);
}

其实这个是个重载方法,我这里只是抽取了一个最简单的示例。可以看到,Universal Image Loader是将ImageView包装成了ImageViewAware来进行后续逻辑的。

3.ImageViewAware类分析

/**
 * Wrapper for Android {@link android.widget.ImageView ImageView}. Keeps weak reference of ImageView to prevent memory
 * leaks.
 *
 * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
 * @since 1.9.0
 */
public class ImageViewAware extends ViewAware {

从该类的注释可以看出,它是将ImageView做了一个封装,同时它会将ImageView的强引用改为弱引用。即当内存不足时可以更好地对ImageView进行回收。同时该类还可以获取到ImageView的宽高,对ImageView进行裁剪,减少内存的使用。 

4.继续回到displayImage()方法源码

多个displayImage()方法其实是在不断向深层次调用。我们进入最终一层的displayImage()方法,完整方法的源码如下:

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
			ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
    checkConfiguration();
    if (imageAware == null) {
        throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
    }
    if (listener == null) {
        listener = defaultListener;
    }
    if (options == null) {
        options = configuration.defaultDisplayImageOptions;
    }
    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());
    }
    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);
        }
    } 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);
        }
    }
}

<1>从源码中可以看到,首先我们调用了checkConfiguration()方法,代码如下:

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
			ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
    checkConfiguration();
    ...
}

该方法对全局变量configuration进行了判断,如果为null,则抛出异常。

<2>接着源码中调用了如下代码:

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
			ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
    ...
    if (TextUtils.isEmpty(uri)) {
        ...
    }
    ...
}

这里主要是判断url为空时,我们如何处理。

<3>在<2>中的if判断中,有如下代码

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
			ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
    ...
    if (TextUtils.isEmpty(uri)) {
        engine.cancelDisplayTaskFor(imageAware);
        ...
    }
    ...
}

这里会记录我们加载的任务,加载图片的时候我们会将ImageView的id和图片的url,加上尺寸,加载到一个集合队列当中。

看一下engine到底是一个什么集合队列。在ImageLoader的全局变量中,我们找到了这个engine

private ImageLoaderEngine engine;

接着我们进入ImageLoaderEngine类中,我们在ImageLoaderEngine类的全局变量中又找到了如下属性

private final Map uriLocks = new WeakHashMap();

这里我们就可以看到它的实现还是通过一个HashMap实现的。这就说明我们所有的属性都会添加到这个HashMap当中。

那么我们回到ImageLoader类中,再看刚才的代码,我们就明白了,当我们判断我们的url为空后,我们会把ImageView的一些属性添加到这个HashMap当中,加载完之后我们会移除这个方法,因为它是cancelDisplay,说明加载完之后它会移除,这是对我们内存的一种优化,它不会将我们所有的配置全放入内存中占用内存空间。

<4>我们接着往后看

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
			ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
    ...
    if (TextUtils.isEmpty(uri)) {
        ...
        if (options.shouldShowImageForEmptyUri()) {
            imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
        } else {
            ...
        }
        ...
    }
    ...
}

这里我们会调用getImageForEmptyUri将我们的属性配置给ImageView。

<5>我们接着往后看

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
			ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
    ...
    if (TextUtils.isEmpty(uri)) {
        ...
        listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
        return;
    }
    ...
}

在这个if判断的最后,UIL会通过一个listener的回调,告诉我们这次任务已经完成了。回顾整个整个if模块,代码如下:

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
			ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
    ...
    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;
    }
    ...
}

 它就是判断url为空时进行默认图片的加载。

<6>当url不为空时,我们继续往后看,会执行如下代码:

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
			ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
    ...
    if (targetSize == null) {
        targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware,configuration.getMaxImageSize());
    }
    ...
}

这个方法主要是将ImageView的宽高封装成ImageSize这个类型。如果我们获取ImageView的高度、宽度为0的话,我们就会使用手机屏幕的宽度、高度。

<7>获取targetSize之后,它会进行MemoryCache的操作,即从内存中获取我们需要的图片。

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
			ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
    ...
    Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
    ...
}

这里注意用到的是get方法。跟入这个get方法,可以看到MemoryCache是个接口类,它的具体实现是LruMemoryCache。get方法源码如下:

@Override
public final Bitmap get(String key) {
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    synchronized (this) {
        return map.get(key);
    }
}

这里其实就是从缓存中获取Bitmap对象,默认使用的是LruCache算法。LruCache算法就是最近最少使用缓存对象。我们会把最近最少缓存对象从队列中移除,然后把最新的所要使用的缓存对象添加到队列当中。

<8>回到displayImage方法

在从内存中获取到Bitmap后,我们要对这个Bitmap进行判断

if (bmp != null && !bmp.isRecycled()) {
    ★1
}else{
    ★2
}    

这个判断是Bitmap对象是否为空,是否已经进行了回收。

<9>在★1处,即Bitmap不为空并且未被回收时,我们进行如下判断

if (options.shouldPostProcess()) {
    ★★1
}else{
    ★★2
}

这个判断的意思是我们后期是否应当对图片进行处理。这里注意,这个postProcessor属性我们是可以在使用UniversalImageLoader的时候在Option中设置的。

<10>在★2处,即从内存中获取的bmp为空或已回收时,我们会执行如下代码:

LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,defineHandler(options));

这里可以看出最后我们是通过这个Task进行图片的加载。其实所有图片加载的原理都是一样的,它都是内部需要一个线程池,然后不断发送消息给线程池,来进行图片的加载请求。

<11>我们进入LoadAndDisplayImageTask类来看一下它的内容

final class LoadAndDisplayImageTask implements Runnable, IoUtils.CopyListener {
    ...
}

这里该类实现的是一个Runnable接口,说明它就是一个线程,去执行图片的加载。

接着我们看一下它的run方法,代码如下:

@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);
}

可以看到,在run方法中它首先执行了如下代码:

@Override
public void run() {
    if (waitIfPaused()) return;
    ...
}

这个方法的场景主要用于滚动的ListView加载图片的时候。有时候为了使我们滑动更加流畅,我们会选择手指在滑动的时候,不去加载图片。所以才提供了这样的方法。ListView可以设置一个Listener,来使滑动时不加载图片。

在waitIfPaused()方法中,有如下代码:

private boolean waitIfPaused() {
    ...
    if (pause.get()) {
        synchronized (engine.getPauseLock()) {
		    ...
        }
    }
	return isTaskNotActual();
}

synchronized (engine.getPauseLock())其实就是获得找个栈的一个锁,即如果获得一个停止的锁就不去加载找个图片。这个方法最终返回isTaskNotActual(),即不仅要监听ListView的滑动,也要看这个图片加载Task是否是活动的。isTaskNotActual()方法源码如下:

private boolean isTaskNotActual() {
    return isViewCollected() || isViewReused();
}

该方法内部主要判断View是否已被回收,是否这个View被复用了。也就是它会判断ListView在滑动时,ImageView是否被垃圾回收机制回收了。如果回收了的话,刚才所说的Task中run方法就直接返回了,不会做相应的加载。同样,如果ImageView已经被复用了,这时候我们的run方法也会直接返回。为什么会提到复用?ListView当中有很多会复用item的对象,当我们加载ListView时,首先会加载第一页图片。第一页图片还没加载完成就快速滑动。这时候我们使用isViewReused()这个方法来避免这些没有可见的item去加载图片,而直接加载当前已经显示在桌面上的图片。

<11>让我们回到LoadAndDisplayImageTask的run方法中

执行完所有判断后,它会获得一个重入锁

@Override
public void run() {
    ...
    ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
    ...
}

而这个重入锁它是通过ImageLoaderEngine类中的getLockForUri()方法获得的。

而重入锁有什么作用?在ListView中,某个item获取图片时,我们将这个item滚出界面后又滚进来,这时如果我们没有对它加锁,那这个item它又会加载一次图片。这样如果在短时间内频繁滚出滚进的话,那对我们的内存会有很大的消耗。所以在这里我们要判断一个重入锁,就是当ListView的item短时间内不断滚出滚进的时候,我们所进行的锁的判断。

<12>上一步我们根据url获取了一个重入锁对象,接着执行如下代码

@Override
public void run() {
    ...
    loadFromUriLock.lock();
    ...
}

即我们会让重入锁对象在这里进行等待。等到图片加载完成后我们这个锁就会被释放。而刚刚那些相同url的请求,就会执行到接下来的代码中。

<13>接着执行如下代码

@Override
public void run() {
    ...
    try {
        ...
        bmp = configuration.memoryCache.get(memoryCacheKey);
        ...
    } catch (TaskCancelledException e) {
        ...
    } finally {
        ...
    }
    ...
}

可以看到,它会首先从内存中获取Bitmap。

<14>接着会判断这个bmp是否为空,即从内存中有没有获取到图片

@Override
public void run() {
    ...
    try {
        ...
        if (bmp == null || bmp.isRecycled()) {
            ...
        } else {
            ...
        }
        ...
    } catch (TaskCancelledException e) {	
        ...
    } finally {
        ...
    }
    ...
}

如果内存中没有我们要的图片,我们执行如下代码

@Override
public void run() {
    ...
    try {
        ...
        if (bmp == null || bmp.isRecycled()) {
            bmp = tryLoadBitmap();
            ...
        } else {
            ...
        }
        ...
    } catch (TaskCancelledException e) {
        ...
    } finally {
        ...
    }
    ...
}

<15>我们进入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()));
			}
			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);
				}
			}
		} 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 Bitmap tryLoadBitmap() throws TaskCancelledException {
    ...
    try {
        File imageFile = configuration.diskCache.get(uri);
        ...
    } catch (IllegalStateException e) {
        ...
    } catch (TaskCancelledException e) {
        ...
    } catch (IOException e) {
        ...
    } catch (OutOfMemoryError e) {
        ...
    } catch (Throwable e) {
        ...
    }
    ...
}

这句代码意思是我们先从disk中尝试获取该文件。

接着执行如下语句

private Bitmap tryLoadBitmap() throws TaskCancelledException {
    ...
    try {
        ...
        if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
            ...
        }
        ...
    } catch (IllegalStateException e) {
        ...
    } catch (TaskCancelledException e) {
        ...
    } catch (IOException e) {
        ...
    } catch (OutOfMemoryError e) {
        ...
    } catch (Throwable e) {
        ...
    }
    ...
}

这里可以看到,我们对刚刚尝试获取的imageFile对象进行了判断,如果进入该判断,则说明我们成功从disk中获取到了图片。

如果我们从磁盘中获取到了图片,则在if判断中执行了如下代码

private Bitmap tryLoadBitmap() throws TaskCancelledException {
    ...
    try {
        ...
        if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
            ...
            bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
        }
        ...
    } catch (IllegalStateException e) {
        ...
    } catch (TaskCancelledException e) {
        ...
    } catch (IOException e) {
        ...
    } catch (OutOfMemoryError e) {
        ...
    } catch (Throwable e) {
        ...
    }
    ...
}

 在执行完这个if语句后,我们又执行一个判断

private Bitmap tryLoadBitmap() throws TaskCancelledException {
    ...
    try {
        ...
        if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
		    ...
		}
        if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
            ...
        }
    } catch (IllegalStateException e) {
        ...
    } catch (TaskCancelledException e) {
        ...
    } catch (IOException e) {
        ...
    } catch (OutOfMemoryError e) {
        ...
    } catch (Throwable e) {
        ...
    }
    ...
}

如果进入这个判断,说明我们从磁盘中没有获取到相应的图片,只能从网络中去获取图片。

我们可以看一下它是如何做的。在这个if判断中有如下方法

private Bitmap tryLoadBitmap() throws TaskCancelledException {
    ...
    try {
        ...
        if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
            ...
            if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
                ...
			}
            ...
        }
    } catch (IllegalStateException e) {
        ...
    } catch (TaskCancelledException e) {
        ...
    } catch (IOException e) {
        ...
    } catch (OutOfMemoryError e) {
        ...
    } catch (Throwable e) {
        ...
    }
    ...
}

 可以看到我们调用了一个tryCacheImageOnDisk()方法,该方法源码如下

private boolean tryCacheImageOnDisk() throws TaskCancelledException {
    ...
    try {
        loaded = downloadImage();
        if (loaded) {
            ...
            if (width > 0 || height > 0) {
                ...
                resizeAndSaveImage(width, height); 
            }
        }
    } catch (IOException e) {
        ...
    }
    ...
}

在该方法中又调用了downloadImage方法,之后调用了resizeAndSaveImage方法把图片保存下来。resizeAndSaveImage方法就是将图片分别保存在了磁盘和内存当中。

<16>我们回到LoadAndDisplayImageTask的run方法中

@Override
public void run() {
    ...
    DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
    runTask(displayBitmapTask, syncLoading, handler, engine);
}

这是最后两行代码,如果图片已经保存到缓存中,此时就要进行Bitmap的处理。我们进入DisplayBitmapTask的run方法中,代码如下:

@Override
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是否被回收,是否被重用,如果有此类情况,就调用listener的cancel接口取消,如果没有,就调用displayer.display(bitmap, imageAware, loadedFrom);

@Override
public void run() {
    if (imageAware.isCollected()) {
        ...
    } else if (isViewWasReused()) {
        ...
    } else {
        ...
    	displayer.display(bitmap, imageAware, loadedFrom);
        ...
        listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
    }
}

这句代码是把图片显示出来,之后调用listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);告诉我们整个图片加载已完成。

你可能感兴趣的:(BAT大牛亲授技能+技巧,Android面试快速充电升级,缓存)