[关闭]
@946898963 2018-05-18T12:15:50.000000Z 字数 12221 阅读 1131

Volley-ImageLoader的源码解析

Android控件跟框架 Android源码分析


在Volley中,我们不只可以利用ImageRequest请求网络图片,还可以利用ImageLoader加载网络上的图片,并且它的内部也是使用ImageRequest来实现的,不过ImageLoader明显要比ImageRequest更加高效,因为它不仅可以帮我们对图片进行缓存,还可以过滤掉重复的链接,避免重复发送请求。

关于ImageRequest建议阅读:Volley-ImageRequest的源码解析。这里我们先介绍ImageLoader的基本使用,然后再对其源码进行分析。

基本使用

ImageLoader的用法,总结起来大致可以分为以下四步:

  1. 创建一个RequestQueue对象。

  2. 创建一个ImageLoader对象。

  3. 获取一个ImageListener对象。

  4. 调用ImageLoader的get()方法加载网络上的图片。

首先第一步,创建RequestQueue。

  1. RequestQueue requestQueue = Volley.newRequestQueue(getApplicationContext());

第二步,新建一个ImageLoader对象,代码如下所示:

  1. ImageLoader imageLoader = new ImageLoader(mQueue, new ImageCache() {
  2. @Override
  3. public void putBitmap(String url, Bitmap bitmap) {
  4. }
  5. @Override
  6. public Bitmap getBitmap(String url) {
  7. return null;
  8. }
  9. });

可以看到,ImageLoader的构造函数接收两个参数,第一个参数就是RequestQueue对象,第二个参数是一个ImageCache对象,这里我们先new出一个空的ImageCache的实现即可。

接下来需要获取一个ImageListener对象,代码如下所示:

  1. ImageListener listener = ImageLoader.getImageListener(imageView,
  2. R.drawable.default_image, R.drawable.failed_image);

我们通过调用ImageLoader的getImageListener()方法能够获取到一个ImageListener对象,getImageListener()方法接收三个参数,第一个参数指定用于显示图片的ImageView控件,第二个参数指定加载图片的过程中显示的图片,第三个参数指定加载图片失败的情况下显示的图片。
最后,调用ImageLoader的get()方法来加载图片,代码如下所示:

  1. imageLoader.get("http://img.my.csdn.net/uploads/201404/13/1397393290_5765.jpeg", listener);

get()方法接收两个参数,第一个参数就是图片的URL地址,第二个参数则是刚刚获取到的ImageListener对象。当然,如果你想对图片的大小进行限制,也可以使用get()方法的重载,指定图片允许的最大宽度和高度,如下所示:

  1. imageLoader.get("http://img.my.csdn.net/uploads/201404/13/1397393290_5765.jpeg",
  2. listener, 200, 200);

运行一下程序并开始加载图片,你将看到ImageView中会先显示一张默认的图片,等到网络上的图片加载完成后,ImageView则会自动显示该图。

这里创建的ImageCache对象是一个空的实现,完全没能起到图片缓存的作用。这里我们借助Android提供的LruCache写一个ImageCache,关于LruCache的,建议阅读:LruCache源码解析

这里我们新建一个BitmapCache并实现了ImageCache接口,如下所示:

  1. public class BitmapCache implements ImageCache {
  2. private LruCache<String, Bitmap> mCache;
  3. public BitmapCache() {
  4. int maxSize = 10 * 1024 * 1024;
  5. mCache = new LruCache<String, Bitmap>(maxSize) {
  6. @Override
  7. protected int sizeOf(String key, Bitmap bitmap) {
  8. return bitmap.getRowBytes() * bitmap.getHeight();
  9. }
  10. };
  11. }
  12. @Override
  13. public Bitmap getBitmap(String url) {
  14. return mCache.get(url);
  15. }
  16. @Override
  17. public void putBitmap(String url, Bitmap bitmap) {
  18. mCache.put(url, bitmap);
  19. }
  20. }

可以看到,这里我们将缓存图片的大小设置为10M。接着修改创建ImageLoader实例的代码,第二个参数传入BitmapCache的实例,如下所示:

  1. ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache());

这样我们就把ImageLoader的功能优势充分利用起来了。

源码剖析

构造方法:

  1. public ImageLoader(RequestQueue queue, ImageCache imageCache) {
  2. mRequestQueue = queue;
  3. mCache = imageCache;
  4. }

构造方法很简单,就是对成员变量赋值。ImageCache,是ImageLoader的内部接口,源码如下所示:

  1. public interface ImageCache {
  2. public Bitmap getBitmap(String url);
  3. public void putBitmap(String url, Bitmap bitmap);
  4. }

getImageListener方法

  1. public interface ImageListener extends ErrorListener {
  2. public void onResponse(ImageContainer response, boolean isImmediate);
  3. }

可以看到ImageListener是一个接口,并且同时他继承自ErrorListener接口,这个接口位于Response类中:

  1. public interface ErrorListener {
  2. public void onErrorResponse(VolleyError error);
  3. }

那么也就是说在我们定义ImageListener的时候需要实现:onResponse和onErrorResponse这两个方法,下面看getImageListener方法:

  1. public static ImageListener getImageListener(final ImageView view,
  2. final int defaultImageResId, final int errorImageResId) {
  3. return new ImageListener() {
  4. @Override
  5. public void onErrorResponse(VolleyError error) {
  6. if (errorImageResId != 0) {
  7. view.setImageResource(errorImageResId);
  8. }
  9. }
  10. @Override
  11. public void onResponse(ImageContainer response, boolean isImmediate) {
  12. if (response.getBitmap() != null) {
  13. view.setImageBitmap(response.getBitmap());
  14. } else if (defaultImageResId != 0) {
  15. view.setImageResource(defaultImageResId);
  16. }
  17. }
  18. };
  19. }

这里面主要是将回调出来的Bitmap设置给对应的ImageView,以及做一些图片加载的容错处理。

get()方法

  1. public ImageContainer get(String requestUrl, final ImageListener listener) {
  2. return get(requestUrl, listener, 0, 0);
  3. }
  1. public ImageContainer get(String requestUrl, ImageListener imageListener,
  2. int maxWidth, int maxHeight) {
  3. return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
  4. }
  1. public ImageContainer get(String requestUrl, ImageListener imageListener,
  2. int maxWidth, int maxHeight, ScaleType scaleType) {
  3. throwIfNotOnMainThread();
  4. final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
  5. Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
  6. if (cachedBitmap != null) {
  7. ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
  8. imageListener.onResponse(container, true);
  9. return container;
  10. }
  11. ImageContainer imageContainer =
  12. new ImageContainer(null, requestUrl, cacheKey, imageListener);
  13. imageListener.onResponse(imageContainer, true);
  14. BatchedImageRequest request = mInFlightRequests.get(cacheKey);
  15. if (request != null) {
  16. request.addContainer(imageContainer);
  17. return imageContainer;
  18. }
  19. Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
  20. cacheKey);
  21. mRequestQueue.add(newRequest);
  22. mInFlightRequests.put(cacheKey,
  23. new BatchedImageRequest(newRequest, imageContainer));
  24. return imageContainer;
  25. }

最终调用的都是参数最多的那个get方法,源码比较长,我们分开分析。

  1. throwIfNotOnMainThread();
  1. private void throwIfNotOnMainThread() {
  2. if (Looper.myLooper() != Looper.getMainLooper()) {
  3. throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
  4. }
  5. }

首先检查是不是在主线程中调用的get方法,如果不是的话,抛出异常。之所要求必须是在主线程,是因为这个方法中会调用imageListener.onResponse方法,我们知道这个方法中进行了UI操作,所以必须在主线程中调用get方法。

  1. final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
  2. private static String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) {
  3. return new StringBuilder(url.length()+12).append("#W").append(maxWidth).append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url) .toString();
  4. }

根据requestUrl,maxWidth,maxHeight和scaleType得到数据的key值,以对数据进行缓存和查找。

  1. Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
  2. if (cachedBitmap != null) {
  3. ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
  4. imageListener.onResponse(container, true);
  5. return container;
  6. }

从缓存中查找,这里的一级缓存就是我们在创建ImageLoader对象的时候传递进来的实现了ImageCache接口的对象,一般使用LruCache,如果在缓存中能找到Bitmap的话,将查找到的缓存的Bitmap,requestUrl等封装成一个ImageContainer对象,然后调用imageListener.onResponse方法,最后将ImageContainer对象返回。这里我们又看到了ImageContainer这个类,我们看下他的源码:

  1. public class ImageContainer {
  2. private Bitmap mBitmap;
  3. private final ImageListener mListener;
  4. private final String mCacheKey;
  5. private final String mRequestUrl;
  6. public ImageContainer(Bitmap bitmap, String requestUrl,
  7. String cacheKey, ImageListener listener) {
  8. mBitmap = bitmap;
  9. mRequestUrl = requestUrl;
  10. mCacheKey = cacheKey;
  11. mListener = listener;
  12. }
  13. ....
  14. public Bitmap getBitmap() {
  15. return mBitmap;
  16. }
  17. public String getRequestUrl() {
  18. return mRequestUrl;
  19. }
  20. }

