Android Volley图片加载功能详解

Android Volley图片加载功能详解,第1张

概述Gituhb项目Volley源码中文注释项目我已经上传到github,欢迎大家fork和start.为什么写这篇博客

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;  /** Bitmap解析同步锁,保证同一时间只有一个Bitmap被load到内存进行解析,防止OOM. */  private static final Object sDecodeLock = new Object();  /**   * 构造一个网络图片请求.   * @param url 图片的URL地址.   * @param Listener 请求成功用户设置的回调接口.   * @param maxWIDth 图片的最大宽度.   * @param maxHeight 图片的最大高度.   * @param scaleType 图片缩放类型.   * @param decodeConfig 解析bitmap的配置.   * @param errorListener 请求失败用户设置的回调接口.   */  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,data.length,decodeOptions);    } else {      // 获取网络图片的真实尺寸.      decodeOptions.inJustDecodeBounds = true;      BitmapFactory.decodeByteArray(data,decodeOptions);      int actualWIDth = decodeOptions.outWIDth;      int actualHeight = decodeOptions.outHeight;      int desireDWIDth = getResizedDimension(mMaxWIDth,mMaxHeight,actualWIDth,actualHeight,mScaleType);      int desireHeight = getResizedDimension(mMaxWIDth,mScaleType);      decodeOptions.inJustDecodeBounds = false;      decodeOptions.inSampleSize =          findBestSampleSize(actualWIDth,desireDWIDth,desireHeight);      Bitmap tempBitmap = BitmapFactory.decodeByteArray(data,decodeOptions);      if (tempBitmap != null && (tempBitmap.getWIDth() > desireDWIDth ||          tempBitmap.getHeight() > desireHeight)) {        bitmap = Bitmap.createScaledBitmap(tempBitmap,desireHeight,true);        tempBitmap.recycle();      } else {        bitmap = tempBitmap;      }    }    if (bitmap == null) {      return Response.error(new VolleyError(response));    } else {      return Response.success(bitmap,@R_419_6822@headerParser.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;  }  /** 根据ImageVIEw的ScaleType设置图片的大小. */  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 {  /**   * 关联用来调用ImageLoader的RequestQueue.   */  private final RequestQueue mRequestQueue;  /** 图片内存缓存接口实现类. */  private final ImageCache mCache;  /** 存储同一时间执行的相同CacheKey的BatchedImageRequest集合. */  private final HashMap<String,BatchedImageRequest> mInFlightRequests =      new HashMap<String,BatchedImageRequest>();  private final HashMap<String,BatchedImageRequest> mBatchedResponses =      new HashMap<String,BatchedImageRequest>();  /** 获取主线程的Handler. */  private final Handler mHandler = new Handler(Looper.getMainLooper());  private Runnable mRunnable;  /** 定义图片K1缓存接口,即将图片的内存缓存工作交给用户来实现. */  public interface ImageCache {    Bitmap getBitmap(String url);    voID putBitmap(String url,Bitmap bitmap);  }  /** 构造一个ImageLoader. */  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,ScaleType scaleType) {    // 判断当前方法是否在UI线程中执行.如果不是,则抛出异常.    throwIfNotOnMainThread();    final String cacheKey = getCacheKey(requestUrl,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,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缓存)或者@R_419_6822@网络请求.    Request<Bitmap> newRequest =        makeImageRequest(requestUrl,scaleType,cacheKey);    mRequestQueue.add(newRequest);    mInFlightRequests.put(cacheKey,new BatchedImageRequest(newRequest,imageContainer));    return imageContainer;  }  /** 构造L1缓存的key值. */  private String getCacheKey(String url,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 maxHeight) {    return isCached(requestUrl,ScaleType.CENTER_INSIDE);  }  private boolean isCached(String requestUrl,ScaleType scaleType) {    throwIfNotOnMainThread();    String cacheKey = getCacheKey(requestUrl,scaleType);    return mCache.getBitmap(cacheKey) != null;  }  /** 当L1缓存没有命中时,构造ImageRequest,通过ImageRequest和RequestQueue获取图片. */  protected Request<Bitmap> makeImageRequest(final String requestUrl,ScaleType scaleType,final String cacheKey) {    return new ImageRequest(requestUrl,new Response.Listener<Bitmap>() {      @OverrIDe      public voID onResponse(Bitmap response) {        onGetimageSuccess(cacheKey,response);      }    },Bitmap.Config.RGB_565,new Response.ErrorListener() {      @OverrIDe      public voID onErrorResponse(VolleyError error) {        onGetimageError(cacheKey,error);      }    });  }  /** 图片请求失败回调.运行在UI线程中. */  private voID onGetimageError(String cacheKey,VolleyError error) {    BatchedImageRequest request = mInFlightRequests.remove(cacheKey);    if (request != null) {      request.setError(error);      batchResponse(cacheKey,request);    }  }  /** 图片请求成功回调.运行在UI线程中. */  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.");    }  }  /** 抽象出请求成功和失败的回调接口.默认可以使用Volley提供的ImageListener. */  public interface ImageListener extends Response.ErrorListener {    voID onResponse(ImageContainer response,boolean isImmediate);  }  /** 网络图片请求的承载对象. */  public class ImageContainer {    /** ImageVIEw需要加载的Bitmap. */    private Bitmap mBitmap;    /** L1缓存的key */    private final String mCacheKey;    /** ImageRequest请求的url. */    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;    }  }  /**   * CacheKey相同的ImageRequest请求抽象类.   * 判定两个ImageRequest相同包括:   * 1. url相同.   * 2. maxWIDth和maxHeight相同.   * 3. 显示的scaleType相同.   * 同一时间可能有多个相同CacheKey的ImageRequest请求,由于需要返回的Bitmap都一样,所以用BatchedImageRequest   * 来实现该功能.同一时间相同CacheKey的ImageRequest只能有一个.   * 为什么不使用RequestQueue的mWaitingRequestQueue来实现该功能?   * 答:是因为仅靠URL是没法判断两个ImageRequest相等的.   */  private class BatchedImageRequest {    /** 对应的ImageRequest请求. */    private final Request<?> mRequest;    /** 请求结果的Bitmap对象. */    private Bitmap mResponseBitmap;    /** ImageRequest的错误. */    private VolleyError mError;    /** 所有相同ImageRequest请求结果的封装集合. */    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,request);    }  }

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

  private voID batchResponse(String cacheKey,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;/** Lru算法的L1缓存实现类. */@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图片加载功能详解所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1148110.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-31
下一篇 2022-05-31

发表评论

登录后才能评论

评论列表(0条)

保存