Android Volley 原始碼解析(三),圖片載入的實現

developerHaoz發表於2018-02-24

前言

在上一篇文章中,我們一起深入探究了 Volley 的快取機制,通過原始碼分析對快取的工作原理進行了瞭解,這篇文章將帶大家一起探究「Volley 圖片載入的實現」,圖片載入跟快取還是有比較緊密的聯絡的,建議大家先去看下:Android Volley 原始碼解析(二),探究快取機制

這是 Volley 原始碼解析系列的最後一篇文章,今天我們通過以基本用法和原始碼分析相結合的方式來進行,當然本文的原始碼還是建立在第一篇原始碼分析的基礎上的,還沒有看過這篇文章的朋友,建議先去閱讀:Android Volley 原始碼解析(一),網路請求的執行流程

一、圖片載入的基本用法


在進行原始碼解析之前,我們先來看一下 Volley 中有關圖片載入的基本用法。

1.1 ImageRequest 的用法

ImageRequest 和 StringRequest 以及 JsonRequest 都是繼承自 Request,因此他們的用法也基本是相同的,首先需要獲取一個 RequestQueue 物件:

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

接著 new 出一個 ImageRequest 物件:

   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 接收六個引數:

1、圖片的 URL 地址

2、圖片請求成功的回撥,這裡我們將返回的 Bitmap 設定到 ImageView 中

3、4 分別用於指定允許圖片最大的寬度和高度,如果指定的網路圖片的寬度或高度大於這裡的值,就會對圖片進行壓縮,指定為 0 的話,表示不管圖片有多大,都不進行壓縮

5、指定圖片的屬性,Bitmap.Config 下的幾個常量都可以使用,其中 ARGB_8888 可以展示最好的顏色屬性,每個圖片畫素畫素佔 4 個位元組,RGB_565 表示每個圖片畫素佔 2 個位元組

6、圖片請求失敗的回撥

最後將這個 ImageRequest 新增到 RequestQueue 就行了

mQueue.add(imageRequest);
複製程式碼

1.2 ImageLoader 的用法

ImageLoader 其實是對 ImageRequest 的封裝,它不僅可以幫我們對圖片進行快取,還可以過濾掉重複的連結,避免重複傳送請求,因此 ImageLoader 要比 ImageRequest 更加高效。

ImageLoader 的用法,主要分為以下四步:

1、建立 RequestQueue 物件 2、建立一個 ImageLoader 物件 3、獲取一個 ImageListener 物件 4、呼叫 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 步:

1、建立一個 RequestQueue 物件 2、建立一個 ImageLoader 物件 3、在程式碼中獲取 NetworkImageView 的例項 4、設定要載入的圖片地址

如下所示:

   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);
複製程式碼

二、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));
   }
複製程式碼

三、ImageLoader 原始碼解析


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

1、建立 RequestQueue 物件 2、建立一個 ImageLoader 物件 3、獲取一個 ImageListener 物件 4、呼叫 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);
        }
    }
複製程式碼

四、NetworkImageView 原始碼解析


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

1、建立一個 RequestQueue 物件 2、建立一個 ImageLoader 物件 3、在程式碼中獲取 NetworkImageView 的例項 4、呼叫 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 原始碼的閱讀,到之後的程式碼整理以及現在的文章輸出,花了我差不多一個星期的時間,不過對於網路載入和圖片載入有了更深的理解。能完整看到這裡的都是真愛啊,謝謝大家了。


相關文章

相關文章