Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android網絡通信Volley框架源碼淺析(三)

Android網絡通信Volley框架源碼淺析(三)

編輯:關於Android編程

 

通過前面淺析(一)和淺析(二)的分析,相信大家對於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 mInFlightRequests =
            new HashMap();

    /** 用於存放具有相同Key,並且返回了數據的請求*/
    private final HashMap mBatchedResponses =
            new HashMap();

    /** 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 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);
                }
            }
        };
    }

   
    /**
     * 判斷圖片是否已經緩存,不同尺寸的圖片的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() {
                @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);
        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 mContainers = new LinkedList();

        /**
         * 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 onErrorResponse(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的使用.....

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved