Volley 原始碼解析之圖片請求

哆啦miss_A夢發表於2018-12-29

一、前言

上篇文章我們分析了網路請求,這篇文章分析對圖片的處理操作,如果沒看上一篇,可以先看上一篇文章Volley 原始碼解析之網路請求。Volley 不僅僅對請求網路資料作了良好的封裝,還封裝了對圖片的載入等操作,雖然比不上glidefresco ,不過可以滿足我們的日常使用,從學習者的角度看看是怎麼封裝的。

二、簡單使用

  1. 使用ImageRequest載入圖片,用法跟請求網路的用法差不多,只是構造request的引數不太一樣:
     String imageUrl = "https://pic1.zhimg.com/80/1a60ca062a1fe2f6d091cdd9749e9c68_hd.jpg";
     RequestQueue queue = Volley.newRequestQueue(this);
     ImageRequest imageRequest = new ImageRequest(imageUrl,
                        response -> imageView.setImageBitmap(response),
                        0, 0, ImageView.ScaleType.CENTER_CROP, Bitmap.Config.ARGB_8888,
                        error -> {});
     queue.add(imageRequest);
    複製程式碼
    • 第一個引數是圖片地址沒啥說的
    • 第二個引數是成功的回撥,返回一個bitmap
    • 第三和第四個引數則是圖片的最大的高度和寬度,0為預設圖片大小,如果填寫的圖片最大的高度和寬度小於圖片的實際尺寸則會進行壓縮
    • 第五個值就是對圖片進行邊界縮放
    • 第六個引數是圖片的格式,常用的就是RGB_565ARGB_8888,前者每個畫素佔2個位元組,後者每個畫素佔4個位元組,後者成像質量高,有alpha通道,如果使用的是jpg,不需要alpha通道則可以使用前者; 還有個ARGB_4444,不過已經廢棄了,在4.4以後預設轉成ARGB_8888ALPHA_8只有透明度,沒有顏色值,一般很少使用
    • 最後個引數就是錯誤的回撥
  2. 使用ImageLoader載入圖片
    String imageUrl = "https://pic1.zhimg.com/80/1a60ca062a1fe2f6d091cdd9749e9c68_hd.jpg";
    RequestQueue queue = Volley.newRequestQueue(this);
    ImageLoader imageLoader = new ImageLoader(queue, new BitmapCache());
    ImageLoader.ImageListener imageListener = ImageLoader.getImageListener(imageView, 
         R.mipmap.ic_launcher, R.mipmap.ic_launcher_round);
    imageLoader.get(imageUrl,
         imageListener, 0, 0);
    private class BitmapCache implements ImageLoader.ImageCache{
         private LruCache<String, Bitmap> lruCache;
    
         public BitmapCache() {
             int maxSize = 10 * 1024 * 1024;
             lruCache = new LruCache<String, Bitmap>(maxSize) {
                 @Override
                 protected int sizeOf(String key, Bitmap bitmap) {
                     return bitmap.getRowBytes() * bitmap.getHeight();
                 }
             };
         }
    
         @Override
         public Bitmap getBitmap(String url) {
             return lruCache.get(url);
         }
    
         @Override
         public void putBitmap(String url, Bitmap bitmap) {
             lruCache.put(url, bitmap);
         }
    }
    複製程式碼
  3. 使用NetworkImageView載入圖片
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     tools:context=".MainActivity">
    
     <Button
         android:id="@+id/button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginStart="8dp"
         android:layout_marginTop="88dp"
         android:layout_marginEnd="8dp"
         android:text="Button"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
    
     <com.android.volley.toolbox.NetworkImageView
         android:id="@+id/imageView"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginStart="8dp"
         android:layout_marginTop="32dp"
         android:layout_marginEnd="8dp"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@+id/textView"
         tools:srcCompat="@tools:sample/avatars" />
    </android.support.constraint.ConstraintLayout>
    複製程式碼
     String imageUrl = "https://pic1.zhimg.com/80/1a60ca062a1fe2f6d091cdd9749e9c68_hd.jpg";
     NetworkImageView networkImageView = findViewById(R.id.imageView);
     networkImageView.setDefaultImageResId(R.mipmap.ic_launcher);
     networkImageView.setErrorImageResId(R.mipmap.ic_launcher_round);
     networkImageView.setImageUrl(imageUrl, imageLoader);
    複製程式碼

三、原始碼分析

一、 ImageRequest 分析

首先我們分析ImageRequest,直接分析這個類,程式碼不多,直接繼承Request,那麼不用說跟上一篇我們分析的網路請求的request大體相同,不同的是這個是請求圖片,如果我們需要自定義大小那麼這裡就對圖片進行了裁剪以滿足我們的大小:

public class ImageRequest extends Request<Bitmap> {
    //圖片請求的超時時間,單位毫秒
    public static final int DEFAULT_IMAGE_TIMEOUT_MS = 1000;

    //圖片請求的預設重試次數
    public static final int DEFAULT_IMAGE_MAX_RETRIES = 2;

    //發生衝突時的預設重傳延遲增加數,和TCP協議有關係,退避演算法,短時間的重複請求失敗還會是失敗
    public static final float DEFAULT_IMAGE_BACKOFF_MULT = 2f;

    //對mListener加鎖,保證執行緒安全,避免取消的時候同時執行分發
    private final Object mLock = new Object();

    @GuardedBy("mLock")
    @Nullable
    private Response.Listener<Bitmap> mListener;

    private final Config mDecodeConfig;
    private final int mMaxWidth;
    private final int mMaxHeight;
    private final ScaleType mScaleType;

    //Bitmap 的同步解析鎖,保證一個時間內只有一個Bitmap被載入到記憶體進行解析,避免多個同時解析oom
    private static final Object sDecodeLock = new Object();

   
    public ImageRequest(
            String url,
            Response.Listener<Bitmap> listener,
            int maxWidth,
            int maxHeight,
            ScaleType scaleType,
            Config decodeConfig,
            @Nullable Response.ErrorListener errorListener) {
        super(Method.GET, url, errorListener);
        setRetryPolicy(
                new DefaultRetryPolicy(
                        DEFAULT_IMAGE_TIMEOUT_MS,
                        DEFAULT_IMAGE_MAX_RETRIES,
                        DEFAULT_IMAGE_BACKOFF_MULT));
        mListener = listener;
        mDecodeConfig = decodeConfig;
        mMaxWidth = maxWidth;
        mMaxHeight = maxHeight;
        mScaleType = scaleType;
    }

    @Deprecated
    public ImageRequest(
            String url,
            Response.Listener<Bitmap> listener,
            int maxWidth,
            int maxHeight,
            Config decodeConfig,
            Response.ErrorListener errorListener) {
        this(
                url,
                listener,
                maxWidth,
                maxHeight,
                ScaleType.CENTER_INSIDE,
                decodeConfig,
                errorListener);
    }

    @Override
    public Priority getPriority() {
        return Priority.LOW;
    }

    //根據ScaleType設定圖片大小
    private static int getResizedDimension(
            int maxPrimary,
            int maxSecondary,
            int actualPrimary,
            int actualSecondary,
            ScaleType scaleType) {

        // 如果主要值和次要的值為0,就返回實際值,如果我們計算寬度的期望值,
        那麼主要值就是寬度,高度就是次要值,反之亦然
        if ((maxPrimary == 0) && (maxSecondary == 0)) {
            return actualPrimary;
        }

        // 如果為ScaleType.FIT_XY,填充整個矩形,忽略比值;
        即如果主要的值為0則返回實際值,否則返回傳入的值
        if (scaleType == ScaleType.FIT_XY) {
            if (maxPrimary == 0) {
                return actualPrimary;
            }
            return maxPrimary;
        }

        // 如果主要的值為0,則通過比例值計算出主要的值返回
        if (maxPrimary == 0) {
            double ratio = (double) maxSecondary / (double) actualSecondary;
            return (int) (actualPrimary * ratio);
        }

        // 次要的值為0,下面的比例調整就是多餘的,那麼直接返回主要的值,
        if (maxSecondary == 0) {
            return maxPrimary;
        }

        // 圖片真實尺寸大小的比例,通過這個比例我們可以計算出次要的最大值,通
        過計算出的值和我們傳遞進來的值做比較
        double ratio = (double) actualSecondary / (double) actualPrimary;
        int resized = maxPrimary;

        // 如果是ScaleType.CENTER_CROP,填充整個矩形,保持長寬比,這裡的寬高值相等或者大於傳入的寬高尺寸
        if (scaleType == ScaleType.CENTER_CROP) {
            // 小於傳入的次要最大值,則返回通過比例計算的最大值,這裡相當於把resized 值增大
            if ((resized * ratio) < maxSecondary) {
                resized = (int) (maxSecondary / ratio);
            }
            return resized;
        }
        //  其它scaleType值,如果計算的值大於次要值,那麼resized 值減小
        if ((resized * ratio) > maxSecondary) {
            resized = (int) (maxSecondary / ratio);
        }
        return resized;
    }

    //解析response
    @Override
    protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
        synchronized (sDecodeLock) {
            try {
                return doParse(response);
            } catch (OutOfMemoryError e) {
                VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
                return Response.error(new ParseError(e));
            }
        }
    }

    //解析的地方
    private Response<Bitmap> doParse(NetworkResponse response) {
        byte[] data = response.data;
        BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
        Bitmap bitmap = null;
        //如果最大寬度和最大高度都傳入的為0,直接解析成一個bitmap
        if (mMaxWidth == 0 && mMaxHeight == 0) {
            decodeOptions.inPreferredConfig = mDecodeConfig;
            bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
        } else {
            //如果要調整圖片的大小,首先獲取圖片真實的尺寸大小,首先設定inJustDecodeBounds為true,不載入到記憶體但是可以獲取影象的寬高
            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);

            // 計算出取樣值,2的倍數
            decodeOptions.inJustDecodeBounds = false;
            decodeOptions.inSampleSize =
                    findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
            Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

            // 如果取樣率計算出的值為1的話,那麼就沒有尺寸壓縮,tempBitmap的寬高值就是圖片的
            真實值,那麼這裡就需要縮放到滿足我們上面計算出來的值
            if (tempBitmap != null
                    && (tempBitmap.getWidth() > desiredWidth
                            || tempBitmap.getHeight() > desiredHeight)) {
                bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true);
                tempBitmap.recycle();
            } else {
                bitmap = tempBitmap;
            }
        }

        //回撥給使用者
        if (bitmap == null) {
            return Response.error(new ParseError(response));
        } else {
            return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
        }
    }

    @Override
    public void cancel() {
        super.cancel();
        synchronized (mLock) {
            mListener = null;
        }
    }

    @Override
    protected void deliverResponse(Bitmap response) {
        Response.Listener<Bitmap> listener;
        synchronized (mLock) {
            listener = mListener;
        }
        if (listener != null) {
            listener.onResponse(response);
        }
    }

    //計算合適的取樣率
    @VisibleForTesting
    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的高效載入。首先我們獲取到返回的response進行解析,然後根據傳遞的期望寬高以及圖片格式生成Bitmap返回,對於我們傳入的寬高會按比例裁剪,不是直接使用裁剪到合適的值,不然會有拉伸,最後再回撥給使用者。

二、ImageLoader 分析

我們直接先看構造方法,看所有的關鍵地方,不重要的就不分析了

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

沒啥說的,就是賦值,一個是請求佇列,一個是圖片快取的自己實現,這個是在內部把請求新增到請求佇列,所以直接傳遞進去,第二個引數快取,我們可以自己實現,一般使用LruCache實現。 接下來我們接著看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);
                }
            }
        };
}
複製程式碼

這個方法比較簡單,就是傳入我們的image view進行設定影象,然後分別提供一個預設和請求失敗的佔點陣圖,剛開始設定的時候還沒有請求到Bitmap,所以最開始設定的事預設圖。
首先看兩個變數,後面需要用到:

//相同URL正在請求中儲存的map
private final HashMap<String, BatchedImageRequest> mInFlightRequests = new HashMap<>();
//相同URL請求結果儲存的map
private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<>();
複製程式碼

接下來我們看看關鍵的一步

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

        // 檢查當前執行緒是否在主執行緒,只滿足從主執行緒發起的請求
        Threads.throwIfNotOnMainThread();

        //根據url、width、height、scaleType拼接的快取key
        final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);

        // 從快取中查詢bitmap
        Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
        if (cachedBitmap != null) {
            // 有相應的快取那麼則返回一個ImageContainer,包括其中的bitmap
            ImageContainer container =
                    new ImageContainer(
                            cachedBitmap, requestUrl, /* cacheKey= */ null, /* listener= */ null);
            // 直接呼叫onResponse,把bitmap設定給imageView
            imageListener.onResponse(container, true);
            return container;
        }

        // 快取中沒有查詢到,那麼我們直接獲取,首先new一個ImageContainer
        ImageContainer imageContainer =
                new ImageContainer(null, requestUrl, cacheKey, imageListener);

        // 更新呼叫的地方,使用預設的圖片先設定
        imageListener.onResponse(imageContainer, true);
        //檢查是否有相同的cacheKey請求正在執行
        BatchedImageRequest request = mInFlightRequests.get(cacheKey);
        if (request != null) {
            // 如果相同的請求正在執行,那麼不需要重複請求,只需要將這個例項化
            的imageContainer新增到BatchedImageRequest的mContainers中,然後請
            求結束後對所有新增到集合中的imageContainer依次回撥
            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;
}
複製程式碼

這個方法是一個重點,主要流程是首先看看快取裡面有沒有快取的Bitmap,來源於我們自己實現的快取策略,我們使用的是記憶體快取的話這裡就是一級快取;如果有直接呼叫onResponse方法設定圖片,如果沒有,首先例項化ImageContainer,涉及到了幾個類,接下來就看是否有相同的請求,如果有則新增到一個集合中,請求下來統一處理;如果沒有那麼則構造一個Request,通過RequestQueue去獲取網路圖片,可能是網路請求也有可能是磁碟快取的,這裡就是二級快取,然後新增到正在請求的集合中。
接下來看一看ImageContainer這個類,這個類就是影象請求的一個容器物件

public class ImageContainer {
        //imageview載入的Bitmap
        private Bitmap mBitmap;
        
        //圖片載入成功和失敗的監聽
        private final ImageListener mListener;

        //快取的key
        private final String mCacheKey;

        //請求指定的URL
        private final String mRequestUrl;

        public ImageContainer(
                Bitmap bitmap, String requestUrl, String cacheKey, ImageListener listener) {
            mBitmap = bitmap;
            mRequestUrl = requestUrl;
            mCacheKey = cacheKey;
            mListener = listener;
        }

        //取消請求
        @MainThread
        public void cancelRequest() {
            Threads.throwIfNotOnMainThread();

            if (mListener == null) {
                return;
            }
            //從正在請求的集合獲取一個批處理request
            BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
            if (request != null) {
                //如果取到request,那麼首先從mContainers移除當前的這個ImageContainer,
                如果移除後集合為空一個ImageContainer也沒有了,那麼則取消掉這個請求並返回 true
                boolean canceled = request.removeContainerAndCancelIfNecessary(this);
                if (canceled) {
                    //取消了請求則從正在請求的集合中移除BatchedImageRequest
                    mInFlightRequests.remove(mCacheKey);
                }
            } else {
                // 如果已經請求完成新增到批處理中準備處理分發
                request = mBatchedResponses.get(mCacheKey);
                if (request != null) {
                    //首先從mContainers移除當前的這個ImageContainer
                    request.removeContainerAndCancelIfNecessary(this);
                    if (request.mContainers.size() == 0) {
                        //如果集合中一個ImageContainer都沒有,則從等待處理的
                        response中移除掉這個BatchedImageRequest
                        mBatchedResponses.remove(mCacheKey);
                    }
                }
            }
        }

        public Bitmap getBitmap() {
            return mBitmap;
        }

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

這個類主要包含是一個圖片的容器物件,裡面包括了bitmap、監聽器、快取的key以及請求的URL,每個請求都會先組裝這個類,然後新增到一個BatchedImageRequest的mContainers中
接下來我們看看真正發起請求的地方:

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);
                    }
                });
    }
    
    //圖片請求成功
    protected void onGetImageSuccess(String cacheKey, Bitmap response) {
        // 新增到以及快取中
        mCache.putBitmap(cacheKey, response);

        // 從正在執行的請求列表中刪除這個請求
        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);

        if (request != null) {
            //更新BatchedImageRequest的bitmap
            request.mResponseBitmap = response;
            //傳送一個批處理請求,將多個相同的請求進行分發
            batchResponse(cacheKey, request);
        }
    }

    //圖片請求失敗,跟上面成功處理大致類似
    protected void onGetImageError(String cacheKey, VolleyError error) {
        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);

        if (request != null) {
            //設定這個請求的錯誤
            request.setError(error);

            batchResponse(cacheKey, request);
        }
    }
複製程式碼

這裡執行網路請求還是呼叫我們上面分析的ImageRequest方法,而且在回撥中分別對成功和失敗在進行了一次處理。
接下來我們看看這個批量處理圖片的batchResponse方法:

private void batchResponse(String cacheKey, BatchedImageRequest request) {
        //首先新增到這個map中,表明現在進入了批處理中
        mBatchedResponses.put(cacheKey, request);
        // 如果還沒有進行處理,那麼我們則開始一個新的任務
        if (mRunnable == null) {
            mRunnable =
                    new Runnable() {
                        @Override
                        public void run() {
                            //迴圈mBatchedResponses的所有值
                            for (BatchedImageRequest bir : mBatchedResponses.values()) {
                                //迴圈BatchedImageRequest的mContainers的值
                                for (ImageContainer container : bir.mContainers) {
                                    //如果有的請求取消了,在收到請求的
                                    響應後還沒有分發之前那麼跳過迴圈下一個
                                    if (container.mListener == null) {
                                        continue;
                                    }
                                    // 如果不是請求錯誤則呼叫onResponse
                                    if (bir.getError() == null) {
                                        container.mBitmap = bir.mResponseBitmap;
                                        container.mListener.onResponse(container, false);
                                    } else {
                                       //請求報錯則呼叫onErrorResponse設定一個錯誤的圖片展示 container.mListener.onErrorResponse(bir.getError());
                                    }
                                }
                            }
                            //清除所有響應的BatchedImageRequest
                            mBatchedResponses.clear();
                            //置為null,通過是否為null判斷當前是否正在處理
                            mRunnable = null;
                        }
                    };
            // 將這個post投遞到主執行緒去執行
            mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
        }
    }
複製程式碼

這段程式碼也很簡單,不過有個地方有個比較奇怪的地方,為啥是使用雙層迴圈,為啥不直接使用內層的迴圈;個人認為有可能是這樣,首先這個mBatchedResponses剛開始進來新增了相同的key的請求的BatchedImageRequest,那麼存在正在分發的時候又有不同的key的請求進來了,因為正在處理的時候runnable不為null,則後續新增的有可能不能分發,所以要遍歷這個map中所有的請求。

三、 NetworkImageView 分析

這是一個繼承ImageView的自定義view

public class NetworkImageView extends ImageView {
    private String mUrl;
    //設定預設的圖片
    private int mDefaultImageId;
    //設定請求錯誤的時候顯示的圖片
    private int mErrorImageId;
    
    private ImageLoader mImageLoader;
    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
    @MainThread
    public void setImageUrl(String url, ImageLoader imageLoader) {
        Threads.throwIfNotOnMainThread();
        mUrl = url;
        mImageLoader = imageLoader;
        // 我們的url可能已經更改,那麼我們則需要判斷是否需要載入
        loadImageIfNecessary(/* isInLayoutPass= */ false);
    }

