首页 > 代码库 > Android网络通信Volley框架源码浅析(三)

Android网络通信Volley框架源码浅析(三)

尊重原创 http://write.blog.csdn.net/postedit/26002961


通过前面浅析(一)和浅析(二)的分析,相信大家对于Volley有了初步的认识,但是如果想更深入的理解,还需要靠大家多多看源码。

这篇文章中我们主要来研究一下使用Volley框架请求大量图片的原理,在Android的应用中,通过http请求获取的数据主要有三类:

1、json 
2、xml
3、Image

其中json和xml的获取其实原理很简单,使用Volley获取感觉有点大财小用了,了解Volley获取图片的原理才是比较有意义的,因为里面涉及到很多知识点,比如获取大量图片如何防止OOM。


那么我们就开始研究源码吧。

(1) ImageLoader.java
通过它的名字我们就知道是用来加载Image的工具类


/**
通过调用ImageLoader的get方法就可以获取到图片,然后通过一个Listener回调,将图片设置到ImgeView中(这个方法务必在主线程中调用)
 */
public class ImageLoader {
    /** 前面已经接触过,请求队列(其实不是真实的队列,里面包含了本地队列和网络队列) */
    private final RequestQueue mRequestQueue;

   

    /** 图片缓冲,这个缓存不是前面提到的磁盘缓存,这个是内存缓存,我们可以通过LruCache实现这个接口 */
    private final ImageCache mCache;

    /**
     * 用于存放具有相同cacheKey的请求
     */
    private final HashMap<String, BatchedImageRequest> mInFlightRequests =
            new HashMap<String, BatchedImageRequest>();

    /** 用于存放具有相同Key,并且返回了数据的请求*/
    private final HashMap<String, BatchedImageRequest> mBatchedResponses =
            new HashMap<String, BatchedImageRequest>();

    /** Handler to the main thread. */
    private final Handler mHandler = new Handler(Looper.getMainLooper());

    /** Runnable for in-flight response delivery. */
    private Runnable mRunnable;

    /**
     * Simple cache adapter interface. If provided to the ImageLoader, it
     * will be used as an L1 cache before dispatch to Volley. Implementations
     * must not block. Implementation with an LruCache is recommended.
     */
    public interface ImageCache {
        public Bitmap getBitmap(String url);
        public void putBitmap(String url, Bitmap bitmap);
    }

    /**
     * 构造函数需要传入一个RequestQueue对象和一个内存缓存对象
     * @param queue The RequestQueue to use for making image requests.
     * @param imageCache The cache to use as an L1 cache.
     */
    public ImageLoader(RequestQueue queue, ImageCache imageCache) {
        mRequestQueue = queue;
        mCache = imageCache;
    }

    /**
     * 用于图片获取成功或者失败的回调
     * @param imageView 需要设置图片的ImageView.
     * @param defaultImageResId 默认显示图片.
     * @param errorImageResId 出错时显示的图片.
     */
    public static ImageListener getImageListener(final ImageView view,
            final int defaultImageResId, final int errorImageResId) {
        return new ImageListener() {
            @Override
            public void one rrorResponse(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);
                }
            }
        };
    }

   
    /**
     * 判断图片是否已经缓存,不同尺寸的图片的cacheKey是不一样的
     * @param requestUrl 图片的url
     * @param maxWidth 请求图片的宽度.
     * @param maxHeight 请求图片的高度.
     * @return 返回true则缓存.
     */
    public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
        throwIfNotOnMainThread();

        String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
        return mCache.getBitmap(cacheKey) != null;
    }

    /**
     * 这个方法时个核心方法,我们主要通过它来获取图片
     *
     * @param requestUrl The URL of the image to be loaded.
     * @param defaultImage Optional default image to return until the actual image is loaded.
     */
    public ImageContainer get(String requestUrl, final ImageListener listener) {
        return get(requestUrl, listener, 0, 0);
    }

    /**
     * 这个方法比上面方法多了两个参数,如果传入则图片大小会做相应处理,如果不传默认为0,图片大小不做处理
     * @param requestUrl The url of the remote image
     * @param imageListener The listener to call when the remote image is loaded
     * @param maxWidth The maximum width of the returned image.
     * @param maxHeight The maximum height of the returned image.
     * @return A container object that contains all of the properties of the request, as well as
     *     the currently available image (default if remote is not loaded).
     */
    public ImageContainer get(String requestUrl, ImageListener imageListener,
            int maxWidth, int maxHeight) {
        // only fulfill requests that were initiated from the main thread.
        throwIfNotOnMainThread();
		//获取key,其实就是url,width,height按照某种格式拼接
        final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);

        // 首先从缓存里面取图片
        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
        if (cachedBitmap != null) {
            // 如果缓存命中,则直接放回
            ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
            imageListener.onResponse(container, true);
            return container;
        }

        // 没有命中,则创建一个ImageContainer,注意此时图片数据传入的null,
        ImageContainer imageContainer =
                new ImageContainer(null, requestUrl, cacheKey, imageListener);

        // 这就是为什么在onResponse中我们需要判断图片数据是否为空,此时就是为空的
        imageListener.onResponse(imageContainer, true);

        // 判断同一个key的请求是否已经存在
        BatchedImageRequest request = mInFlightRequests.get(cacheKey);
        if (request != null) {
            // 如果存在,则直接加入request中,没有必要对一个key发送多个请求
            request.addContainer(imageContainer);
            return imageContainer;
        }

        // 发送一个请求,并加入RequestQueue
        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 one rrorResponse(VolleyError error) {
                    onGetImageError(cacheKey, error);
                }
            });
        mRequestQueue.add(newRequest);
        VolleyLog.e("-------------->"+newRequest.getSequence());
		//加入到HashMap中,表明这个key已经存在一个请求
        mInFlightRequests.put(cacheKey,
                new BatchedImageRequest(newRequest, imageContainer));
        return imageContainer;
    }

   

    /**
     * Handler for when an image was successfully loaded.
     * @param cacheKey The cache key that is associated with the image request.
     * @param response The bitmap that was returned from the network.
     */
    private void onGetImageSuccess(String cacheKey, Bitmap response) {
        // 获取图片成功,放入缓存
        mCache.putBitmap(cacheKey, response);

        // 将cacheKey对应的请求从mInFlightRequests中移除
        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);

        if (request != null) {
            // Update the response bitmap.
            request.mResponseBitmap = response;

            // Send the batched response
            batchResponse(cacheKey, request);
        }
    }

    /**
     * Handler for when an image failed to load.
     * @param cacheKey The cache key that is associated with the image request.
     */
    private void onGetImageError(String cacheKey, VolleyError error) {
        // Notify the requesters that something failed via a null result.
        // Remove this request from the list of in-flight requests.
        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);

        if (request != null) {
            // Set the error for this request
            request.setError(error);

            // Send the batched response
            batchResponse(cacheKey, request);
        }
    }

    /**
     * Container object for all of the data surrounding an image request.
     */
    public class ImageContainer {
        /**
         * 保存从网络获取的图片
         */
        private Bitmap mBitmap;

        private final ImageListener mListener;

        /** The cache key that was associated with the request */
        private final String mCacheKey;

        /** The request URL that was specified */
        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;
            }
			//判断此key对应的请求有没有
            BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
            if (request != null) {
				/**如果存在,request中mContainers中的这个Container,如果mContainers的size为0,那么
					removeContainerAndCancelIfNecessary返回true
				*/
                boolean canceled = request.removeContainerAndCancelIfNecessary(this);
                if (canceled) {
					//如果返回true,那么说明没有任何一个ImageView对这个请求感兴趣,需要移除它
                    mInFlightRequests.remove(mCacheKey);
                }
            } else {
                // 判断是否这个request已经成功返回了
                request = mBatchedResponses.get(mCacheKey);
                if (request != null) {
                    request.removeContainerAndCancelIfNecessary(this);
                    if (request.mContainers.size() == 0) {
						//如果已经成功返回,并且没有ImageView对他感兴趣,那么删除它
                        mBatchedResponses.remove(mCacheKey);
                    }
                }
            }
        }

        /**
         * Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
         */
        public Bitmap getBitmap() {
            return mBitmap;
        }

        /**
         * Returns the requested URL for this container.
         */
        public String getRequestUrl() {
            return mRequestUrl;
        }
    }

    /**
     * 对Request的一个包装,将所有有共同key的请求放入一个LinkedList中
     */
    private class BatchedImageRequest {
        /** The request being tracked */
        private final Request<?> mRequest;

        /** The result of the request being tracked by this item */
        private Bitmap mResponseBitmap;

        /** Error if one occurred for this response */
        private VolleyError mError;

        /** 存放具有共同key的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);
        }

        /**
         * Set the error for this response
         */
        public void setError(VolleyError error) {
            mError = error;
        }

        /**
         * Get the error for this response
         */
        public VolleyError getError() {
            return mError;
        }

        /**
         * Adds another ImageContainer to the list of those interested in the results of
         * the request.
         */
        public void addContainer(ImageContainer container) {
            mContainers.add(container);
        }

        /**
         * 移除一个ImageContainer,如果此时size==0,那么需要从mInFlightRequests中移除该BatchedImageRequest
         * @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放入到mBatchedResponses,然后将结果发送给所有具有相同key的ImageContainer,ImageContainer通过里面的Listener发送到ImageView,从而显示出来
     * @param cacheKey The cacheKey of the response being delivered.
     * @param request The BatchedImageRequest to be delivered.
     * @param error The volley error associated with the request (if applicable).
     */
    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);
        }
    }

  
    /**
     * 获取一个请求的key,拼接规则就是使用#讲几个连接起来
     * @param url The URL of the request.
     * @param maxWidth The max-width of the output.
     * @param maxHeight The max-height of the output.
     */
    private static String getCacheKey(String url, int maxWidth, int maxHeight) {
        return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
                .append("#H").append(maxHeight).append(url).toString();
    }
    
    
}


