[Android] Volley库源码简析(Image Request部分)

*** 本文仅对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资源。