在上一篇文章中,咱們一塊兒深刻探究了 Volley 的緩存機制,經過源碼分析對緩存的工做原理進行了瞭解,這篇文章將帶你們一塊兒探究「Volley 圖片加載的實現」,圖片加載跟緩存仍是有比較緊密的聯繫的,建議你們先去看下:Android Volley 源碼解析(二),探究緩存機制。緩存
這是 Volley 源碼解析系列的最後一篇文章,今天咱們經過以基本用法和源碼分析相結合的方式來進行,固然本文的源碼仍是創建在第一篇源碼分析的基礎上的,尚未看過這篇文章的朋友,建議先去閱讀:Android Volley 源碼解析(一),網絡請求的執行流程。bash
在進行源碼解析以前,咱們先來看一下 Volley 中有關圖片加載的基本用法。服務器
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);
複製代碼
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() 方法便能進行圖片的加載。
除了以上兩種方式以外,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);
複製代碼
在上一節中介紹了 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 步來看
經過 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;
}
複製代碼
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;
}
複製代碼
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));
}
複製代碼
咱們在上面說到 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);
}
}
複製代碼
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 源碼的閱讀,到以後的代碼整理以及如今的文章輸出,花了我差很少一個星期的時間,不過對於網絡加載和圖片加載有了更深的理解。能完整看到這裏的都是真愛啊,謝謝你們了。