Android Volley 源碼解析(三),圖片加載的實現

前言

在上一篇文章中,咱們一塊兒深刻探究了 Volley 的緩存機制,經過源碼分析對緩存的工做原理進行了瞭解,這篇文章將帶你們一塊兒探究「Volley 圖片加載的實現」,圖片加載跟緩存仍是有比較緊密的聯繫的,建議你們先去看下:Android Volley 源碼解析(二),探究緩存機制緩存

這是 Volley 源碼解析系列的最後一篇文章,今天咱們經過以基本用法和源碼分析相結合的方式來進行,固然本文的源碼仍是創建在第一篇源碼分析的基礎上的,尚未看過這篇文章的朋友,建議先去閱讀:Android Volley 源碼解析(一),網絡請求的執行流程bash

1、圖片加載的基本用法


在進行源碼解析以前,咱們先來看一下 Volley 中有關圖片加載的基本用法。服務器

1.1 ImageRequest 的用法

ImageRequest 和 StringRequest 以及 JsonRequest 都是繼承自 Request,所以他們的用法也基本是相同的,首先須要獲取一個 RequestQueue 對象:網絡

RequestQueue mQueue = Volley.newRequestQueue(context);  
複製代碼

接着 new 出一個 ImageRequest 對象:ide

private static final String URL = "http://ww4.sinaimg.cn/large/610dc034gw1euxdmjl7j7j20r2180wts.jpg";

   ImageRequest imageRequest = new ImageRequest(URL, new Response.Listener<Bitmap>() {
       @Override
       public void onResponse(Bitmap response) {
           imageView.setImageBitmap(response);
       }
   }, 0, 0, ImageView.ScaleType.CENTER_CROP, Bitmap.Config.RGB_565, new Response.ErrorListener() {
       @Override
       public void onErrorResponse(VolleyError error) {

       }
   });
複製代碼

能夠看到 ImageRequest 接收六個參數:函數

一、圖片的 URL 地址oop

二、圖片請求成功的回調,這裏咱們將返回的 Bitmap 設置到 ImageView 中源碼分析

三、4 分別用於指定容許圖片最大的寬度和高度,若是指定的網絡圖片的寬度或高度大於這裏的值,就會對圖片進行壓縮,指定爲 0 的話,表示無論圖片有多大,都不進行壓縮post

五、指定圖片的屬性,Bitmap.Config 下的幾個常量均可以使用,其中 ARGB_8888 能夠展現最好的顏色屬性,每一個圖片像素像素佔 4 個字節,RGB_565 表示每一個圖片像素佔 2 個字節ui

六、圖片請求失敗的回調

最後將這個 ImageRequest 添加到 RequestQueue 就好了

mQueue.add(imageRequest);
複製代碼

1.2 ImageLoader 的用法

ImageLoader 實際上是對 ImageRequest 的封裝,它不只能夠幫咱們對圖片進行緩存,還能夠過濾掉重複的連接,避免重複發送請求,所以 ImageLoader 要比 ImageRequest 更加高效。

ImageLoader 的用法,主要分爲如下四步:

一、建立 RequestQueue 對象 二、建立一個 ImageLoader 對象 三、獲取一個 ImageListener 對象 四、調用 ImageLoader 的 get() 方法記載圖片

RequestQueue requestQueue = Volley.newRequestQueue(this);
   ImageLoader imageLoader = new ImageLoader(requestQueue, new ImageLoader.ImageCache() {
       @Override
       public Bitmap getBitmap(String url) {
           return null;
       }

       @Override
       public void putBitmap(String url, Bitmap bitmap) {

       }
   });
   ImageLoader.ImageListener listener = ImageLoader.getImageListener(mIvShow, R.mipmap.ic_launcher, R.mipmap.ic_launcher_round);
   imageLoader.get(URL, listener);
複製代碼

能夠看到 ImageLoader 的構造函數接收兩個參數,第一個參數就是 RequestQueue 對象,第二個參數是 ImageCache,咱們這裏直接 new 出一個空的 ImageCache 實現就好了。

在 ImageListener 中傳入所加載圖片的 URL,以及圖片佔位符和加載失敗後顯示的圖片,最後調用 ImageLoader.get() 方法便能進行圖片的加載。

1.3 NetworkImageView