    public void setDefaultImageResId(int defaultImage) {
        mDefaultImageId = defaultImage;
    }

    public void setErrorImageResId(int errorImage) {
        mErrorImageId = errorImage;
    }

    //如果檢視尚未載入影象,那麼我們則去載入它
    void loadImageIfNecessary(final boolean isInLayoutPass) {
        int width = getWidth();
        int height = getHeight();
        ScaleType scaleType = getScaleType();

        boolean wrapWidth = false, wrapHeight = false;
        if (getLayoutParams() != null) {
            wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT;
            wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT;
        }
        
        //如果不知道檢視的大小並且不是WRAP_CONTENT就暫停載入
        boolean isFullyWrapContent = wrapWidth && wrapHeight;
        if (width == 0 && height == 0 && !isFullyWrapContent) {
            return;
        }

        // 如果url為空的話則請取消所有的請求,包括以前的請求,假如請求兩次
        最後次的url為null,這時候還沒請求完成,肯定以最後次為準
        if (TextUtils.isEmpty(mUrl)) {
            if (mImageContainer != null) {
                mImageContainer.cancelRequest();
                mImageContainer = null;
            }
            //設定預設的圖片
            setDefaultImageOrNull();
            return;
        }

        // 檢查是否取消以前的請求
        if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
            if (mImageContainer.getRequestUrl().equals(mUrl)) {
                // 如果請求和以前相同則沒必要再次請求
                return;
            } else {
                // 如果存在正在請求的url並且請求url不同,那麼取消正在請求的url
                mImageContainer.cancelRequest();
                setDefaultImageOrNull();
            }
        }

        // 計算最大寬高,如果設定WRAP_CONTENT那麼則圖片是多大就是多大,其它
        情況則直接使用佈局的寬高,如果設定了具體的值就有可能裁剪
        int maxWidth = wrapWidth ? 0 : width;
        int maxHeight = wrapHeight ? 0 : height;

        // 使用ImageLoader來請求影象,上面已經分析了,最終返回一個ImageContainer
        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) {
                                //isImmediate:在網路請求過程中呼叫的時候為true,可以用來
                                區分是否是取的快取影象還是網路影象載入
                                isInLayoutPass:如果通過onLayout呼叫此函式,
                                則為true,否則為false
                                這個if的意思就是,如果是快取影象並且是在佈局中呼叫那麼則傳送
                                到主執行緒並延遲設定影象,因為可能多次呼叫
                                if (isImmediate && isInLayoutPass) {
                                    post(
                                            new Runnable() {
                                                @Override
                                                public void run() {
                                                    onResponse(response, /* isImmediate= */ false);
                                                }
                                            });
                                    return;
                                }
                                //請求成功載入圖片
                                if (response.getBitmap() != null) {
                                    setImageBitmap(response.getBitmap());
                                } else if (mDefaultImageId != 0) {
                                    setImageResource(mDefaultImageId);
                                }
                            }
                        },
                        maxWidth,
                        maxHeight,
                        scaleType);
    }

    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(/* isInLayoutPass= */ true);
    }

    //當imageview銷燬的時候,取消請求並且清除ImageContainer以便重新載入影象
    @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();
    }
}

複製程式碼

這個類沒啥好分析的,就是利用前兩個類來完成請求,只不過方便的是直接在xml中使用,使用ImageLoader請求的Bitmap設定給NetworkImageView

四、總結

三種不同的方式都可以完成圖片的載入,不過後面的方式都比較依賴前面的ImageRequest,畢竟還是要這個類去完成網路請求操作;在使用中,根據不同的場景選擇不同的方式使用。不過我建議使用ImageLoader來載入圖片,可以自己設定快取,兩級快取,一級記憶體快取,一級volley請求時候的磁碟快取。總體來講封裝的很不錯,對一些細節處理的比較好,比如相同的請求、圖片的裁剪等,值得我們學習的地方很多。

參考

Android Volley完全解析(二),使用Volley載入網路圖片
Volley 原始碼解析

相關文章