ImageLoader的代码还是比较复杂的,但是思路还是比较清晰的,总结如下:
1、通过ImageLoader的get方法获取图片,如果我们只想获取原始图片,不用关心大小,则只用传入url和Listener,如果需要设置图片大小,那么传入你需要设置大大小
2、get方法中,先回去缓存中查找,如果命中,那么就直接放回,如果没有命中,那么就判断mInFlightRequests中是否有相同key的BatchedImageRequest,如果有则直接将ImageConainer加入BatchedImageRequest的mContainres中,因为对于同一个key没有必要发送两次请求
3、如果在mInFlightRequest中没有此key,那么需要创建一个ImageRequest对象,并加入RequestQueue中,并使用ImageRequest创建一个BatchedImageRequest加入mInFlightRequest
4、当请求返回后,将BatchedImageRequest从mInFlightRequest中移除,加入mBatchedResponses中,将返回结果返回给所有的ImageContainer
5、如果一个ImageContainer在收到返回结果之前就被cancel掉,那么需要将它从mInFlightRequest的mContainers中移除,如果移除后mContainers的size为0,说明这个请求只有一次,取消了就没有必要请求,需要把BatchedImageRequestmInFlightRequest中移走,从如果不等于0,说明这个请求被其他的ImageContainr需要,不能取消


如果我们仅仅是获取少量图片,Volley框架为我们提供了一个NetworkImageView,这个类继承自ImageView,使用时,我们只需要调用setImageUrl即可,下面就来看其实现机制
(2) NetworkImageView.java


public class NetworkImageView extends ImageView {
    /** 需要加载图片的url */
    private String mUrl;

    /**
     * 默认显示图片的id
     */
    private int mDefaultImageId;

    /**
     * 错误图片的id
     */
    private int mErrorImageId;

    /** ImageLoader对象,其实就是用该对象去获取图片,所以了解了ImageLoader后,这个类很好理解 */
    private ImageLoader mImageLoader;

    /**把这个对象当成url和Listener的封装即可 */
    private ImageContainer mImageContainer;

    public NetworkImageView(Context context) {
        this(context, null);
    }