除了以上兩種方式以外,Volley 還提供了第三種方式來加載網絡圖片,NetworkImageView 是一個繼承自 ImageView 的自定義 View,在 ImageView 的基礎上拓展加載網絡圖片的功能。NetworkImageView 的用法仍是比較簡單的。大體能夠分爲 4 步:

一、建立一個 RequestQueue 對象 二、建立一個 ImageLoader 對象 三、在代碼中獲取 NetworkImageView 的實例 四、設置要加載的圖片地址

以下所示:

RequestQueue requestQueue = Volley.newRequestQueue(this);
   ImageLoader imageLoader = new ImageLoader(requestQueue, new ImageLoader.ImageCache() {
       @Override
       public Bitmap getBitmap(String url) {
           return null;
       }

       @Override
       public void putBitmap(String url, Bitmap bitmap) {

       }
    });
   networkImageView.setImageUrl(URL, imageLoader);
複製代碼

2、ImageRequest 源碼解析


在上一節中介紹了 Volley 圖片加載的三種方法,從這節開始咱們結合源碼來分析 Volley 中圖片加載的實現,就從 ImageRequest 開始吧。

咱們在 Android Volley 源碼解析(一),網絡請求的執行流程 這篇文章中講到,網絡請求最終會將從服務器返回的結果封裝成 NetworkResponse 而後傳給 Request 進行處理。而 ImageRequest 的工做,其實就是將 NetworkResponse 解析成包含 Bitmap 的 Response,最後再回調出去。

咱們要進行分析的,也就是這個過程。

能夠看到 parseNetworkResponse 中只有一個 doParse() 方法

@Override
    protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
        synchronized (sDecodeLock) {
            try {
                return doParse(response);
            } catch (OutOfMemoryError e) {
                return Response.error(new ParseError(e));
            }
        }
    }
複製代碼

就讓咱們看看 doParse() 裏面究竟進行了什麼操做

private Response<Bitmap> doParse(NetworkResponse response) {
        byte[] data = response.data;
        BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
        Bitmap bitmap = null;
        if (mMaxWidth == 0 && mMaxHeight == 0) {
            decodeOptions.inPreferredConfig = mDecodeConfig;
            bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
        } else {
            // ① 獲取 Bitmap 原始的寬和高
            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 desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
                    actualHeight, actualWidth, mScaleType);

            // ③ 根據咱們想要的寬和高獲得對應的 Bitmap
            decodeOptions.inJustDecodeBounds = false;
            decodeOptions.inSampleSize =
                findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
            Bitmap tempBitmap =
                BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

            // ④ 若是 Bitmap 不爲 bull 並且寬或高大於目標寬高的話,再一次壓縮
            if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
                    tempBitmap.getHeight() > desiredHeight)) {
                bitmap = Bitmap.createScaledBitmap(tempBitmap,
                        desiredWidth, desiredHeight, true);
                tempBitmap.recycle();
            } else {
                bitmap = tempBitmap;
            }
        }

         // ⑤ 將獲得的 包含 Bitmap 的 Response 回調出去
        if (bitmap == null) {
            return Response.error(new ParseError(response));
        } else {
            return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
        }
    }
複製代碼

代碼比較長,咱們分爲 5 步來看

① 獲取 Bitmap 原始的寬和高

經過 BitmapFactory 將傳入的 NetworkResponse 中的 data 轉換成對應的 Bitmap,而後經過設置 BitmapOptions.inJustDecodeBounds = true,獲得 Bitmap 的原始寬和高,這裏補充一下,當 BitmapOptions.inJustDecodeBounds = true 的時候,BitmapFactory.decode 並不會真的返回一個 bitmap 給你,它僅僅會把一些圖片的大小信息(如寬和高)返回給你,而不會佔用太多的內存。

② 計算咱們真正想要的寬和高

應該還記得咱們構建 ImageRequest 的時候傳入的參數吧,那 6 個參數裏面,包含兩個分別指定圖片最大寬和高的參數,咱們將傳入的圖片最大寬和高以及 Bitmap 真實的寬和高,經過 getResizedDemension() 方法計算出比較合適的圖片顯示寬高,代碼以下:

private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
            int actualSecondary, ScaleType scaleType) {

        if ((maxPrimary == 0) && (maxSecondary == 0)) {
            return actualPrimary;
        }

        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 == ScaleType.CENTER_CROP) {
            if ((resized * ratio) < maxSecondary) {
                resized = (int) (maxSecondary / ratio);
            }
            return resized;
        }

        if ((resized * ratio) > maxSecondary) {
            resized = (int) (maxSecondary / ratio);
        }
        return resized;
    }