可以看到ImageContainer就是一个容器类,封装了一次请求的url,Listener,请求被缓存时候的key值,以及请求的最终结果Bitmap。
继续看get方法的源码,

  1. ImageContainer imageContainer = new ImageContainer(null,requestUrl,cacheKey,imageListener);
  2. imageListener.onResponse(imageContainer, true);

如果查找不到缓存的话,就将请求的url,缓存时候用到的key值,和这次请求的回调封装到一个ImageContainer中去。然后调用imageListener.onResponse方法,之所以在这里调用,是为了显示我们设置的默认图片,默认图片会一直显示,直到请求结束。

  1. private final HashMap<String, BatchedImageRequest> mInFlightRequests = new HashMap<String, BatchedImageRequest>();
  2. BatchedImageRequest request = mInFlightRequests.get(cacheKey);
  3. if (request != null) {
  4. request.addContainer(imageContainer);
  5. return imageContainer;
  6. }

查看这个请求对应的BatchedImageRequest是否已经存在了,如果存在的话,就将这次请求创建的imageContainer放到BatchedImageRequest中去,然后反回imageContainer。
这里这么做是为了防止重复请求,当一次请求结束后,会将重复的请求创建的ImageContainer都从BatchedImageRequest取出来,调用它们的imageListener.onResponse方法。
看下BatchedImageRequest的源码:

  1. private class BatchedImageRequest {
  2. private final Request<?> mRequest;
  3. private Bitmap mResponseBitmap;
  4. private VolleyError mError;
  5. private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
  6. public BatchedImageRequest(Request<?> request, ImageContainer container) {
  7. mRequest = request;
  8. mContainers.add(container);
  9. }
  10. public void setError(VolleyError error) {
  11. mError = error;
  12. }
  13. public VolleyError getError() {
  14. return mError;
  15. }
  16. public void addContainer(ImageContainer container) {
  17. mContainers.add(container);
  18. }
  19. ....
  20. }

BatchedImageRequest也是一个容器类,很简单,就不分析了。
继续看get方法,

  1. Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,cacheKey);
  2. mRequestQueue.add(newRequest);
  3. mInFlightRequests.put(cacheKey,
  4. new BatchedImageRequest(newRequest, imageContainer));
  5. return imageContainer;
  1. protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight,ScaleType scaleType, final String cacheKey) {
  2. return new ImageRequest(requestUrl, new Listener<Bitmap>() {
  3. @Override
  4. public void onResponse(Bitmap response) {
  5. onGetImageSuccess(cacheKey, response);
  6. }
  7. }, maxWidth, maxHeight, scaleType, Config.RGB_565, new ErrorListener() {
  8. @Override
  9. public void onErrorResponse(VolleyError error) {
  10. onGetImageError(cacheKey, error);
  11. }
  12. });
  13. }

创建一个ImageRequest,将其添加到RequestQueue中去执行,并创建一个BatchedImageRequest将其存储到mInFlightRequests中,最后将imageContainer返回。这里我们就清楚了,原来ImageLoader就是利用ImageRequest来完成图片的获取的。我们知道,当一次ImageRequest请求成功后,其传递进去的Listener的onResponse方法就会被调用,这里调用了onGetImageSuccess(cacheKey, response)方法,我们看下这个方法的源码:

  1. protected void onGetImageSuccess(String cacheKey, Bitmap response) {
  2. mCache.putBitmap(cacheKey, response);
  3. BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
  4. if (request != null) {
  5. request.mResponseBitmap = response;
  6. batchResponse(cacheKey, request);
  7. }
  8. }

这个方法很简单,就是将数据缓存起来,然后从mInFlightRequests中取出BatchedImageRequest,将请求的结果Bitmap设置给BatchedImageRequest,然后调用了batchResponse(cacheKey, request)方法,我们继续看这个方法,

  1. private int mBatchResponseDelayMs = 100;
  2. private final Handler mHandler = new Handler(Looper.getMainLooper());
  3. private Runnable mRunnable;
  4. private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<String, BatchedImageRequest>();
  1. private void batchResponse(String cacheKey, BatchedImageRequest request) {
  2. mBatchedResponses.put(cacheKey, request);
  3. if (mRunnable == null) {
  4. mRunnable = new Runnable() {
  5. @Override
  6. public void run() {
  7. for (BatchedImageRequest bir : mBatchedResponses.values()) {
  8. for (ImageContainer container : bir.mContainers) {
  9. if (container.mListener == null) {
  10. continue;
  11. }
  12. if (bir.getError() == null) {
  13. container.mBitmap = bir.mResponseBitmap;
  14. container.mListener.onResponse(container, false);
  15. } else {
  16. container.mListener.onErrorResponse(bir.getError());
  17. }
  18. }
  19. }
  20. mBatchedResponses.clear();
  21. mRunnable = null;
  22. }
  23. };
  24. mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
  25. }
  26. }

