文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Android Volley图片加载功能详解

2022-06-06 07:33

关注

Gituhb项目

Volley源码中文注释项目我已经上传到github,欢迎大家fork和start.

为什么写这篇博客

本来文章是维护在github上的,但是我在分析ImageLoader源码过程中与到了一个问题,希望大家能帮助解答.

Volley获取网络图片 

本来想分析Universal Image Loader的源码,但是发现Volley已经实现了网络图片的加载功能.其实,网络图片的加载也是分几个步骤:
1. 获取网络图片的url.
2. 判断该url对应的图片是否有本地缓存.
3. 有本地缓存,直接使用本地缓存图片,通过异步回调给ImageView进行设置.
4. 无本地缓存,就先从网络拉取,保存在本地后,再通过异步回调给ImageView进行设置.

我们通过Volley源码,看一下Volley是否是按照这个步骤实现网络图片加载的.

ImageRequest.java

按照Volley的架构,我们首先需要构造一个网络图片请求,Volley帮我们封装了ImageRequest类,我们来看一下它的具体实现:



@SuppressWarnings("unused")
public class ImageRequest extends Request<Bitmap> {
  
  public static final int DEFAULT_IMAGE_REQUEST_MS = 1000;
  
  public static final int DEFAULT_IMAGE_MAX_RETRIES = 2;
  private final Response.Listener<Bitmap> mListener;
  private final Bitmap.Config mDecodeConfig;
  private final int mMaxWidth;
  private final int mMaxHeight;
  private ImageView.ScaleType mScaleType;
  
  private static final Object sDecodeLock = new Object();
  
  public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
            ImageView.ScaleType scaleType, Bitmap.Config decodeConfig,
            Response.ErrorListener errorListener) {
    super(Method.GET, url, errorListener);
    mListener = listener;
    mDecodeConfig = decodeConfig;
    mMaxWidth = maxWidth;
    mMaxHeight = maxHeight;
    mScaleType = scaleType;
  }
  
  @Override
  public Priority getPriority() {
    return Priority.LOW;
  }
  @Override
  protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
    synchronized (sDecodeLock) {
      try {
        return doParse(response);
      } catch (OutOfMemoryError e) {
        return Response.error(new VolleyError(e));
      }
    }
  }
  private Response<Bitmap> doParse(NetworkResponse response) {
    byte[] data = response.data;
    BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
    Bitmap bitmap;
    if (mMaxWidth == 0 && mMaxHeight == 0) {
      decodeOptions.inPreferredConfig = mDecodeConfig;
      bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
    } else {
      // 获取网络图片的真实尺寸.
      decodeOptions.inJustDecodeBounds = true;
      BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
      int actualWidth = decodeOptions.outWidth;
      int actualHeight = decodeOptions.outHeight;
      int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
          actualWidth, actualHeight, mScaleType);
      int desireHeight = getResizedDimension(mMaxWidth, mMaxHeight,
          actualWidth, actualHeight, mScaleType);
      decodeOptions.inJustDecodeBounds = false;
      decodeOptions.inSampleSize =
          findBestSampleSize(actualWidth, actualHeight, desiredWidth, desireHeight);
      Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
      if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
          tempBitmap.getHeight() > desireHeight)) {
        bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desireHeight, true);
        tempBitmap.recycle();
      } else {
        bitmap = tempBitmap;
      }
    }
    if (bitmap == null) {
      return Response.error(new VolleyError(response));
    } else {
      return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
    }
  }
  static int findBestSampleSize(
      int actualWidth, int actualHeight, int desiredWidth, int desireHeight) {
    double wr = (double) actualWidth / desiredWidth;
    double hr = (double) actualHeight / desireHeight;
    double ratio = Math.min(wr, hr);
    float n = 1.0f;
    while ((n * 2) <= ratio) {
      n *= 2;
    }
    return (int) n;
  }
  
  private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
                      int actualSecondary, ImageView.ScaleType scaleType) {
    // 如果没有设置ImageView的最大值,则直接返回网络图片的真实大小.
    if ((maxPrimary == 0) && (maxSecondary == 0)) {
      return actualPrimary;
    }
    // 如果ImageView的ScaleType为FIX_XY,则将其设置为图片最值.
    if (scaleType == ImageView.ScaleType.FIT_XY) {
      if (maxPrimary == 0) {
        return actualPrimary;
      }
      return maxPrimary;
    }
    if (maxPrimary == 0) {
      double ratio = (double)maxSecondary / (double)actualSecondary;
      return (int)(actualPrimary * ratio);
    }
    if (maxSecondary == 0) {
      return maxPrimary;
    }
    double ratio = (double) actualSecondary / (double) actualPrimary;
    int resized = maxPrimary;
    if (scaleType == ImageView.ScaleType.CENTER_CROP) {
      if ((resized * ratio) < maxSecondary) {
        resized = (int)(maxSecondary / ratio);
      }
      return resized;
    }
    if ((resized * ratio) > maxSecondary) {
      resized = (int)(maxSecondary / ratio);
    }
    return resized;
  }
  @Override
  protected void deliverResponse(Bitmap response) {
    mListener.onResponse(response);
  }
}

因为Volley本身框架已经实现了对网络请求的本地缓存,所以ImageRequest做的主要事情就是解析字节流为Bitmap,再解析过程中,通过静态变量保证每次只解析一个Bitmap防止OOM,使用ScaleType和用户设置的MaxWidth和MaxHeight来设置图片大小.
总体来说,ImageRequest的实现非常简单,这里不做过多的讲解.ImageRequest的缺陷在于:

1.需要用户进行过多的设置,包括图片的大小的最大值.
2.没有图片的内存缓存,因为Volley的缓存是基于Disk的缓存,有对象反序列化的过程. 

ImageLoader.java

鉴于以上两个缺点,Volley又提供了一个更牛逼的ImageLoader类.其中,最关键的就是增加了内存缓存.
再讲解ImageLoader的源码之前,需要先介绍一下ImageLoader的使用方法.和之前的Request请求不同,ImageLoader并不是new出来直接扔给RequestQueue进行调度,它的使用方法大体分为4步:

 •创建一个RequestQueue对象. 

RequestQueue queue = Volley.newRequestQueue(context);

 •创建一个ImageLoader对象.

ImageLoader构造函数接收两个参数,第一个是RequestQueue对象,第二个是ImageCache对象(也就是内存缓存类,我们先不给出具体实现,讲解完ImageLoader源码之后,我会提供一个利用LRU算法的ImageCache实现类) 


ImageLoader imageLoader = new ImageLoader(queue, new ImageCache() {
  @Override
  public void putBitmap(String url, Bitmap bitmap) {}
  @Override
  public Bitmap getBitmap(String url) { return null; }
});

 •获取一个ImageListener对象. 

ImageListener listener = ImageLoader.getImageListener(imageView, R.drawable.default_imgage, R.drawable.failed_image); 

•调用ImageLoader的get方法加载网络图片. 

imageLoader.get(mImageUrl, listener, maxWidth, maxHeight, scaleType);

有了ImageLoader的使用方法,我们结合使用方法来看一下ImageLoader的源码:


@SuppressWarnings({"unused", "StringBufferReplaceableByString"})
public class ImageLoader {
  
  private final RequestQueue mRequestQueue;
  
  private final ImageCache mCache;
  
  private final HashMap<String, BatchedImageRequest> mInFlightRequests =
      new HashMap<String, BatchedImageRequest>();
  private final HashMap<String, BatchedImageRequest> mBatchedResponses =
      new HashMap<String, BatchedImageRequest>();
  
  private final Handler mHandler = new Handler(Looper.getMainLooper());
  private Runnable mRunnable;
  
  public interface ImageCache {
    Bitmap getBitmap(String url);
    void putBitmap(String url, Bitmap bitmap);
  }
  
