前言
在上一篇文章中,我們一起深入探究了 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 原始碼的閱讀,到之後的程式碼整理以及現在的文章輸出,花了我差不多一個星期的時間,不過對於網路載入和圖片載入有了更深的理解。能完整看到這裡的都是真愛啊,謝謝大家了。