上篇文章我們講到了如何用volley進行簡單的網絡請求,我們可以很容易的接受到string、JsonObjec類型的返回結果,之前的例子僅僅是一次請求,這里需要說明volley本身就是適合高並發的,所以它可以運行你用volley在短時間內進行多次請求,並且不用去手動管理線程數。僅僅是請求文字過於基礎了,本篇將講述如何用volley從網絡下載圖片。
一、用ImageRequest來請求圖片
ImageRequest是一個圖片請求對象,它繼承自Request<Bitmap>,所以請求得到的結果是一個bitmap。
1.1 使用步驟
ImageRequest仍舊是一個request對象,所以使用方式和StringRequest、JsonObjectRequest、JsonArrayRequest十分相似。
步驟:
- 建立一個RequestQueue對象
- 建立一個ImageRequest對象
- 將ImageRequest添加到RequestQueue中
第一步、第三步我們在上篇文章中已經做好了,如果不清楚的話可以去上一篇文章查看。
1.2 分析構造函數
源碼中的構造函數是這樣定義的:
public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight, Config decodeConfig, Response.ErrorListener errorListener) { super(Method.GET, url, errorListener); setRetryPolicy( new DefaultRetryPolicy(IMAGE_TIMEOUT_MS, IMAGE_MAX_RETRIES, IMAGE_BACKOFF_MULT)); mListener = listener; mDecodeConfig = decodeConfig; mMaxWidth = maxWidth; mMaxHeight = maxHeight; }
默認的請求方式是GET,初始化方法需要傳入:圖片的url,一個響應結果監聽器,圖片的最大寬度,圖片的最大高度,圖片的顏色屬性,出錯響應的監聽器。
說明:圖片的顏色屬性,Bitmap.Config下的幾個常量都可以在這里使用,其中ARGB_8888可以展示最好的顏色屬性,每個圖片像素占據4個字節的大小,而RGB_565則表示每個圖片像素占據2個字節大小
/** Socket timeout in milliseconds for image requests */ private static final int IMAGE_TIMEOUT_MS = 1000; /** Default number of retries for image requests */ private static final int IMAGE_MAX_RETRIES = 2; /** Default backoff multiplier for image requests */ private static final float IMAGE_BACKOFF_MULT = 2f;
- 設定超時時間:1000ms;
- 最大的請求次數:2次;
- 發生沖突時的重傳延遲增加數:2f(這個應該和TCP協議有關,沖突時需要退避一段時間,然后再次請求);
1.3 解釋maxWidth,maxHeight參數
注釋中詳細說明了圖片寬高的意義和作用,為了便於理解我再詳細說一下。
/** * Creates a new image request, decoding to a maximum specified width and * height. If both width and height are zero, the image will be decoded to * its natural size. If one of the two is nonzero, that dimension will be * clamped and the other one will be set to preserve the image's aspect * ratio. If both width and height are nonzero, the image will be decoded to * be fit in the rectangle of dimensions width x height while keeping its * aspect ratio. * * @param url URL of the image * @param listener Listener to receive the decoded bitmap * @param maxWidth Maximum width to decode this bitmap to, or zero for none * @param maxHeight Maximum height to decode this bitmap to, or zero for * none * @param decodeConfig Format to decode the bitmap to * @param errorListener Error listener, or null to ignore errors */
先來完整解釋下注釋的意思:
- 建立一個請求對象,按照最大寬高進行解碼 。
- 如果設定的寬和高都是0,那么下載到的圖片將會按照實際的大小進行解碼,也就是不壓縮。
- 如果寬和高中的一個或兩個值不為0,那么圖片的寬/高(取決於你設定了寬還是高)會壓縮至設定好的值,而另一個寬/高將會按原始比例改變。
- 如果寬和高都不是0,那么得到的圖片將會“按比例”解碼到你設定的寬高,也就是說最終得到的圖片大小不一定是你最初設定的大小。
舉個例子:
我的圖片原本像素是:850x1200.
當maxWidth = 0,maxHeight = 0時,最終得到的bitmap的寬高是850x1200
當maxWidth = 0,maxHeight = 600時,得到的bitmap是425x600.這就說明它會按照一個不為0的邊的值,將圖片進行等比縮放。
當maxWidth = 100,maxHeight = 600時,我們得到的bitmap竟然是100x141,是按照100進行等比縮小后的圖片,而不是100x600.
要弄清這個問題,我們還得看源碼,源碼中解析響應結果的方法叫做doParse(…)
/** * The real guts of parseNetworkResponse. Broken out for readability. */ private Response<Bitmap> doParse(NetworkResponse response) { byte[] data = response.data; BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); Bitmap bitmap = null; if (mMaxWidth == 0 && mMaxHeight == 0) { // 如果寬高都是0,那么就返回原始尺寸 decodeOptions.inPreferredConfig = mDecodeConfig; bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); } else { // If we have to resize this image, first get the natural bounds. // 如果我們已經重設了image的尺寸(寬高中有一個或兩個不為0),那么先得到原始的大小 decodeOptions.inJustDecodeBounds = true; // 設置先不得到bitmap,僅僅獲取bitmap的參數。 BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); // 第一次解碼,主要獲得的是bitmap的實際寬、高 int actualWidth = decodeOptions.outWidth; // 得到bitmap的寬 int actualHeight = decodeOptions.outHeight; // 得到bitmap的高 // Then compute the dimensions we would ideally like to decode to. // 然后計算我們想要得到的最終尺寸 int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight, actualWidth, actualHeight); int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth, actualHeight, actualWidth); // Decode to the nearest power of two scaling factor. // 把圖片解碼到最接近2的冪次方的大小 decodeOptions.inJustDecodeBounds = false; // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it? // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED; decodeOptions.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); // If necessary, scale down to the maximal acceptable size. // 如果有必要的話,把得到的bitmap的最大邊進行壓縮來適應尺寸 if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || tempBitmap.getHeight() > desiredHeight)) { // 通過createScaledBitmap來壓縮到目標尺寸 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)); } } /** * Returns the largest power-of-two divisor for use in downscaling a bitmap * that will not result in the scaling past the desired dimensions. * * @param actualWidth Actual width of the bitmap * @param actualHeight Actual height of the bitmap * @param desiredWidth Desired width of the bitmap * @param desiredHeight Desired height of the bitmap */ // Visible for testing. static int findBestSampleSize( int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) { // 計算inSampleSize的方法,詳細知識自行百度吧。最終原圖會被壓縮為inSampleSize分之一 // inSampleSize的值計算出來都是2的冪次方 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; }
此時我們發現重要的方法是getResizedDimension,它最終確定了圖片的最終尺寸。
/** * Scales one side of a rectangle to fit aspect ratio. * * @param maxPrimary Maximum size of the primary dimension (i.e. width for * max width), or zero to maintain aspect ratio with secondary * dimension * @param maxSecondary Maximum size of the secondary dimension, or zero to * maintain aspect ratio with primary dimension * @param actualPrimary Actual size of the primary dimension * @param actualSecondary Actual size of the secondary dimension */ private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, int actualSecondary) { // If no dominant value at all, just return the actual. if (maxPrimary == 0 && maxSecondary == 0) { return actualPrimary; } // If primary is unspecified, scale primary to match secondary's scaling ratio. 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 (resized * ratio > maxSecondary) { resized = (int) (maxSecondary / ratio); } return resized; }
在我們目標寬、高都不為0時會調用下面的代碼段:
double ratio = (double) actualSecondary / (double) actualPrimary; int resized = maxPrimary; if (resized * ratio > maxSecondary) { resized = (int) (maxSecondary / ratio); }
它會計算一個ratio(比值),這就是為啥它會按比例縮小的原因。
1.4 初始化對象並使用
ImageRequest imageRequest = new ImageRequest( "http://img5.duitang.com/uploads/item/201409/14/20140914162144_MBEmX.jpeg", new ResponseListener(), 0, // 圖片的寬度,如果是0,就不會進行壓縮,否則會根據數值進行壓縮 0, // 圖片的高度,如果是0,就不進行壓縮,否則會壓縮 Config.ARGB_8888, // 圖片的顏色屬性 new ResponseErrorListener());
監聽器:
private class ResponseListener implements Response.Listener<Bitmap> { @Override public void onResponse(Bitmap response) { // Log.d("TAG", "-------------\n" + response.toString()); iv.setImageBitmap(response); } } private class ResponseErrorListener implements Response.ErrorListener { @Override public void onErrorResponse(VolleyError error) { Log.e("TAG", error.getMessage(), error); } }
最后將其添加到請求隊列即可:
mQueue.add(imageRequest);
1.5 題外話
這樣我們就用volley獲得了網絡圖片,代碼也十分簡單。你可能會說,有沒有其他的,更好的方式來獲取圖片呢?當然有的,比如volley還提供了ImageLoader、NetworkImageView這樣的對象,它們可以更加方便的獲取圖片。值得一提的是這兩個對象的內部都是使用了ImageRequest進行操作的,也就是說imageRequest是本質,這也就是為啥我專門寫一篇來分析ImageRequest的原因。
說話要言之有理,所以貼上ImageLoader、NetworkImageView源碼中部分片段來證明其內部確實是用了ImageRequest。
ImageLoader的源碼片段:
public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight) { // ………// The request is not already in flight. Send the new request to the network and // track it. Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, cacheKey); newRequest.setShouldCache(mShouldCache); mRequestQueue.add(newRequest); mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); return imageContainer; }
protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight, final String cacheKey) { return new ImageRequest(requestUrl, new Listener<Bitmap>() { @Override public void onResponse(Bitmap response) { onGetImageSuccess(cacheKey, response); } }, maxWidth, maxHeight, Config.RGB_565, new ErrorListener() { @Override public void onErrorResponse(VolleyError error) { onGetImageError(cacheKey, error); } }); }
在ImageLoader重要的get()方法中,建立了一個newRequest對象,並將其放入請求隊列中。這里的newRequest是通過makeImageRequest()來產生的,而makeImageRequest()實際是返回了一個ImageRequest對象。所以用到了ImageRequest對象。
NetworkImageView的源碼片段:
public void setImageUrl(String url, ImageLoader imageLoader) { mUrl = url; mImageLoader = imageLoader; // The URL has potentially changed. See if we need to load it. loadImageIfNecessary(false); }
它本身就調用的是ImageLoader對象,所以自然也是用到了ImageRequest。
二、Request簡介
2.1 前言
Request是Volley中最最核心的類,之前講到的對象都是它的子類。從字面意思看,這個對象是用來執行請求的,但通過之前的使用我們發現,它還做了很多別的事情。先貼一個Request的子類。
ImageRequest imageRequest = new ImageRequest( "http://img5.duitang.com/uploads/item/201409/14/20140914162144_MBEmX.jpeg", new ResponseListener(), 0, // 圖片的寬度,如果是0,就不會進行壓縮,否則會根據數值進行壓縮 0, // 圖片的高度,如果是0,就不進行壓縮,否則會壓縮 Config.ARGB_8888, // 圖片的顏色屬性 new ResponseErrorListener());
從中我們可以發現這個ImageRequest中傳入了請求的url,畢竟是request嘛,請求的url是必須的,但我們還發現這個請求對象還處理了兩個監聽器,這就說明它不僅僅做了請求,同時對於響應的結果也做了分發處理。
2.2 部分API
getCacheKey()
Returns the cache key for this request. By default, this is the URL.
返回這個請求對象中緩存對象的key,默認返回的是請求的URL
getBodyContentType()
Returns the content type of the POST or PUT body.
返回POST或PUT請求內容的類型,我測試的結果是:application/x-www-form-urlencoded; charset=UTF-8
從源碼就能看出,默認的編碼方式是UTF-8:
/** * Default encoding for POST or PUT parameters. See {@link #getParamsEncoding()}. */ private static final String DEFAULT_PARAMS_ENCODING = "UTF-8";
/** * Returns the content type of the POST or PUT body. */ public String getBodyContentType() { return "application/x-www-form-urlencoded; charset=" + getParamsEncoding(); }
getSequence()
Returns the sequence number of this request.
返回請求的序列數
getUrl()
Returns the URL of this request.
返回請求的URL
setShouldCache(boolean bl)
Set whether or not responses to this request should be cached.
設置這個請求是否有緩存,這個緩存是磁盤緩存,和內存緩存沒什么事情,默認是true,也就是說如果你不設置為false,這個請求就會在磁盤中進行緩存。其實,之前講的的StringRequest,JsonRequest,ImageRequest得到的數據都會被緩存,無論是Json數據,還是圖片都會自動的緩存起來。然而,一旦你設置setShouldCache(false),這些數據就不會被緩存了。
getBody()
Returns the raw POST or PUT body to be sent.
返回POST或PUT的請求體
deliverError()
分發錯誤信息,這個就是調用監聽器的方法,貼源碼就明白了。
/** * Delivers error message to the ErrorListener that the Request was * initialized with. * * @param error Error details */ public void deliverError(VolleyError error) { if (mErrorListener != null) { mErrorListener.onErrorResponse(error); } }
setRetryPolicy(RetryPolicy retryPolicy)
對一個request的重新請求策略的設置,不同的項目是否需要重新請求,重新請求幾次,請求超時的時間,這些就在這設置到里面。
/** * Sets the retry policy for this request. * * @return This Request object to allow for chaining. */ public Request<?> setRetryPolicy(RetryPolicy retryPolicy) { mRetryPolicy = retryPolicy; return this; }
從上面的源碼可以看出,這里需要傳入一個RetryPlicy的子類,就是重新請求策略的子類,Volley會在構造Request時傳一個默認的對象,叫做DefaultRetryPolicy。
/** * Creates a new request with the given method (one of the values from {@link Method}), * URL, and error listener. Note that the normal response listener is not provided here as * delivery of responses is provided by subclasses, who have a better idea of how to deliver * an already-parsed response. */ public Request(int method, String url, Response.ErrorListener listener) { mMethod = method; mUrl = url; mErrorListener = listener; setRetryPolicy(new DefaultRetryPolicy()); mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url); }
如果你對於網絡請求有具體的要求,可以實現RetryPolicy接口,進行自由的配置。下面貼一下DefaultRetryPolicy源碼,方便參考。

/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.volley; /** * Default retry policy for requests. */ public class DefaultRetryPolicy implements RetryPolicy { /** The current timeout in milliseconds. */ private int mCurrentTimeoutMs; /** The current retry count. */ private int mCurrentRetryCount; /** The maximum number of attempts. */ private final int mMaxNumRetries; /** The backoff multiplier for the policy. */ private final float mBackoffMultiplier; /** The default socket timeout in milliseconds */ public static final int DEFAULT_TIMEOUT_MS = 2500; /** The default number of retries */ public static final int DEFAULT_MAX_RETRIES = 1; /** The default backoff multiplier */ public static final float DEFAULT_BACKOFF_MULT = 1f; /** * Constructs a new retry policy using the default timeouts. */ public DefaultRetryPolicy() { this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT); } /** * Constructs a new retry policy. * @param initialTimeoutMs The initial timeout for the policy. * @param maxNumRetries The maximum number of retries. * @param backoffMultiplier Backoff multiplier for the policy. */ public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) { mCurrentTimeoutMs = initialTimeoutMs; mMaxNumRetries = maxNumRetries; mBackoffMultiplier = backoffMultiplier; } /** * Returns the current timeout. */ @Override public int getCurrentTimeout() { return mCurrentTimeoutMs; } /** * Returns the current retry count. */ @Override public int getCurrentRetryCount() { return mCurrentRetryCount; } /** * Returns the backoff multiplier for the policy. */ public float getBackoffMultiplier() { return mBackoffMultiplier; } /** * Prepares for the next retry by applying a backoff to the timeout. * @param error The error code of the last attempt. */ @Override public void retry(VolleyError error) throws VolleyError { mCurrentRetryCount++; mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier); if (!hasAttemptRemaining()) { throw error; } } /** * Returns true if this policy has attempts remaining, false otherwise. */ protected boolean hasAttemptRemaining() { return mCurrentRetryCount <= mMaxNumRetries; } }
2.3 產生Request對象
雖然我們在代碼中都會初始化一個Request對象,但是我們要在把他添加到響應隊列中后才能得到它的完整體。
public <T> Request<T> add(Request<T> request) {
舉例:
com.android.volley.Request<Bitmap> bitmapRequest = mQueue.add(imageRequest);
說明:如果你要設定這個request是不需要進行磁盤緩存的,那么請在把它添加到響應隊列之前就進行設置,否則會得到不想要的效果。原因:源碼在添加隊列時會判斷是否需要緩存。
/** * Adds a Request to the dispatch queue. * @param request The request to service * @return The passed-in request */ public <T> Request<T> add(Request<T> request) { // Tag the request as belonging to this queue and add it to the set of current requests. request.setRequestQueue(this); synchronized (mCurrentRequests) { mCurrentRequests.add(request); } // Process requests in the order they are added. request.setSequence(getSequenceNumber()); request.addMarker("add-to-queue"); // If the request is uncacheable, skip the cache queue and go straight to the network. if (!request.shouldCache()) { mNetworkQueue.add(request); return request; // 如果不需要緩存,直接返回request對象,不會執行下面的代碼 } // Insert request into stage if there's already a request with the same cache key in flight. synchronized (mWaitingRequests) { String cacheKey = request.getCacheKey(); if (mWaitingRequests.containsKey(cacheKey)) { // There is already a request in flight. Queue up. Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey); if (stagedRequests == null) { stagedRequests = new LinkedList<Request<?>>(); } stagedRequests.add(request); mWaitingRequests.put(cacheKey, stagedRequests); if (VolleyLog.DEBUG) { VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey); } } else { // Insert 'null' queue for this cacheKey, indicating there is now a request in // flight. mWaitingRequests.put(cacheKey, null); mCacheQueue.add(request); } return request; } }