編輯:關於Android編程
通過前面淺析(一)和淺析(二)的分析,相信大家對於Volley有了初步的認識,但是如果想更深入的理解,還需要靠大家多多看源碼。
這篇文章中我們主要來研究一下使用Volley框架請求大量圖片的原理,在Android的應用中,通過http請求獲取的數據主要有三類:
1、json
2、xml
3、Image
其中json和xml的獲取其實原理很簡單,使用Volley獲取感覺有點大財小用了,了解Volley獲取圖片的原理才是比較有意義的,因為裡面涉及到很多知識點,比如獲取大量圖片如何防止OOM。
那麼我們就開始研究源碼吧。
(1) ImageLoader.java
通過它的名字我們就知道是用來加載Image的工具類
/**
通過調用ImageLoader的get方法就可以獲取到圖片,然後通過一個Listener回調,將圖片設置到ImgeView中(這個方法務必在主線程中調用)
*/
public class ImageLoader {
/** 前面已經接觸過,請求隊列(其實不是真實的隊列,裡面包含了本地隊列和網絡隊列) */
private final RequestQueue mRequestQueue;
/** 圖片緩沖,這個緩存不是前面提到的磁盤緩存,這個是內存緩存,我們可以通過LruCache實現這個接口 */
private final ImageCache mCache;
/**
* 用於存放具有相同cacheKey的請求
*/
private final HashMap mInFlightRequests =
new HashMap();
/** 用於存放具有相同Key,並且返回了數據的請求*/
private final HashMap mBatchedResponses =
new HashMap();
/** Handler to the main thread. */
private final Handler mHandler = new Handler(Looper.getMainLooper());
/** Runnable for in-flight response delivery. */
private Runnable mRunnable;
/**
* Simple cache adapter interface. If provided to the ImageLoader, it
* will be used as an L1 cache before dispatch to Volley. Implementations
* must not block. Implementation with an LruCache is recommended.
*/
public interface ImageCache {
public Bitmap getBitmap(String url);
public void putBitmap(String url, Bitmap bitmap);
}
/**
* 構造函數需要傳入一個RequestQueue對象和一個內存緩存對象
* @param queue The RequestQueue to use for making image requests.
* @param imageCache The cache to use as an L1 cache.
*/
public ImageLoader(RequestQueue queue, ImageCache imageCache) {
mRequestQueue = queue;
mCache = imageCache;
}
/**
* 用於圖片獲取成功或者失敗的回調
* @param imageView 需要設置圖片的ImageView.
* @param defaultImageResId 默認顯示圖片.
* @param errorImageResId 出錯時顯示的圖片.
*/
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);
}
}
};
}
/**
* 判斷圖片是否已經緩存,不同尺寸的圖片的cacheKey是不一樣的
* @param requestUrl 圖片的url
* @param maxWidth 請求圖片的寬度.
* @param maxHeight 請求圖片的高度.
* @return 返回true則緩存.
*/
public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
throwIfNotOnMainThread();
String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
return mCache.getBitmap(cacheKey) != null;
}
/**
* 這個方法時個核心方法,我們主要通過它來獲取圖片
*
* @param requestUrl The URL of the image to be loaded.
* @param defaultImage Optional default image to return until the actual image is loaded.
*/
public ImageContainer get(String requestUrl, final ImageListener listener) {
return get(requestUrl, listener, 0, 0);
}
/**
* 這個方法比上面方法多了兩個參數,如果傳入則圖片大小會做相應處理,如果不傳默認為0,圖片大小不做處理
* @param requestUrl The url of the remote image
* @param imageListener The listener to call when the remote image is loaded
* @param maxWidth The maximum width of the returned image.
* @param maxHeight The maximum height of the returned image.
* @return A container object that contains all of the properties of the request, as well as
* the currently available image (default if remote is not loaded).
*/
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight) {
// only fulfill requests that were initiated from the main thread.
throwIfNotOnMainThread();
//獲取key,其實就是url,width,height按照某種格式拼接
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
// 首先從緩存裡面取圖片
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
// 如果緩存命中,則直接放回
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container, true);
return container;
}
// 沒有命中,則創建一個ImageContainer,注意此時圖片數據傳入的null,
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener);
// 這就是為什麼在onResponse中我們需要判斷圖片數據是否為空,此時就是為空的
imageListener.onResponse(imageContainer, true);
// 判斷同一個key的請求是否已經存在
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
// 如果存在,則直接加入request中,沒有必要對一個key發送多個請求
request.addContainer(imageContainer);
return imageContainer;
}
// 發送一個請求,並加入RequestQueue
Request newRequest =
new ImageRequest(requestUrl, new Listener() {
@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);
}
});
mRequestQueue.add(newRequest);
VolleyLog.e(-------------->+newRequest.getSequence());
//加入到HashMap中,表明這個key已經存在一個請求
mInFlightRequests.put(cacheKey,
new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
/**
* Handler for when an image was successfully loaded.
* @param cacheKey The cache key that is associated with the image request.
* @param response The bitmap that was returned from the network.
*/
private void onGetImageSuccess(String cacheKey, Bitmap response) {
// 獲取圖片成功,放入緩存
mCache.putBitmap(cacheKey, response);
// 將cacheKey對應的請求從mInFlightRequests中移除
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
if (request != null) {
// Update the response bitmap.
request.mResponseBitmap = response;
// Send the batched response
batchResponse(cacheKey, request);
}
}
/**
* Handler for when an image failed to load.
* @param cacheKey The cache key that is associated with the image request.
*/
private void onGetImageError(String cacheKey, VolleyError error) {
// Notify the requesters that something failed via a null result.
// Remove this request from the list of in-flight requests.
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
if (request != null) {
// Set the error for this request
request.setError(error);
// Send the batched response
batchResponse(cacheKey, request);
}
}
/**
* Container object for all of the data surrounding an image request.
*/
public class ImageContainer {
/**
* 保存從網絡獲取的圖片
*/
private Bitmap mBitmap;
private final ImageListener mListener;
/** The cache key that was associated with the request */
private final String mCacheKey;
/** The request URL that was specified */
private final String mRequestUrl;
/**
* Constructs a BitmapContainer object.
* @param bitmap The final bitmap (if it exists).
* @param requestUrl The requested URL for this container.
* @param cacheKey The cache key that identifies the requested URL for this container.
*/
public ImageContainer(Bitmap bitmap, String requestUrl,
String cacheKey, ImageListener listener) {
mBitmap = bitmap;
mRequestUrl = requestUrl;
mCacheKey = cacheKey;
mListener = listener;
}
/**
* 取消一個圖片請求
*/
public void cancelRequest() {
if (mListener == null) {
return;
}
//判斷此key對應的請求有沒有
BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
if (request != null) {
/**如果存在,request中mContainers中的這個Container,如果mContainers的size為0,那麼
removeContainerAndCancelIfNecessary返回true
*/
boolean canceled = request.removeContainerAndCancelIfNecessary(this);
if (canceled) {
//如果返回true,那麼說明沒有任何一個ImageView對這個請求感興趣,需要移除它
mInFlightRequests.remove(mCacheKey);
}
} else {
// 判斷是否這個request已經成功返回了
request = mBatchedResponses.get(mCacheKey);
if (request != null) {
request.removeContainerAndCancelIfNecessary(this);
if (request.mContainers.size() == 0) {
//如果已經成功返回,並且沒有ImageView對他感興趣,那麼刪除它
mBatchedResponses.remove(mCacheKey);
}
}
}
}
/**
* Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
*/
public Bitmap getBitmap() {
return mBitmap;
}
/**
* Returns the requested URL for this container.
*/
public String getRequestUrl() {
return mRequestUrl;
}
}
/**
* 對Request的一個包裝,將所有有共同key的請求放入一個LinkedList中
*/
private class BatchedImageRequest {
/** The request being tracked */
private final Request mRequest;
/** The result of the request being tracked by this item */
private Bitmap mResponseBitmap;
/** Error if one occurred for this response */
private VolleyError mError;
/** 存放具有共同key的ImageContainer*/
private final LinkedList mContainers = new LinkedList();
/**
* Constructs a new BatchedImageRequest object
* @param request The request being tracked
* @param container The ImageContainer of the person who initiated the request.
*/
public BatchedImageRequest(Request request, ImageContainer container) {
mRequest = request;
mContainers.add(container);
}
/**
* Set the error for this response
*/
public void setError(VolleyError error) {
mError = error;
}
/**
* Get the error for this response
*/
public VolleyError getError() {
return mError;
}
/**
* Adds another ImageContainer to the list of those interested in the results of
* the request.
*/
public void addContainer(ImageContainer container) {
mContainers.add(container);
}
/**
* 移除一個ImageContainer,如果此時size==0,那麼需要從mInFlightRequests中移除該BatchedImageRequest
* @param container The container to remove from the list
* @return True if the request was canceled, false otherwise.
*/
public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
mContainers.remove(container);
if (mContainers.size() == 0) {
mRequest.cancel();
return true;
}
return false;
}
}
/**
* 當請求返回後,將BatchedImageRequest放入到mBatchedResponses,然後將結果發送給所有具有相同key的ImageContainer,ImageContainer通過裡面的Listener發送到ImageView,從而顯示出來
* @param cacheKey The cacheKey of the response being delivered.
* @param request The BatchedImageRequest to be delivered.
* @param error The volley error associated with the request (if applicable).
*/
private void batchResponse(String cacheKey, BatchedImageRequest request) {
mBatchedResponses.put(cacheKey, request);
// If we don't already have a batch delivery runnable in flight, make a new one.
// Note that this will be used to deliver responses to all callers in mBatchedResponses.
if (mRunnable == null) {
mRunnable = new Runnable() {
@Override
public void run() {
for (BatchedImageRequest bir : mBatchedResponses.values()) {
for (ImageContainer container : bir.mContainers) {
// If one of the callers in the batched request canceled the request
// after the response was received but before it was delivered,
// skip them.
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;
}
};
// Post the runnable.
mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
}
}
/**
* 獲取一個請求的key,拼接規則就是使用#講幾個連接起來
* @param url The URL of the request.
* @param maxWidth The max-width of the output.
* @param maxHeight The max-height of the output.
*/
private static String getCacheKey(String url, int maxWidth, int maxHeight) {
return new StringBuilder(url.length() + 12).append(#W).append(maxWidth)
.append(#H).append(maxHeight).append(url).toString();
}
}
ImageLoader的代碼還是比較復雜的,但是思路還是比較清晰的,總結如下:
如果我們僅僅是獲取少量圖片,Volley框架為我們提供了一個NetworkImageView,這個類繼承自ImageView,使用時,我們只需要調用setImageUrl即可,下面就來看其實現機制
(2) NetworkImageView.java
public class NetworkImageView extends ImageView {
/** 需要加載圖片的url */
private String mUrl;
/**
* 默認顯示圖片的id
*/
private int mDefaultImageId;
/**
* 錯誤圖片的id
*/
private int mErrorImageId;
/** ImageLoader對象,其實就是用該對象去獲取圖片,所以了解了ImageLoader後,這個類很好理解 */
private ImageLoader mImageLoader;
/**把這個對象當成url和Listener的封裝即可 */
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
*
* @param url The URL that should be loaded into this ImageView.
* @param imageLoader ImageLoader that will be used to make the request.
*/
public void setImageUrl(String url, ImageLoader imageLoader) {
mUrl = url;
mImageLoader = imageLoader;
// 這個方法我們後面分析
loadImageIfNecessary(false);
}
/**
* Sets the default image resource ID to be used for this view until the attempt to load it
* completes.
*/
public void setDefaultImageResId(int defaultImage) {
mDefaultImageId = defaultImage;
}
/**
* Sets the error image resource ID to be used for this view in the event that the image
* requested fails to load.
*/
public void setErrorImageResId(int errorImage) {
mErrorImageId = errorImage;
}
/**
* 這個方法在onLayout方法中傳入true,其他地方傳入false
* @param isInLayoutPass True if this was invoked from a layout pass, false otherwise.
*/
void loadImageIfNecessary(final boolean isInLayoutPass) {
int width = getWidth();
int height = getHeight();
boolean wrapWidth = false, wrapHeight = false;
if (getLayoutParams() != null) {
wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT;
wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT;
}
// if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
// view, hold off on loading the image.
boolean isFullyWrapContent = wrapWidth && wrapHeight;
if (width == 0 && height == 0 && !isFullyWrapContent) {
return;
}
// if the URL to be loaded in this view is empty, cancel any old requests and clear the
// currently loaded image.
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
setDefaultImageOrNull();
return;
}
// if there was an old request in this view, check if it needs to be canceled.
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
if (mImageContainer.getRequestUrl().equals(mUrl)) {
//如果請求url相同,則直接return
return;
} else {
// 請求url不同,則cancel,並顯示默認圖片或者不顯示圖片
mImageContainer.cancelRequest();
setDefaultImageOrNull();
}
}
// Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens.
int maxWidth = wrapWidth ? 0 : width;
int maxHeight = wrapHeight ? 0 : height;
//調用了get方法
ImageContainer newContainer = 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 this was an immediate response that was delivered inside of a layout
// pass do not set the image immediately as it will trigger a requestLayout
// inside of a layout. Instead, defer setting the image by posting back to
// the main thread.
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);
// update the ImageContainer to be the new bitmap container.
mImageContainer = newContainer;
}
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(true);
}
@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();
}
}
Android嵌套滑動控件的沖突解決和ViewPager適配當前子控件高度不留空白的辦法
最近項目有一個需求,需要多層可滑動控件的嵌套展示,demo效果如下,demo的下載地址在最後 咋一看好像挺簡單啊,不就是一個ScrollView + ViewP
小米edge什麼時候上市 小米edge發布時間
小米edge什麼時候上市?相信很多米粉對於小米edge手機很是期待和關注,紛紛上網咨詢,下文介紹小米edge上市時間,一起和小編來了解下吧! 小米edge
Android RakNet 系列之五 視頻通訊 OpenCV4Android
簡介 引入OpenCV4Android的目標是在Raknet框架下解決視頻通訊的問題,目前在ubuntu下已成功實現,現在把它引用到Android平台下。 OpenCV是
如何優雅地使用NDK[功能補充]
說在前面的話:這篇文章是看了如何優雅地使用NDK後,對原博客功能的補充。為了方便大家的閱讀順序,直接添加到原博客之中,如有侵犯版權,請聯系我。這篇博客有轉載,有原創,只是