上一篇我们分析了Volley框架的源码,知道了使用Volley框架的三个步骤,如果你对此还不是很熟,建议能看看上一篇博文:android-----Volley框架源码分析,这篇我们将首先使用Volley框架的ImageLoader来实现加载图片的功能,接着从源码角度来分析加载流程;
使用ImageLoader来加载图片步骤:
(1)创建一个RequestQueue对象;
(2)创建一个ImageLoader对象;
(3)获取一个ImageListener对象(通过ImageLoader的getImageListener方法来获取);
(4)调用ImageLoader的get方法来加载图片;
创建一个RequestQueue,很简单,使用Volley的静态方法newRequestQueue
//创建RequestQueue队列 RequestQueue queue = Volley.newRequestQueue(this);创建一个ImageLoader对象
ImageLoader loader = new ImageLoader(queue,imageCache);其中imageCache是一个ImageCache类型对象,这里我们定义一个实现ImageCache接口的类
class MyImageCache implements ImageCache { public LruCache<String, Bitmap> lruCache; public MyImageCache(LruCache<String, Bitmap> lruCache) { this.lruCache = lruCache; } @Override public Bitmap getBitmap(String url) { Bitmap bitmap = lruCache.get(url); if(bitmap != null) return bitmap; return null; } @Override public void putBitmap(String url, Bitmap bitmap) { if(lruCache.get(url) == null) lruCache.put(url, bitmap); } }他的构造函数会传入一个LruCache对象,并定义了getBitmap和putBitmap充分使用了LruCache内存缓存
LruCache的定义:
//获得可用内存的大小 int size = (int) (Runtime.getRuntime().maxMemory()/1024); //设置LruCache缓存的大小 int cacheSize = size/8; LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(cacheSize){ @Override protected int sizeOf(String key, Bitmap value) { //返回每个图片占用的字节数(单位是KB) return value.getByteCount()/1024; } };最后就是创建ImageListener对象了
//创建ImageListener对象 ImageListener listener = ImageLoader.getImageListener(mImageView,R.drawable.before, R.drawable.now);getmageListener有三个参数,第一个参数指的是被加载图片显示的控件,第二个参数指的是默认显示图片的ID,第三个参数指的是加载图片失败的情况下所要显示图片的ID
之后就是调用ImageLoader的get方法来获取图片了
//调用ImageLoader的get方法获取图片 loader.get("http://.......", listener);至此,ImageLoader加载图片流程已经走完,接下来我们从源码角度看看具体流程中的细节:
(1)创建RequestQueue,这部分的源码之前已经分析过了,在此不再赘述;
(2)创建ImageLoader对象:
public ImageLoader(RequestQueue queue, ImageCache imageCache) { mRequestQueue = queue; mCache = imageCache; }
这个构造函数第一个参数就是我们前面创建的RequestQueue队列,第二个参数是一个实现了ImageCache接口的类的实例:
public interface ImageCache { public Bitmap getBitmap(String url); public void putBitmap(String url, Bitmap bitmap); }
可以看到ImageCache定义了两个方法getBitmap和putBitmap,这两个方法就是我们在定义实现ImageCache接口的类的时候需要实现的。具体可以用来将Bitmap添加至缓存或者从缓存中读取Bitmap图片了;
(3)接着就是调用ImageLoader的getImageListener获得一个ImageListener对象了:
public interface ImageListener extends ErrorListener { public void onResponse(ImageContainer response, boolean isImmediate); }可以看到ImageListener是一个接口,并且同时他继承自ErrorListener接口,这个接口位于Response类中:
public interface ErrorListener { public void onErrorResponse(VolleyError error); }那么也就是说在我们定义ImageListener的时候需要实现:onResponse和onErrorResponse这两个方法;
我们来看看getImageListener是怎么返回ImageListener对象的:
public static ImageListener getImageListener(final ImageView view, final int defaultImageResId, final int errorImageResId) { return new ImageListener() { @Override public void onErrorResponse(VolleyError error) { if (errorImageResId != 0) { view.setImageResource(errorImageResId); } } @Override public void onResponse(ImageContainer response, boolean isImmediate) { if (response.getBitmap() != null) { view.setImageBitmap(response.getBitmap()); } else if (defaultImageResId != 0) { view.setImageResource(defaultImageResId); } } }; }
getImageListener有三个参数,第一个是我们想要显示图片的控件,第二个参数指的是默认加载的图片的资源id,第三个参数是加载图片失败之后显示的图片的资源id,看到第3行开始创建了ImageListener对象,并且实现了onErrorResponse方法,这个方法会在图片加载失败之后调用,而onResponse方法就是在图片加载成功之后调用了,那么这两个方法具体是由谁调用的呢?待会马上就能看到啦!
有了ImageListener之后,接下来就是调用ImageLoader的get方法来获取图片,先来看看get方法,这个方法会传入请求图片所在的地址以及ImageListener对象:
public ImageContainer get(String requestUrl, final ImageListener listener) { return get(requestUrl, listener, 0, 0); }他会调用四个参数的get方法:
public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight) { // only fulfill requests that were initiated from the main thread. throwIfNotOnMainThread(); final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight); // Try to look up the request in the cache of remote images. Bitmap cachedBitmap = mCache.getBitmap(cacheKey); if (cachedBitmap != null) { // Return the cached bitmap. ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null); imageListener.onResponse(container, true); return container; } // The bitmap did not exist in the cache, fetch it! ImageContainer imageContainer = new ImageContainer(null, requestUrl, cacheKey, imageListener); // Update the caller to let them know that they should use the default bitmap. imageListener.onResponse(imageContainer, true); // Check to see if a request is already in-flight. BatchedImageRequest request = mInFlightRequests.get(cacheKey); if (request != null) { // If it is, add this request to the list of listeners. request.addContainer(imageContainer); return imageContainer; } // The request is not already in flight. Send the new request to the network and // track it. Request<?> newRequest = new ImageRequest(requestUrl, new Listener<Bitmap>() { @Override public void onResponse(Bitmap response) { onGetImageSuccess(cacheKey, response); } }, maxWidth, maxHeight, Config.RGB_565, new ErrorListener() { @Override public void onErrorResponse(VolleyError error) { onGetImageError(cacheKey, error); } }); mRequestQueue.add(newRequest); mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); return imageContainer; }
get方法的四个参数分别表示:第一个参数是所请求的图片地址,第二个参数是我们的ImageListener对象,第三个参数指的是返回图片的最大宽度,设置为0表示宽度等于图片本身宽度,不会对其采取任何压缩措施,第四个参数指的是返回图片的最大高度,设置为0表示高度等于图片本身高度;
在正式的讲解get方法源码之前,我们需要首先了解一下ImageLoader里面的ImageContainer和BatchedImageRequest这两个类是用来做什么的,因为这两者在get方法中都用到了:
先来看ImageContainer:
public class ImageContainer { private Bitmap mBitmap; private final ImageListener mListener; private final String mCacheKey; private final String mRequestUrl; /** * Constructs a BitmapContainer object. * @param bitmap The final bitmap (if it exists). * @param requestUrl The requested URL for this container. * @param cacheKey The cache key that identifies the requested URL for this container. */ 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 { // check to see if it is already batched for delivery. 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; } }
这个类其实就是对图片请求的相关数据进行了封装而已,包括图片对象、请求路径url、图片缓存key(这个key值里面即包含请求图片的地址以及显示的这个图片宽和高尺寸限制,是通过getCacheKey方法拼装的)、图片回调监听器,还包括一个取消请求的方法,我们来看看cancelRequest做了些什么事,第29行会首先从mInFlifhtRequests中取出key值为mCacheKey的BatchedImageRequest ,mInFlifhtRequests是HashMap<String, BatchedImageRequest>类型的对象,他存储的是我们正在运行的图片请求集合,如果对应mCacheKey的请求不为null的话,进入if语句块第31行调用BatchedImageRequest的removeContainerAndCancelIfNecessary来暂停当前ImageContainer请求,这个方法的源码:
public boolean removeContainerAndCancelIfNecessary(ImageContainer container) { mContainers.remove(container); if (mContainers.size() == 0) { mRequest.cancel(); return true; } return false; }可以看到首先他会从ImageContainer集合中删除当前ImageContainer,接着判断集合中是否还有元素,有的话返回false,没有的话会直接将当前BatchedImageRequest暂停掉并且返回true,回到ImageContainer的cancelRequest方法,removeContainerAndCancelIfNecessary的返回值会赋给canceled,如果canceled为true的话表示在当前BatchedImageRequest下已经没有ImageContainer请求了,那么第33行直接将其从正在运行的图片请求集合中移除当前请求;对应mCacheKey的请求本身就不存在于正在运行的图片请求集合mInFlifhtRequests中的话,则会进入35行的else子句到mBatchedResponses从查看是否存在对应于mCacheKey的BatchedImageRequest对象存在,mBatchedResponses也是一个HashMap<String, BatchedImageRequest>类型的对象,和mInFlifhtRequests不同的是他存储的是当前被挂起的BatchedImageRequest请求,当然一个BatchedImageRequest请求下面有多个ImageContainer请求,如果在挂起的请求中存在当前mCacheKey对应的BatchedImageRequest请求的话,则进入第38行的if语句块,接下来的代码和上面一样啦;
前面分析的时候用到了BatchedImageRequest,那么我们有必要来看看BatchedImageRequest到底是什么了:
private class BatchedImageRequest { private final Request<?> mRequest; //返回的图片 private Bitmap mResponseBitmap; //返回的出错信息 private VolleyError mError; //对于同一个url的request请求的有效ImageContainer列表 private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>(); /** * Constructs a new BatchedImageRequest object * @param request The request being tracked * @param container The ImageContainer of the person who initiated the request. */ public BatchedImageRequest(Request<?> request, ImageContainer container) { mRequest = request; mContainers.add(container); } public void setError(VolleyError error) { mError = error; } public VolleyError getError() { return mError; } public void addContainer(ImageContainer container) { mContainers.add(container); } /** * Detatches the bitmap container from the request and cancels the request if no one is * left listening. * @param container The container to remove from the list * @return True if the request was canceled, false otherwise. */ public boolean removeContainerAndCancelIfNecessary(ImageContainer container) { mContainers.remove(container); if (mContainers.size() == 0) { mRequest.cancel(); return true; } return false; } }BatchedImageRequest其实就是对网络请求的封装,跟ImageContainer不同的是,ImageContainer是对数据的封装,BatchedImageRequest是对请求的封装,假如我们有三个请求,但是他们请求同一地址的图片的话,对应的会产生三个ImageContainer对象,但是BatchedImageRequest对象却只有一个的,也即一个BatchedImageRequest请求可以被多个ImageContainer,这点做的目的是为了减少对同一url图片的请求,达到了过滤掉重复url的目的,这里面的removeContainerAndCancelIfNecessary在之前已经分析过了,而addContainer就是向当前BatchedImageRequest请求的mContainers集合中添加ImageContainer对象;
接下来,我们回到ImageLoader的get方法里面继续分析,第4行会查看是否是在主线程中运行的,第6行通过getCacheKey方法生成包含有requestUrl、maxWidth、maxHeight三个内容的对象,接着在第9行通过刚刚生成的cacheKey来查看我们的一级缓存中是否有对应于cacheKey的Bitmap存在,这里的一级缓存就是我们在创建ImageLoader对象的时候传递进来的实现了ImageCache接口的对象,一般使用LruCache,在第10行判断获得的cacheBitmap是否存在,如果存在的话进入if语句块,通过ImageContainer封装一个ImageContainer对象,这个对象第一个参数携带有刚刚从缓存中取出的Bitmap对象以及请求的url地址,接着调用ImageListener对象的onResponse方法,而这个对象是通过getImageListener创建的,那么相应的onResponse方法也应该调用的getImageListener里面的,源码如下:
@Override public void onResponse(ImageContainer response, boolean isImmediate) { if (response.getBitmap() != null) { view.setImageBitmap(response.getBitmap()); } else if (defaultImageResId != 0) { view.setImageResource(defaultImageResId); } }可以看出这种情况下我们调用的将是view.setImageBitmap(response.getBitmap());这一句,将从缓存获取到的图片显示在了ImageView控件上,回到get方法中,当onResponse执行结束后,执行14行return语句返回;如果一级缓存中不存在对应cacheKey的Bitmap,则接着执行18行创建一个默认的ImageContainer对象,注意这个ImageContainer对象的Bitmap参数值等于null,并且同样执行ImageListener的onRespose方法,同样走到getImageListener的onRespose方法里面,此时执行的将是view.setImageResource(defaultImageResId);这句代码,显示一张默认的图片到ImageView上面;执行到25行会到当前正在执行的图片请求集合中对应于cacheKey的请求,26行进行判断如果当前正在执行的图片请求集合中存在此cacheKey的请求,那么只会将当前ImageLoader对象加入到当前请求中,这样做的目的是过滤掉重复的url,让对于同一url的请求位于一个各自的集合中,执行结束之后在第29行返回;如果一级缓存中不存在并且当前运行的请求中也不存在对应于cacheKey请求的话,那么这时候只能创建新的Request请求出来了,也就是第34行创建ImageRequest的代码了,ImageRequest会传入6个参数,第一个是我们请求的url地址,第2个是请求成功的回调接口,第3个是我们所要显示的图片的最大宽度,如果图片宽度大于这个值,则会进行压缩,设置为0表示按照图片的实际大小显示,第4个参数是显示图片的最大高度,第5个参数是图片的解码编码,在这里使用的是Config.RGB_565,也就是每个像素点会占用2个字节,有时我们也会用ARGB_8888编码,这种解码图片方法每个像素点占用4个字节,第6个参数是请求失败所要回调的接口;有了ImageRequest对象之后会在第48行通过RequestQueue的add方法将其添加到请求队列中去,这里的源码在上一篇博客已经分析过啦,同时在第49行将当前新生成的请求加入到了正在运行的图片请求集合中去,防止以后新建请求可能出现的url重复;
那么ImageRequest中请求成功和请求失败的方法是怎么回调的呢?别急,马上来分析,get方法的第38行的onGetImageSuccess方法就是请求成功的回调方法,那么我们应该来看看这个方法里面做些什么事了:
protected void onGetImageSuccess(String cacheKey, Bitmap response) { // cache the image that was fetched. mCache.putBitmap(cacheKey, response); // remove the request from the list of in-flight requests. BatchedImageRequest request = mInFlightRequests.remove(cacheKey); if (request != null) { // Update the response bitmap. request.mResponseBitmap = response; // Send the batched response batchResponse(cacheKey, request); } }第3行会首先将我们请求成功的图片添加到一级缓存中,同时在第6行从运行的请求集合中移出当前cacheKey对应的请求,如果移出的请求不为空的话,则更新BatchedImageRequest的mResponseBitmap属性,使其始终是最新的图片,接着执行batchResponse方法,这个方法是用来分发同一时间被阻塞的相同的ImageRequest对应的ImageContainer:
private void batchResponse(String cacheKey, BatchedImageRequest request) { mBatchedResponses.put(cacheKey, request); // If we don't already have a batch delivery runnable in flight, make a new one. // Note that this will be used to deliver responses to all callers in mBatchedResponses. if (mRunnable == null) { mRunnable = new Runnable() { @Override public void run() { for (BatchedImageRequest bir : mBatchedResponses.values()) { for (ImageContainer container : bir.mContainers) { // If one of the callers in the batched request canceled the request // after the response was received but before it was delivered, // skip them. 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, mBatchResponseDelayMs); } }首先将BatchedImageRequest添加到待处理的BatchedImageRequest的hashmap中,也即mBatchedResponses,接着开启线程遍历mBatchedResponses,依次获得其中的BatchedImageRequest对象,接着遍历BatchedImageRequest,获得其中的ImageContainer对象,如果请求成功的话执行ImageContainer中的mListener属性的onResponse方法,这个方法同样也是之前getImageListener的onRespose,将图片显示在ImageView上面;如果请求失败的话,会执行ImageContainer中的mListener属性的onErrorResponse方法,这个方法就是调用之前getImageListener方法中的onErrorResponse:
@Override public void onErrorResponse(VolleyError error) { if (errorImageResId != 0) { view.setImageResource(errorImageResId); } }这个方法会在ImageView上面显示加载失败之后的默认图片,最后通过Handler的post方法发送消息,至于这里使用postDelayed的原因我在想可能是为了等待UI线程中的图片显示完成,即保证多个post发送消息顺序,减少主线程的压力;
至此,使用Volley来的ImageLoader加载图片的源码分析完毕了;
下面我们做一下总结:
(1)使用ImageLoader加载图片,在其内部的话会用到ImageContainer和BatchedImageRequest这两个类,其中BatchedImageRequest主要用于对请求进行封装,ImageContainer主要用于对数据进行封装;
(2)一个BatchedImageRequest请求可能对应于多个ImageContainer数组,因此BatchedImageRequest里面有一个ImageContainer类型的LinkedList,这样做的话很大程度上过滤掉了相同的url;
(3)ImageLoader中存在着两个缓存请求的HashMap,分别是正在运行的请求缓存已经待处理的请求缓存,当正在运行的请求执行结束之后会进行分发操作来执行待处理的请求;
以上就是我个人对ImageLoader源码的分析,希望大家能够就不对的地方批评指正!!!!!