複製代碼
③ 根據咱們想要的寬和高獲得對應的 Bitmap

DecodeOptions.inJustDecodeBounds = true 表明將一個真正的 Bitmap 返回給你, DecodeOptions.inSampleSize 表明圖片的採樣率,是跟圖片壓縮有關的參數,若是 inSampliSize = 2 則表明將原先圖片的寬和高分別減少爲原來的 1/2,以此類推。

decodeOptions.inJustDecodeBounds = false;
    decodeOptions.inSampleSize =
        findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
    Bitmap tempBitmap =
        BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
複製代碼
// 計算採樣率的方法
    static int findBestSampleSize(
            int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
        double wr = (double) actualWidth / desiredWidth;
        double hr = (double) actualHeight / desiredHeight;
        double ratio = Math.min(wr, hr);
        float n = 1.0f;
        while ((n * 2) <= ratio) {
            n *= 2;
        }
        return (int) n;
    }
複製代碼
④ 若是 Bitmap 不爲 bull 並且寬或高大於目標寬高的話,再一次壓縮
if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
            tempBitmap.getHeight() > desiredHeight)) {
        bitmap = Bitmap.createScaledBitmap(tempBitmap,
                desiredWidth, desiredHeight, true);
        tempBitmap.recycle();
   } else {
        bitmap = tempBitmap;
   }
複製代碼
⑤ 將獲得的包含 Bitmap 的 Response 回調出去
if (bitmap == null) {
       return Response.error(new ParseError(response));
   } else {
       return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
   }
複製代碼

3、ImageLoader 源碼解析


咱們在上面說到 ImageLoader 的用法,主要分爲四步:

一、建立 RequestQueue 對象 二、建立一個 ImageLoader 對象 三、獲取一個 ImageListener 對象 四、調用 ImageLoader 的 get() 方法加載圖片

那咱們就從它的用法入手,一步一步分析到底是怎麼實現的。

建立 RequestQueue 在以前已經講過,能夠參考這篇文章:Android Volley 源碼解析(一),網絡請求的執行流程,咱們看下 ImageLoader 的構造方法:

public ImageLoader(RequestQueue queue, ImageCache imageCache) {
        mRequestQueue = queue;
        mCache = imageCache;
    }
複製代碼

能夠看到構造方法將 RequestQueue 和 ImageCache 賦值給當前實例的成員變量,咱們接着看 ImageListener 獲取,ImageListener 是經過 ImageLoader.getImageListener() 方法獲取的:

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);
                }
            }
        };
    }
複製代碼

能夠看到在這裏面主要是將回調出來的 Bitmap 設置給對應的 ImageView,以及作一些圖片加載的容錯處理。

最後重點來了,ImageLoader 的 get() 方法是 ImageLoader 類最複雜的方法,也是最核心的方法,咱們一塊兒來看看吧:

public ImageContainer get(String requestUrl, ImageListener imageListener,
            int maxWidth, int maxHeight, ScaleType scaleType) {

        // 若是當前不是在主線程就拋出異常(UI 操做必須在主線程進行)
        throwIfNotOnMainThread();

        final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);

        // 從緩存中取出對應的 Bitmap,若是 Bitmap 不爲 null,直接回調 imageListener 將 Bitmap 設置給 ImageView
        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
        if (cachedBitmap != null) {
            ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
            imageListener.onResponse(container, true);
            return container;
        }

        ImageContainer imageContainer =
                new ImageContainer(null, requestUrl, cacheKey, imageListener);

        imageListener.onResponse(imageContainer, true);
 
        // 判斷該請求是不是否在緩存隊列中
        BatchedImageRequest request = mInFlightRequests.get(cacheKey);
        if (request != null) {
            request.addContainer(imageContainer);
            return imageContainer;
        }

        // 若是在緩存中並無找到該請求,便進行一次網絡請求,
        Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
                cacheKey);
        mRequestQueue.add(newRequest);
        // 將請求進行緩存
        mInFlightRequests.put(cacheKey,
                new BatchedImageRequest(newRequest, imageContainer));
        return imageContainer;
    }
複製代碼

首先進行了當前線程的判斷,若是不是主線程的話,就直接拋出錯誤。