  public ImageLoader(RequestQueue queue, ImageCache imageCache) {
    mRequestQueue = queue;
    mCache = imageCache;
  }
  
  public static ImageListener getImageListener(final ImageView view, final int defaultImageResId,
                         final int errorImageResId) {
    return new ImageListener() {
      @Override
      public void onResponse(ImageContainer response, boolean isImmediate) {
        if (response.getBitmap() != null) {
          view.setImageBitmap(response.getBitmap());
        } else if (defaultImageResId != 0) {
          view.setImageResource(defaultImageResId);
        }
      }
      @Override
      public void onErrorResponse(VolleyError error) {
        if (errorImageResId != 0) {
          view.setImageResource(errorImageResId);
        }
      }
    };
  }
  public ImageContainer get(String requestUrl, ImageListener imageListener,
                int maxWidth, int maxHeight, ScaleType scaleType) {
    // 判断当前方法是否在UI线程中执行.如果不是,则抛出异常.
    throwIfNotOnMainThread();
    final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
    // 从L1级缓存中根据key获取对应的Bitmap.
    Bitmap cacheBitmap = mCache.getBitmap(cacheKey);
    if (cacheBitmap != null) {
      // L1缓存命中,通过缓存命中的Bitmap构造ImageContainer,并调用imageListener的响应成功接口.
      ImageContainer container = new ImageContainer(cacheBitmap, requestUrl, null, null);
      // 注意:因为目前是在UI线程中,因此这里是调用onResponse方法,并非回调.
      imageListener.onResponse(container, true);
      return container;
    }
    ImageContainer imageContainer =
        new ImageContainer(null, requestUrl, cacheKey, imageListener);
    // L1缓存命中失败,则先需要对ImageView设置默认图片.然后通过子线程拉取网络图片,进行显示.
    imageListener.onResponse(imageContainer, true);
    // 检查cacheKey对应的ImageRequest请求是否正在运行.
    BatchedImageRequest request = mInFlightRequests.get(cacheKey);
    if (request != null) {
      // 相同的ImageRequest正在运行,不需要同时运行相同的ImageRequest.
      // 只需要将其对应的ImageContainer加入到BatchedImageRequest的mContainers集合中.
      // 当正在执行的ImageRequest结束后,会查看当前有多少正在阻塞的ImageRequest,
      // 然后对其mContainers集合进行回调.
      request.addContainer(imageContainer);
      return imageContainer;
    }
    // L1缓存没命中,还是需要构造ImageRequest,通过RequestQueue的调度来获取网络图片
    // 获取方法可能是:L2缓存(ps:Disk缓存)或者HTTP网络请求.
    Request<Bitmap> newRequest =
        makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey);
    mRequestQueue.add(newRequest);
    mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer));
    return imageContainer;
  }
  
  private String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) {
    return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
        .append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url)
        .toString();
  }
  public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
    return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
  }
  private boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) {
    throwIfNotOnMainThread();
    String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
    return mCache.getBitmap(cacheKey) != null;
  }
  
  protected Request<Bitmap> makeImageRequest(final String requestUrl, int maxWidth, int maxHeight,
                        ScaleType scaleType, final String cacheKey) {
    return new ImageRequest(requestUrl, new Response.Listener<Bitmap>() {
      @Override
      public void onResponse(Bitmap response) {
        onGetImageSuccess(cacheKey, response);
      }
    }, maxWidth, maxHeight, scaleType, Bitmap.Config.RGB_565, new Response.ErrorListener() {
      @Override
      public void onErrorResponse(VolleyError error) {
        onGetImageError(cacheKey, error);
      }
    });
  }
  
  private void onGetImageError(String cacheKey, VolleyError error) {
    BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
    if (request != null) {
      request.setError(error);
      batchResponse(cacheKey, request);
    }
  }
  
  protected void onGetImageSuccess(String cacheKey, Bitmap response) {
    // 增加L1缓存的键值对.
    mCache.putBitmap(cacheKey, response);
    // 同一时间内最初的ImageRequest执行成功后,回调这段时间阻塞的相同ImageRequest对应的成功回调接口.
    BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
    if (request != null) {
      request.mResponseBitmap = response;
      // 将阻塞的ImageRequest进行结果分发.
      batchResponse(cacheKey, request);
    }
  }
  private void batchResponse(String cacheKey, BatchedImageRequest request) {
    mBatchedResponses.put(cacheKey, request);
    if (mRunnable == null) {
      mRunnable = new Runnable() {
        @Override
        public void run() {
          for (BatchedImageRequest bir : mBatchedResponses.values()) {
            for (ImageContainer container : bir.mContainers) {
              if (container.mListener == null) {
                continue;
              }
              if (bir.getError() == null) {
                container.mBitmap = bir.mResponseBitmap;
                container.mListener.onResponse(container, false);
              } else {
                container.mListener.onErrorResponse(bir.getError());
              }
            }
          }
          mBatchedResponses.clear();
          mRunnable = null;
        }
      };
      // Post the runnable
      mHandler.postDelayed(mRunnable, 100);
    }
  }
  private void throwIfNotOnMainThread() {
    if (Looper.myLooper() != Looper.getMainLooper()) {
      throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
    }
  }
  
  public interface ImageListener extends Response.ErrorListener {
    void onResponse(ImageContainer response, boolean isImmediate);
  }
  
  public class ImageContainer {
    
    private Bitmap mBitmap;
    
    private final String mCacheKey;
    
    private final String mRequestUrl;
    
    private final ImageListener mListener;
    public ImageContainer(Bitmap bitmap, String requestUrl, String cacheKey,
               ImageListener listener) {
      mBitmap = bitmap;
      mRequestUrl = requestUrl;
      mCacheKey = cacheKey;
      mListener = listener;
    }
    public void cancelRequest() {
      if (mListener == null) {
        return;
      }
      BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
      if (request != null) {
        boolean canceled = request.removeContainerAndCancelIfNecessary(this);
        if (canceled) {
          mInFlightRequests.remove(mCacheKey);
        }
      } else {
        request = mBatchedResponses.get(mCacheKey);
        if (request != null) {
          request.removeContainerAndCancelIfNecessary(this);
          if (request.mContainers.size() == 0) {
            mBatchedResponses.remove(mCacheKey);
          }
        }
      }
    }
    public Bitmap getBitmap() {
      return mBitmap;
    }
    public String getRequestUrl() {
      return mRequestUrl;
    }
  }
  
  private class BatchedImageRequest {
    
    private final Request<?> mRequest;
    
    private Bitmap mResponseBitmap;
    
    private VolleyError mError;
    
    private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
    public BatchedImageRequest(Request<?> request, ImageContainer container) {
      mRequest = request;
      mContainers.add(container);
    }
    public VolleyError getError() {
      return mError;
    }
    public void setError(VolleyError error) {
      mError = error;
    }
    public void addContainer(ImageContainer container) {
      mContainers.add(container);
    }
    public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
      mContainers.remove(container);
      if (mContainers.size() == 0) {
        mRequest.cancel();
        return true;
      }
      return false;
    }
  }
}

重大疑问

个人对Imageloader的源码有两个重大疑问?

 •batchResponse方法的实现. 

我很奇怪,为什么ImageLoader类里面要有一个HashMap来保存BatchedImageRequest集合呢?


 private final HashMap<String, BatchedImageRequest> mBatchedResponses =
    new HashMap<String, BatchedImageRequest>();

毕竟batchResponse是在特定的ImageRequest执行成功的回调中被调用的,调用代码如下:


  protected void onGetImageSuccess(String cacheKey, Bitmap response) {
    // 增加L1缓存的键值对.
    mCache.putBitmap(cacheKey, response);
    // 同一时间内最初的ImageRequest执行成功后,回调这段时间阻塞的相同ImageRequest对应的成功回调接口.
    BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
    if (request != null) {
      request.mResponseBitmap = response;
      // 将阻塞的ImageRequest进行结果分发.
      batchResponse(cacheKey, request);
    }
  }

从上述代码可以看出,ImageRequest请求成功后,已经从mInFlightRequests中获取了对应的BatchedImageRequest对象.而同一时间被阻塞的相同的ImageRequest对应的ImageContainer都在BatchedImageRequest的mContainers集合中.
那我认为,batchResponse方法只需要遍历对应BatchedImageRequest的mContainers集合即可.
但是,ImageLoader源码中,我认为多余的构造了一个HashMap对象mBatchedResponses来保存BatchedImageRequest集合,然后在batchResponse方法中又对集合进行两层for循环各种遍历,实在是非常诡异,求指导.
诡异代码如下:


  private void batchResponse(String cacheKey, BatchedImageRequest request) {
    mBatchedResponses.put(cacheKey, request);
    if (mRunnable == null) {
      mRunnable = new Runnable() {
        @Override
        public void run() {
          for (BatchedImageRequest bir : mBatchedResponses.values()) {
            for (ImageContainer container : bir.mContainers) {
              if (container.mListener == null) {
                continue;
              }
              if (bir.getError() == null) {
                container.mBitmap = bir.mResponseBitmap;
                container.mListener.onResponse(container, false);
              } else {
                container.mListener.onErrorResponse(bir.getError());
              }
            }
          }
          mBatchedResponses.clear();
          mRunnable = null;
        }
      };
      // Post the runnable
      mHandler.postDelayed(mRunnable, 100);
    }
  }

