简单使用
ImageLoader是volley中用来加载图片的工具类。使用如下:
mImageLoader.get(IMAGE_URL, ImageLoader.getImageListener(mImageView,
R.drawable.def_image, R.drawable.err_image))
创建ImageLoader时,需要一个ImageCache对象,它用于图片的缓存。可以利用该对象实现内存以及本地文件缓存。
源码及注释
常用子类
public class ImageContainer {
/**
* 请求获取到的Bitmap对象
*/
private Bitmap mBitmap;
private final ImageListener mListener;// 请求的回调监听
/** 请求对应的key值 */
private final String mCacheKey;
/** 请求要访问的url */
private final String mRequestUrl;
它只是将发起的一个请求封装成一个类。这样能保证不同请求之间数据的准确性以及使用的方便性。
private class BatchedImageRequest {
/** 真正的要执行网络操作的Request */
private final Request<?> mRequest;
/** 网络请求结束后返回的Bitmap,这个bitmap是要展示在界面上的 */
private Bitmap mResponseBitmap;
/** Error if one occurred for this response */
private VolleyError mError;
/** 所有与mRequest相关的ImageContainer对象 */
private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
同一个cacheKey对应的请求可能会被发起多次,因此将后发起的请求统计起来,只发起一次网络请求,并等网络请求结束后对所有的请求进行统一处理,有利于减少网络访问次数节约流量。Volley中对所有的网络请求的处理方式都是这样的!
常用属性
/**
* HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so
* that we can coalesce multiple requests to the same URL into a single network request.
*/
private final HashMap<String, BatchedImageRequest> mInFlightRequests =
new HashMap<String, BatchedImageRequest>();
缓存请求。这样可以将多次相同URL的请求合并成一次网络访问,减少对流量的消耗以及节约时间。
get方法
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight, ScaleType scaleType) {
// 非主线程就扔一个异常,因此get方法必须在主线程中调用
throwIfNotOnMainThread();
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
// 先在缓存中查找
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
// 缓存中有,则返回缓存中的数据
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container, true);
return container;
}
// 缓存中没有,则去获取
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener);
// 通知listener展示默认图片。listener分析在下面。
imageListener.onResponse(imageContainer, true);
// 有可能同一个请求会发生多次,因此需要将请求进行缓存分类。某个请求成功后,则将通知所有发起过的请求进行更新。
// 避免对同一个请求进行多次处理,造成流量浪费。
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) { //已有该请求,则将本次请求存储进来,等到请求结束后统一通知处理。
//
request.addContainer(imageContainer);
return imageContainer;
}
// 缓存中没有,并且该请求也没有正在进行,则新建一个ImageRequest去请求。
Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
cacheKey);
mRequestQueue.add(newRequest); // 与上步合在一起,执行网络请求获取数据。
mInFlightRequests.put(cacheKey,
new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
get方法主要对一个请求进行了三次处理:1,缓存中有,直接获取;2,缓存中没有,但已有同样cacheKey的请求正在执行,则存储该ImageContainer,并等待执行的请求执行完毕后统一处理;3,上述都不满足,那就用ImageRequest进行网络处理。
get并不是一个阻塞式的方法,它会很迅速地返回一个ImageContainer。而NetworkImageView正是利用这一点解决图片错位的。
makeImageRequest方法只是返回一个ImageRequest对象,如:
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);
}
});
}
onGetImageRequest()
protected void onGetImageSuccess(String cacheKey, Bitmap response) {
// 缓存得到的结果
mCache.putBitmap(cacheKey, response);
// 请求完成后,将请求从缓存中移除
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
if (request != null) {
// 更新请求中存储的bitmap
request.mResponseBitmap = response;
// 把网络请求结果分发到各个请求中
batchResponse(cacheKey, request);
}
}
batchResponse
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);
}
}
先用一个Map存储,然后隔mBatchResponseDelayMs后再执行runnable——不知道为啥这么做。
在Runnable中,通过两次循环会取出所有的ImageContainer,然后调用其中封装的listener将结果返回。
getImageListener
在get方法中需要一个ImageListener对象,可以通过getImageListener获取。该listener是得到要显示的Bitmap后进行显示操作的。
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);
}
}
};
}
通过上面的get请求走下来,imageContainer中的bitmap有可能为null,此时就显示默认的图片。如果正常网络请求成功的话,imageContainer中的bitmap不为null,就正常显示即可。
ImageLoader并不能保证加载ListView时图片不错位。因为listview加载时,ImageView是复用的,这导致了getImageListener中的ImageView是对应多个请求的。解决办法参考。
总结
同一个URL的多次请求,必须进行缓存。
NetworkImageView
使用ImageLoader加载图片时,ImageView复用会导致图片错位。可以使用上述的方法解决,也可以使用volley中提供的NetworkImageView。
使用
通过实例调用setImageUrl即可,在ListView等存在imageView复用的控件中,需要不停地调用setImageUrl。
源码及注释
setImageUrl内部核心方法就是调用loadImageIfNecessary,如下:
void loadImageIfNecessary(final boolean isInLayoutPass) {
//关于宽高的判断,略
// 新设置的url为空,但该imageview在本次调用setImageUrl之前已经开始加载别的url,所以需要取消原来的加载,显示成默认的。
// 因为本次url为空,所以肯定加载不成功。
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
setDefaultImageOrNull();
return;
}
// 本次setImageUrl之前已经开始加载过,就根据实际情况判断是否要取消。
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
if (mImageContainer.getRequestUrl().equals(mUrl)) {
// 本次Url与以前url相同,不进行取消,因为这不会导致错位。
return;
} else {
// 本次Url与原来的不同,所以需要取消原来的加载——避免图片错位。
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;
// 执行本次的setImageUrl的加载——走到这步时已经取消了上次的加载,因此不会导致错位问题。
// get方法不是阻塞式的,即调用第一次setImageUrl时就会有ImageContainer返回,并不会因为
// 阻塞或者多线程导致上面关于mImageContainer非空判断失效,从而又导致错位问题。
// 并且通过ImageLoader的get方法可以知道,get返回的ImageContainer
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, scaleType);
// update the ImageContainer to be the new bitmap container.
mImageContainer = newContainer;
}
NetworkImageView给出了一种解决图片错位的思路:取消旧有的加载。