private void throwIfNotOnMainThread() {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
        }
    }
複製代碼

而後從緩存中取出對應的 Bitmap,若是 Bitmap 不爲 null,直接回調 ImageListener 將 Bitmap 設置給對應的 ImageView。

Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
   if (cachedBitmap != null) {
       ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
       imageListener.onResponse(container, true);
       return container;
   }
複製代碼

而後根據 Url 從緩存隊列中取出 Request

BatchedImageRequest request = mInFlightRequests.get(cacheKey);   
   if (request != null) {
       request.addContainer(imageContainer);
       return imageContainer;    
   }
複製代碼

若是在緩存中並無找到該請求,便進行一次網絡請求

Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
           cacheKey);
複製代碼

能夠看到 ImageLoader 調用了 makeImageReqeust() 方法來構建 Request,咱們來看看他是怎麼實現的:

protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight,
            ScaleType scaleType, final String cacheKey) {
        return new ImageRequest(requestUrl, new Listener<Bitmap>() {
            @Override
            public void onResponse(Bitmap response) {
                onGetImageSuccess(cacheKey, response);
            }
        }, maxWidth, maxHeight, scaleType, Config.RGB_565, new ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                onGetImageError(cacheKey, error);
            }
        });
    }
複製代碼

網絡請求成功以後,調用 onGetImageSuccess() 方法,將 Bitmap 進行緩存,以及將緩存隊列中 cacheKey 對應的 BatchedImageRequest 移除掉,最後調用 batchResponse() 方法。

protected void onGetImageSuccess(String cacheKey, Bitmap response) {
        mCache.putBitmap(cacheKey, response);

        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);

        if (request != null) {
            request.mResponseBitmap = response;
            batchResponse(cacheKey, request);
        }
    }
複製代碼

在 batchResponse() 方法中,在主線程裏面將 Bitmap 回調給 ImageListner,而後將 Bitmap 設置給 ImageView,這樣便完成了圖片加載的所有過程。

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;
                }

            };
            mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
        }
    }
複製代碼

4、NetworkImageView 源碼解析


NetworkImageView 是一個內部使用 ImageLoader 來進行加載網絡圖片的自定義 View,咱們在上面提到,NetworkImageView 的使用方法主要分爲四步:

一、建立一個 RequestQueue 對象 二、建立一個 ImageLoader 對象 三、在代碼中獲取 NetworkImageView 的實例 四、調用 setImageUrl() 方法來設置要加載的圖片地址

其中最後一步是 NetworkImageView 的核心,咱們來看看 setImageUrl() 內部是怎麼實現的吧:

public void setImageUrl(String url, ImageLoader imageLoader) {
        mUrl = url;
        mImageLoader = imageLoader;
        loadImageIfNecessary(false);
    }
複製代碼

只有簡單的三行代碼,想必主要的邏輯就在 loadImageIfNecessary() 這個方法裏面,咱們點進去看一下:

void loadImageIfNecessary(final boolean isInLayoutPass) {

        // 若是 URL 爲 null,則取消該請求
        if (TextUtils.isEmpty(mUrl)) {
            if (mImageContainer != null) {
                mImageContainer.cancelRequest();
                mImageContainer = null;
            }
            setDefaultImageOrNull();
            return;
        }

        // 若是該 NetworkImageView 以前已經掉用過 setImageUrl(),
        // 判斷當前的 Url 跟以前請求的 URL 是否相同
        if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
            if (mImageContainer.getRequestUrl().equals(mUrl)) {
                return;
            } else {
                mImageContainer.cancelRequest();
                setDefaultImageOrNull();
            }
        }
        
        // 經過 ImageLoader 進行圖片加載
        mImageContainer = 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 (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, scaleType);
    }
複製代碼

代碼仍是相對比較清晰的,先進行一些容錯性的處理,而後調用 ImageLoader 來獲取對應的 bitmap,最後將其設置給 NetworkImageView.

總結

Volley 源碼解析系列,到這裏就所有結束了,這是我寫過最長的系列文章了,從一開始 Volley 源碼的閱讀,到以後的代碼整理以及如今的文章輸出,花了我差很少一個星期的時間,不過對於網絡加載和圖片加載有了更深的理解。能完整看到這裏的都是真愛啊,謝謝你們了。


相關文章

相關文章
相關標籤/搜索