我认为的代码实现应该是:


  private void batchResponse(String cacheKey, BatchedImageRequest request) {
    if (mRunnable == null) {
      mRunnable = new Runnable() {
        @Override
        public void run() {
          for (ImageContainer container : request.mContainers) {
            if (container.mListener == null) {
              continue;
            }
            if (request.getError() == null) {
              container.mBitmap = request.mResponseBitmap;
              container.mListener.onResponse(container, false);
            } else {
              container.mListener.onErrorResponse(request.getError());
            }
          }
          mRunnable = null;
        }
      };
      // Post the runnable
      mHandler.postDelayed(mRunnable, 100);
    }
  }

 •使用ImageLoader默认提供的ImageListener,我认为存在一个缺陷,即图片闪现问题.当为ListView的item设置图片时,需要增加TAG判断.因为对应的ImageView可能已经被回收利用了. 

自定义L1缓存类

首先说明一下,所谓的L1和L2缓存分别指的是内存缓存和硬盘缓存.
实现L1缓存,我们可以使用Android提供的Lru缓存类,示例代码如下:


import android.graphics.Bitmap;
import android.support.v4.util.LruCache;

@SuppressWarnings("unused")
public class ImageLruCache implements ImageLoader.ImageCache {
  private LruCache<String, Bitmap> mLruCache;
  public ImageLruCache() {
    this((int) Runtime.getRuntime().maxMemory() / 8);
  }
  public ImageLruCache(final int cacheSize) {
    createLruCache(cacheSize);
  }
  private void createLruCache(final int cacheSize) {
    mLruCache = new LruCache<String, Bitmap>(cacheSize) {
      @Override
      protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight();
      }
    };
  }
  @Override
  public Bitmap getBitmap(String url) {
    return mLruCache.get(url);
  }
  @Override
  public void putBitmap(String url, Bitmap bitmap) {
    mLruCache.put(url, bitmap);
  }
}
您可能感兴趣的文章:Android 中Volley二次封装并实现网络请求缓存Android Volley框架全面解析Android Volley框架使用方法详解Android中Volley框架进行请求网络数据的使用Android开发中使用Volley库发送HTTP请求的实例教程Android的HTTP操作库Volley的基本使用教程Android的HTTP类库Volley入门学习教程Android Volley框架使用源码分享Android 开发中Volley详解及实例Android中volley封装实践记录


阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