    public NetworkImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NetworkImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    /**
     * 设置Url
     *
     * @param url The URL that should be loaded into this ImageView.
     * @param imageLoader ImageLoader that will be used to make the request.
     */
    public void setImageUrl(String url, ImageLoader imageLoader) {
        mUrl = url;
        mImageLoader = imageLoader;
        // 这个方法我们后面分析
        loadImageIfNecessary(false);
    }

    /**
     * Sets the default image resource ID to be used for this view until the attempt to load it
     * completes.
     */
    public void setDefaultImageResId(int defaultImage) {
        mDefaultImageId = defaultImage;
    }

    /**
     * Sets the error image resource ID to be used for this view in the event that the image
     * requested fails to load.
     */
    public void setErrorImageResId(int errorImage) {
        mErrorImageId = errorImage;
    }

    /**
     * 这个方法在onLayout方法中传入true,其他地方传入false
     * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise.
     */
    void loadImageIfNecessary(final boolean isInLayoutPass) {
        int width = getWidth();
        int height = getHeight();

        boolean wrapWidth = false, wrapHeight = false;
        if (getLayoutParams() != null) {
            wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT;
            wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT;
        }

        // if the view‘s bounds aren‘t known yet, and this is not a wrap-content/wrap-content
        // view, hold off on loading the image.
        boolean isFullyWrapContent = wrapWidth && wrapHeight;
        if (width == 0 && height == 0 && !isFullyWrapContent) {
            return;
        }

        // if the URL to be loaded in this view is empty, cancel any old requests and clear the
        // currently loaded image.
        if (TextUtils.isEmpty(mUrl)) {
            if (mImageContainer != null) {
                mImageContainer.cancelRequest();
                mImageContainer = null;
            }
            setDefaultImageOrNull();
            return;
        }

        // if there was an old request in this view, check if it needs to be canceled.
        if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
            if (mImageContainer.getRequestUrl().equals(mUrl)) {
                //如果请求url相同,则直接return
                return;
            } else {
                // 请求url不同,则cancel,并显示默认图片或者不显示图片
                mImageContainer.cancelRequest();
                setDefaultImageOrNull();
            }
        }

        // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens.
        int maxWidth = wrapWidth ? 0 : width;
        int maxHeight = wrapHeight ? 0 : height;

		//调用了get方法
        ImageContainer newContainer = mImageLoader.get(mUrl,
                new ImageListener() {
                    @Override
                    public void one rrorResponse(VolleyError error) {
                        if (mErrorImageId != 0) {
                            setImageResource(mErrorImageId);
                        }
                    }

                    @Override
                    public void onResponse(final ImageContainer response, boolean isImmediate) {
                        // If this was an immediate response that was delivered inside of a layout
                        // pass do not set the image immediately as it will trigger a requestLayout
                        // inside of a layout. Instead, defer setting the image by posting back to
                        // the main thread.
                        if (isImmediate && isInLayoutPass) {
                            post(new Runnable() {
                                @Override
                                public void run() {
                                    onResponse(response, false);
                                }
                            });
                            return;
                        }

                        if (response.getBitmap() != null) {
                            setImageBitmap(response.getBitmap());
                        } else if (mDefaultImageId != 0) {
                            setImageResource(mDefaultImageId);
                        }
                    }
                }, maxWidth, maxHeight);

        // update the ImageContainer to be the new bitmap container.
        mImageContainer = newContainer;
    }

    private void setDefaultImageOrNull() {
        if(mDefaultImageId != 0) {
            setImageResource(mDefaultImageId);
        }
        else {
            setImageBitmap(null);
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        loadImageIfNecessary(true);
    }

    @Override
    protected void onDetachedFromWindow() {
        if (mImageContainer != null) {
            // If the view was bound to an image request, cancel it and clear
            // out the image from the view.
            mImageContainer.cancelRequest();
            setImageBitmap(null);
            // also clear out the container so we can reload the image if necessary.
            mImageContainer = null;
        }
        super.onDetachedFromWindow();
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        invalidate();
    }
}

到目前为止Volley框架的源码分析差不多了,下一篇文章我打算使用一个GridView展示大量图片的例子来讲解Volley的使用.....