首先将BatchedImageRequest添加到待处理的BatchedImageRequest的hashmap中,也即mBatchedResponses,接着开启线程遍历mBatchedResponses,依次获得其中的BatchedImageRequest对象,接着遍历BatchedImageRequest,获得其中的ImageContainer对象,如果请求成功的话执行ImageContainer中的mListener属性的onResponse方法,这个方法同样也是之前getImageListener的onRespose,将图片显示在ImageView上面;如果请求失败的话,会执行ImageContainer中的mListener属性的onErrorResponse方法,这个方法就是调用之前getImageListener方法中的onErrorResponse:

  1. @Override
  2. public void onErrorResponse(VolleyError error) {
  3. if (errorImageResId != 0) {
  4. view.setImageResource(errorImageResId);
  5. }
  6. }

这个方法会在ImageView上面显示加载失败之后的默认图片,最后通过Handler的post方法发送消息,至于这里使用postDelayed的原因我在想可能是为了等待UI线程中的图片显示完成,即保证多个post发送消息顺序,减少主线程的压力

上面讲解ImageContainer的时候,由于整个逻辑的问题,并没有讲解其中的cancelRequest()方法,这里,整个逻辑都已经理清楚了,我们再看看下这个方法:

  1. public void cancelRequest() {
  2. if (mListener == null) {
  3. return;
  4. }
  5. BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
  6. if (request != null) {
  7. boolean canceled = request.removeContainerAndCancelIfNecessary(this);
  8. if (canceled) {
  9. mInFlightRequests.remove(mCacheKey);
  10. }
  11. } else {
  12. // check to see if it is already batched for delivery.
  13. request = mBatchedResponses.get(mCacheKey);
  14. if (request != null) {
  15. request.removeContainerAndCancelIfNecessary(this);
  16. if (request.mContainers.size() == 0) {
  17. mBatchedResponses.remove(mCacheKey);
  18. }
  19. }
  20. }
  21. }

cancelRequest方法首先从mInFlifhtRequests中取出key值为mCacheKey的BatchedImageRequest,mInFlifhtRequests存储的是我们正在运行的图片请求集合,如果对应mCacheKey的请求不为null的话,进入if语句块调用BatchedImageRequest的removeContainerAndCancelIfNecessary来暂停当前ImageContainer请求,这个方法的源码:

  1. public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
  2. mContainers.remove(container);
  3. if (mContainers.size() == 0) {
  4. mRequest.cancel();
  5. return true;
  6. }
  7. return false;
  8. }

可以看到首先他会从ImageContainer集合中删除当前ImageContainer,接着判断集合中是否还有元素,有的话返回false,没有的话会直接将当前BatchedImageRequest暂停掉并且返回true,回到ImageContainer的cancelRequest方法,removeContainerAndCancelIfNecessary的返回值会赋给canceled,如果canceled为true的话表示在当前BatchedImageRequest下已经没有ImageContainer请求了,那么直接将其从正在运行的图片请求集合中移除当前请求;对应mCacheKey的请求本身就不存在于正在运行的图片请求集合mInFlifhtRequests中的话,则会进入else子句到mBatchedResponses从查看是否存在对应于mCacheKey的BatchedImageRequest对象存在,mBatchedResponses存储的是当前被挂起的BatchedImageRequest请求,当然一个BatchedImageRequest请求下面有多个ImageContainer请求,如果在挂起的请求中存在当前mCacheKey对应的BatchedImageRequest请求的话,则进入if语句块,接下来的代码和上面一样。

至此,使用Volley的ImageLoader加载图片的源码分析完毕了。

最后做一下总结:

(1)使用ImageLoader加载图片,在其内部的话会用到ImageContainer和BatchedImageRequest这两个类,其中BatchedImageRequest主要用于对请求进行封装,ImageContainer主要用于对数据进行封装;

(2)一个BatchedImageRequest请求可能对应于多个ImageContainer数组,因此BatchedImageRequest里面有一个ImageContainer类型的LinkedList,这样做的话很大程度上过滤掉了相同的url;

(3)ImageLoader中存在着两个缓存请求的HashMap,分别是正在运行的请求缓存和已经处理完的请求缓存,当正在运行的请求执行结束之后会进行分发操作来执行待处理的请求。

android-----Volley框架使用ImageLoader加载图片源码分析

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注