*** 本文仅对Volley中关于Image Request部分的一些简单用例做解析,Http Request部分请参考这里:Android Volley库源码简析(HTTP Request部分) ***
从常用case入手
用Volley请求图片有两种方式,通过ImageRequest或者是使用NetworkImageView。
使用ImageRequest
// 1. 新建一个queue
mRequestQueue = Volley.newRequestQueue(mCtx.getApplicationContext());
ImageView mImageView;
String url = "http://i.imgur.com/7spzG.png";
mImageView = (ImageView) findViewById(R.id.myImage);
// 2. 新建一个ImageRequest,传入url和回调
ImageRequest request = new ImageRequest(url,
new Response.Listener<Bitmap>() {
@Override
public void onResponse(Bitmap bitmap) {
mImageView.setImageBitmap(bitmap);
}
}, 0, 0, null,
new Response.ErrorListener() {
public void onErrorResponse(VolleyError error) {
mImageView.setImageResource(R.drawable.image_load_error);
}
});
// 3. 将image request放到queue中
mRequestQueue.add(request);
使用的具体步骤见注释。可以看出image请求与普通http请求发送流程是一样的,只是Request接口的实现不同,其中最重要的是ImageRequest实现的parseNetworkResponse(NetworkResponse response)
方法。此方法实现了从data到bitmap的转换。
使用NetworkImageView
// 1. 新建ImageLoader,传入queue和imagecache
mImageLoader = new ImageLoader(mRequestQueue,
new ImageLoader.ImageCache() {
private final LruCache<String, Bitmap>
cache = new LruCache<String, Bitmap>(20);
@Override
public Bitmap getBitmap(String url) {
return cache.get(url);
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
cache.put(url, bitmap);
}
});
NetworkImageView mNetworkImageView;
private static final String IMAGE_URL =
"http://developer.android.com/images/training/system-ui.png";
...
mNetworkImageView = (NetworkImageView) findViewById(R.id.networkImageView);
// 2. 将url和ImageLoader传给NetworkImageView
mNetworkImageView.setImageUrl(IMAGE_URL, mImageLoader);
使用的具体步骤见注释。可以看到使用NetworkImageView比直接使用ImageView的代码量较少,而且最重要的一点是,使用者不用手动管理bitmap和image request的生命周期,当NetworkImageView被回收或者不可见的时候,bitmap资源会被回收,正在进行的image request会被cancel。
下面先对第一个用例进行分析。
ImageRequest
ImageRequest的初始化代码如下所示:
public class ImageRequest extends Request<Bitmap> {
...
public ImageRequest(String url
, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
ScaleType scaleType, 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;
mScaleType = scaleType;
}
...
}
可以看到ImageRequest与普通http request的区别在于ImageRequest需要提供一些图片scale相关的参数,以供decode bitmap时使用,其中decode过程中最关键的parseNetworkResponse()
方法源码如下所示:
public class ImageRequest extends Request<Bitmap> {
...
private static final Object sDecodeLock = new Object();
...
@Override
protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
// Serialize all decode on a global lock to reduce concurrent heap usage.
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));
}
}
}
为了避免network dispatcher同时decode bitmap引起的内存不足(OOM),这里使用了一个全局的lock来序列化decode操作。
/**
* 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;
// 如果mMaxWidth和mMaxHeight都为0,则按照bitmap实际大小进行decode
if (mMaxWidth == 0 && mMaxHeight == 0) {
decodeOptions.inPreferredConfig = mDecodeConfig;
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
} else {
... // 根据mMaxWidth、mMaxHeight和scaleType来进行decode
}
if (bitmap == null) {
return Response.error(new ParseError(response));
} else {
// 最后将结果包装成Response返回给Delivery
return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
}
}
...
}
其中decode相关的代码如下所示:
// 1. 先decode一次,求出图片的实际大小
decodeOptions.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
int actualWidth = decodeOptions.outWidth;
int actualHeight = decodeOptions.outHeight;
// 2. 求出根据给定的参数的目标宽度和长度
int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
actualWidth, actualHeight, mScaleType);
int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
actualHeight, actualWidth, mScaleType);
// 3. 再用目标宽度和长度decode bitmap data
decodeOptions.inSampleSize =
findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
Bitmap tempBitmap =
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
// 4. 再次进行downscale
if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
tempBitmap.getHeight() > desiredHeight)) {
bitmap = Bitmap.createScaledBitmap(tempBitmap,
desiredWidth, desiredHeight, true);
tempBitmap.recycle();
} else {
bitmap = tempBitmap;
}
scale的主要逻辑在getResizedDimension()
和findBestSampleSize()
中,这里不做详述。
ImageLoader
ImageLoader在RequestQueue的基础上套了一层memory cache,具体逻辑和RequesQueue很像。
先来看一下它的初始化方法:
public ImageLoader(RequestQueue queue, ImageCache imageCache) {
mRequestQueue = queue;
mCache = imageCache;
}
要传入一个RequestQueue和ImageCache,ImageCache是一个接口,代码如下所示:
public interface ImageCache {
public Bitmap getBitmap(String url);
public void putBitmap(String url, Bitmap bitmap);
}
不适用NetworkImageVIew的情况下,可以通过ImageLoader.get(String, ImageListener)
来发起图片资源请求,get方法的代码如下所示:
public ImageContainer get(String requestUrl, final ImageListener listener) {
return get(requestUrl, listener, 0, 0);
}
最终调用:
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight, ScaleType scaleType) {
// get方法必须在主线程上运行
throwIfNotOnMainThread();
// 1. 生成cache key
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
// 2. 检查mem cache是否命中
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
// 命中直接返回ImageContainer,并调用回调
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container, true);
return container;
}
// 3. mem cache miss,新建ImageContainer
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener);
// 4. 回调listener,这里应该显示default image
imageListener.onResponse(imageContainer, true);
// 5. 检查是否有相同request在执行
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
// If it is, add this request to the list of listeners.
request.addContainer(imageContainer);
return imageContainer;
}
// 6. 新建ImageRequeue,放到Request中,其后步骤跟单独发起ImageRequest相同
Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
cacheKey);
mRequestQueue.add(newRequest);
// 7. 标记当前执行的cache key
mInFlightRequests.put(cacheKey,
new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
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()
的代码如下所示:
protected void onGetImageSuccess(String cacheKey, Bitmap response) {
// 8. 放到mem cache中
mCache.putBitmap(cacheKey, response);
// 9. request完成,清楚标记
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
if (request != null) {
// 10. 更新BatchedImageRequest中的bitmap
request.mResponseBitmap = response;
// Send the batched response
batchResponse(cacheKey, request);
}
}
再来看一下batchResponse()
:
private void batchResponse(String cacheKey, BatchedImageRequest request) {
mBatchedResponses.put(cacheKey, request);
if (mRunnable == null) { // 在batch request被清空前只会进来一次
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;// 将runnable设为null才可以开始下一批reponse
}
};
// Post the runnable.
mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
}
}
可以看出,batchResponse()的作用是确保某一个batch的第一个response能够在mBatchResponseDelayMs时间间隔后在主线程上执行。
onGetImageError
的代码类似,这里不展开了。
NetworkImageView
使用NetworkImageVIew,非常方便:
mNetworkImageView.setImageUrl(IMAGE_URL, mImageLoader);
我们从setImageUrl开始分析。NetworkImageView继承于ImageView:
public class NetworkImageView extends ImageView {
private String mUrl;
private int mDefaultImageId;
private int mErrorImageId;
private ImageLoader mImageLoader;
private ImageContainer mImageContainer;
...
}
setImageUrl()的代码如下所示:
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);
}
调用了loadImageIfNecessary()
方法:
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;
}
// 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;
}
// 可以通过设置null url来清空imageview显示和cancel request
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
setDefaultImageOrNull();
return;
}
// 1. 如果当前view上有request正在执行,那么
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
if (mImageContainer.getRequestUrl().equals(mUrl)) {
// 如果url相同,那么忽略这次请求
return;
} else {
// 如果url不同,那么取消前一次
mImageContainer.cancelRequest();
setDefaultImageOrNull();
}
}
// 2. 计算maxWidth和maxHeight。如果view设置了LayoutParams.WRAP_CONTENT,那么不对maxWidth和maxHeight作限制
int maxWidth = wrapWidth ? 0 : width;
int maxHeight = wrapHeight ? 0 : height;
// 3. 调用ImageLoader的get方法发起image request
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) {
// 防止在layout过程中调用requestLayout方法
if (isImmediate && isInLayoutPass) {
post(new Runnable() {
@Override
public void run() {
onResponse(response, false);
}
});
return;
}
// 4. 拿到bitmap后,设置上去
if (response.getBitmap() != null) {
setImageBitmap(response.getBitmap());
} else if (mDefaultImageId != 0) {
setImageResource(mDefaultImageId);
}
}
}, maxWidth, maxHeight, scaleType);
// update the ImageContainer to be the new bitmap container.
mImageContainer = newContainer;
}
可见NetworkImageView也是利用了ImageLoader的get方法来实现图片下载。NetworkImageView的方便之处在于让我们不用手动管理图片资源的生命周期,和显示图片的状态切换(default, error状态)。
最后,看一下NetworkImageView自动回收资源是怎么实现的:
@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();
}
关键在于onDetachedFromWindow()的调用时机。由这篇文章可以了解到,onDetachedFromWindow()会在view被销毁,不再显示的时候调用。所以这样子做可以确保不会在view还在显示的状态下回收image资源。