Android Volley库源码简析(Image Request部分)
本文仅对Volley中关于Image Request部分的一些简单用例做解析,Http Request部分请参考这里:Android Volley库源码简析(HTTP Request部分)
从常用case入手
用Volley请求图片有两种方式,通过ImageRequest或者是使用NetworkImageView。
使用ImageRequest
// 1. 新建一个queuemRequestQueue = 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和imagecachemImageLoader = 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传给NetworkImageViewmNetworkImageView.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操作。
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返回给Deliveryreturn 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 datadecodeOptions.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);// 4. 再次进行downscaleif (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() {@Overridepublic 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() {@Overridepublic 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自动回收资源是怎么实现的:
@Overrideprotected 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资源。