Gituhb项目
Volley源码中文注释项目我已经上传到github,欢迎大家fork和start.
为什么写这篇博客
本来文章是维护在github上的,但是我在分析ImageLoader源码过程中与到了一个问题,希望大家能帮助解答.
Volley获取网络图片
本来想分析Universal Image Loader的源码,但是发现Volley已经实现了网络图片的加载功能.其实,网络图片的加载也是分几个步骤:
1. 获取网络图片的url.
2. 判断该url对应的图片是否有本地缓存.
3. 有本地缓存,直接使用本地缓存图片,通过异步回调给ImageView进行设置.
4. 无本地缓存,就先从网络拉取,保存在本地后,再通过异步回调给ImageView进行设置.
我们通过Volley源码,看一下Volley是否是按照这个步骤实现网络图片加载的.
ImageRequest.java
按照Volley的架构,我们首先需要构造一个网络图片请求,Volley帮我们封装了ImageRequest类,我们来看一下它的具体实现:
@SuppressWarnings("unused")
public class ImageRequest extends Request<Bitmap> {
public static final int DEFAULT_IMAGE_REQUEST_MS = 1000;
public static final int DEFAULT_IMAGE_MAX_RETRIES = 2;
private final Response.Listener<Bitmap> mListener;
private final Bitmap.Config mDecodeConfig;
private final int mMaxWidth;
private final int mMaxHeight;
private ImageView.ScaleType mScaleType;
private static final Object sDecodeLock = new Object();
public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
ImageView.ScaleType scaleType, Bitmap.Config decodeConfig,
Response.ErrorListener errorListener) {
super(Method.GET, url, errorListener);
mListener = listener;
mDecodeConfig = decodeConfig;
mMaxWidth = maxWidth;
mMaxHeight = maxHeight;
mScaleType = scaleType;
}
@Override
public Priority getPriority() {
return Priority.LOW;
}
@Override
protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
synchronized (sDecodeLock) {
try {
return doParse(response);
} catch (OutOfMemoryError e) {
return Response.error(new VolleyError(e));
}
}
}
private Response<Bitmap> doParse(NetworkResponse response) {
byte[] data = response.data;
BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
Bitmap bitmap;
if (mMaxWidth == 0 && mMaxHeight == 0) {
decodeOptions.inPreferredConfig = mDecodeConfig;
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
} else {
// 获取网络图片的真实尺寸.
decodeOptions.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
int actualWidth = decodeOptions.outWidth;
int actualHeight = decodeOptions.outHeight;
int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
actualWidth, actualHeight, mScaleType);
int desireHeight = getResizedDimension(mMaxWidth, mMaxHeight,
actualWidth, actualHeight, mScaleType);
decodeOptions.inJustDecodeBounds = false;
decodeOptions.inSampleSize =
findBestSampleSize(actualWidth, actualHeight, desiredWidth, desireHeight);
Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
tempBitmap.getHeight() > desireHeight)) {
bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desireHeight, true);
tempBitmap.recycle();
} else {
bitmap = tempBitmap;
}
}
if (bitmap == null) {
return Response.error(new VolleyError(response));
} else {
return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
}
}
static int findBestSampleSize(
int actualWidth, int actualHeight, int desiredWidth, int desireHeight) {
double wr = (double) actualWidth / desiredWidth;
double hr = (double) actualHeight / desireHeight;
double ratio = Math.min(wr, hr);
float n = 1.0f;
while ((n * 2) <= ratio) {
n *= 2;
}
return (int) n;
}
private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
int actualSecondary, ImageView.ScaleType scaleType) {
// 如果没有设置ImageView的最大值,则直接返回网络图片的真实大小.
if ((maxPrimary == 0) && (maxSecondary == 0)) {
return actualPrimary;
}
// 如果ImageView的ScaleType为FIX_XY,则将其设置为图片最值.
if (scaleType == ImageView.ScaleType.FIT_XY) {
if (maxPrimary == 0) {
return actualPrimary;
}
return maxPrimary;
}
if (maxPrimary == 0) {
double ratio = (double)maxSecondary / (double)actualSecondary;
return (int)(actualPrimary * ratio);
}
if (maxSecondary == 0) {
return maxPrimary;
}
double ratio = (double) actualSecondary / (double) actualPrimary;
int resized = maxPrimary;
if (scaleType == ImageView.ScaleType.CENTER_CROP) {
if ((resized * ratio) < maxSecondary) {
resized = (int)(maxSecondary / ratio);
}
return resized;
}
if ((resized * ratio) > maxSecondary) {
resized = (int)(maxSecondary / ratio);
}
return resized;
}
@Override
protected void deliverResponse(Bitmap response) {
mListener.onResponse(response);
}
}
因为Volley本身框架已经实现了对网络请求的本地缓存,所以ImageRequest做的主要事情就是解析字节流为Bitmap,再解析过程中,通过静态变量保证每次只解析一个Bitmap防止OOM,使用ScaleType和用户设置的MaxWidth和MaxHeight来设置图片大小.
总体来说,ImageRequest的实现非常简单,这里不做过多的讲解.ImageRequest的缺陷在于:
1.需要用户进行过多的设置,包括图片的大小的最大值.
2.没有图片的内存缓存,因为Volley的缓存是基于Disk的缓存,有对象反序列化的过程.
ImageLoader.java
鉴于以上两个缺点,Volley又提供了一个更牛逼的ImageLoader类.其中,最关键的就是增加了内存缓存.
再讲解ImageLoader的源码之前,需要先介绍一下ImageLoader的使用方法.和之前的Request请求不同,ImageLoader并不是new出来直接扔给RequestQueue进行调度,它的使用方法大体分为4步:
•创建一个RequestQueue对象.
RequestQueue queue = Volley.newRequestQueue(context);
•创建一个ImageLoader对象.
ImageLoader构造函数接收两个参数,第一个是RequestQueue对象,第二个是ImageCache对象(也就是内存缓存类,我们先不给出具体实现,讲解完ImageLoader源码之后,我会提供一个利用LRU算法的ImageCache实现类)
ImageLoader imageLoader = new ImageLoader(queue, new ImageCache() {
@Override
public void putBitmap(String url, Bitmap bitmap) {}
@Override
public Bitmap getBitmap(String url) { return null; }
});
•获取一个ImageListener对象.
ImageListener listener = ImageLoader.getImageListener(imageView, R.drawable.default_imgage, R.drawable.failed_image);
•调用ImageLoader的get方法加载网络图片.
imageLoader.get(mImageUrl, listener, maxWidth, maxHeight, scaleType);
有了ImageLoader的使用方法,我们结合使用方法来看一下ImageLoader的源码:
@SuppressWarnings({"unused", "StringBufferReplaceableByString"})
public class ImageLoader {
private final RequestQueue mRequestQueue;
private final ImageCache mCache;
private final HashMap<String, BatchedImageRequest> mInFlightRequests =
new HashMap<String, BatchedImageRequest>();
private final HashMap<String, BatchedImageRequest> mBatchedResponses =
new HashMap<String, BatchedImageRequest>();
private final Handler mHandler = new Handler(Looper.getMainLooper());
private Runnable mRunnable;
public interface ImageCache {
Bitmap getBitmap(String url);
void putBitmap(String url, Bitmap bitmap);
}
public ImageLoader(RequestQueue queue, ImageCache imageCache) {
mRequestQueue = queue;
mCache = imageCache;
}
public static ImageListener getImageListener(final ImageView view, final int defaultImageResId,
final int errorImageResId) {
return new ImageListener() {
@Override
public void onResponse(ImageContainer response, boolean isImmediate) {
if (response.getBitmap() != null) {
view.setImageBitmap(response.getBitmap());
} else if (defaultImageResId != 0) {
view.setImageResource(defaultImageResId);
}
}
@Override
public void onErrorResponse(VolleyError error) {
if (errorImageResId != 0) {
view.setImageResource(errorImageResId);
}
}
};
}
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight, ScaleType scaleType) {
// 判断当前方法是否在UI线程中执行.如果不是,则抛出异常.
throwIfNotOnMainThread();
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
// 从L1级缓存中根据key获取对应的Bitmap.
Bitmap cacheBitmap = mCache.getBitmap(cacheKey);
if (cacheBitmap != null) {
// L1缓存命中,通过缓存命中的Bitmap构造ImageContainer,并调用imageListener的响应成功接口.
ImageContainer container = new ImageContainer(cacheBitmap, requestUrl, null, null);
// 注意:因为目前是在UI线程中,因此这里是调用onResponse方法,并非回调.
imageListener.onResponse(container, true);
return container;
}
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener);
// L1缓存命中失败,则先需要对ImageView设置默认图片.然后通过子线程拉取网络图片,进行显示.
imageListener.onResponse(imageContainer, true);
// 检查cacheKey对应的ImageRequest请求是否正在运行.
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
// 相同的ImageRequest正在运行,不需要同时运行相同的ImageRequest.
// 只需要将其对应的ImageContainer加入到BatchedImageRequest的mContainers集合中.
// 当正在执行的ImageRequest结束后,会查看当前有多少正在阻塞的ImageRequest,
// 然后对其mContainers集合进行回调.
request.addContainer(imageContainer);
return imageContainer;
}
// L1缓存没命中,还是需要构造ImageRequest,通过RequestQueue的调度来获取网络图片
// 获取方法可能是:L2缓存(ps:Disk缓存)或者HTTP网络请求.
Request<Bitmap> newRequest =
makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey);
mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
private String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) {
return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
.append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url)
.toString();
}
public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
}
private boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) {
throwIfNotOnMainThread();
String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
return mCache.getBitmap(cacheKey) != null;
}
protected Request<Bitmap> makeImageRequest(final String requestUrl, int maxWidth, int maxHeight,
ScaleType scaleType, final String cacheKey) {
return new ImageRequest(requestUrl, new Response.Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
onGetImageSuccess(cacheKey, response);
}
}, maxWidth, maxHeight, scaleType, Bitmap.Config.RGB_565, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onGetImageError(cacheKey, error);
}
});
}
private void onGetImageError(String cacheKey, VolleyError error) {
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
if (request != null) {
request.setError(error);
batchResponse(cacheKey, request);
}
}
protected void onGetImageSuccess(String cacheKey, Bitmap response) {
// 增加L1缓存的键值对.
mCache.putBitmap(cacheKey, response);
// 同一时间内最初的ImageRequest执行成功后,回调这段时间阻塞的相同ImageRequest对应的成功回调接口.
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
if (request != null) {
request.mResponseBitmap = response;
// 将阻塞的ImageRequest进行结果分发.
batchResponse(cacheKey, request);
}
}
private void batchResponse(String cacheKey, BatchedImageRequest request) {
mBatchedResponses.put(cacheKey, request);
if (mRunnable == null) {
mRunnable = new Runnable() {
@Override
public void run() {
for (BatchedImageRequest bir : mBatchedResponses.values()) {
for (ImageContainer container : bir.mContainers) {
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, 100);
}
}
private void throwIfNotOnMainThread() {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
}
}
public interface ImageListener extends Response.ErrorListener {
void onResponse(ImageContainer response, boolean isImmediate);
}
public class ImageContainer {
private Bitmap mBitmap;
private final String mCacheKey;
private final String mRequestUrl;
private final ImageListener mListener;
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;
}
BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
if (request != null) {
boolean canceled = request.removeContainerAndCancelIfNecessary(this);
if (canceled) {
mInFlightRequests.remove(mCacheKey);
}
} else {
request = mBatchedResponses.get(mCacheKey);
if (request != null) {
request.removeContainerAndCancelIfNecessary(this);
if (request.mContainers.size() == 0) {
mBatchedResponses.remove(mCacheKey);
}
}
}
}
public Bitmap getBitmap() {
return mBitmap;
}
public String getRequestUrl() {
return mRequestUrl;
}
}
private class BatchedImageRequest {
private final Request<?> mRequest;
private Bitmap mResponseBitmap;
private VolleyError mError;
private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
public BatchedImageRequest(Request<?> request, ImageContainer container) {
mRequest = request;
mContainers.add(container);
}
public VolleyError getError() {
return mError;
}
public void setError(VolleyError error) {
mError = error;
}
public void addContainer(ImageContainer container) {
mContainers.add(container);
}
public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
mContainers.remove(container);
if (mContainers.size() == 0) {
mRequest.cancel();
return true;
}
return false;
}
}
}
重大疑问
个人对Imageloader的源码有两个重大疑问?
•batchResponse方法的实现.
我很奇怪,为什么ImageLoader类里面要有一个HashMap来保存BatchedImageRequest集合呢?
private final HashMap<String, BatchedImageRequest> mBatchedResponses =
new HashMap<String, BatchedImageRequest>();
毕竟batchResponse是在特定的ImageRequest执行成功的回调中被调用的,调用代码如下:
protected void onGetImageSuccess(String cacheKey, Bitmap response) {
// 增加L1缓存的键值对.
mCache.putBitmap(cacheKey, response);
// 同一时间内最初的ImageRequest执行成功后,回调这段时间阻塞的相同ImageRequest对应的成功回调接口.
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
if (request != null) {
request.mResponseBitmap = response;
// 将阻塞的ImageRequest进行结果分发.
batchResponse(cacheKey, request);
}
}
从上述代码可以看出,ImageRequest请求成功后,已经从mInFlightRequests中获取了对应的BatchedImageRequest对象.而同一时间被阻塞的相同的ImageRequest对应的ImageContainer都在BatchedImageRequest的mContainers集合中.
那我认为,batchResponse方法只需要遍历对应BatchedImageRequest的mContainers集合即可.
但是,ImageLoader源码中,我认为多余的构造了一个HashMap对象mBatchedResponses来保存BatchedImageRequest集合,然后在batchResponse方法中又对集合进行两层for循环各种遍历,实在是非常诡异,求指导.
诡异代码如下:
private void batchResponse(String cacheKey, BatchedImageRequest request) {
mBatchedResponses.put(cacheKey, request);
if (mRunnable == null) {
mRunnable = new Runnable() {
@Override
public void run() {
for (BatchedImageRequest bir : mBatchedResponses.values()) {
for (ImageContainer container : bir.mContainers) {
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, 100);
}
}
我认为的代码实现应该是:
private void batchResponse(String cacheKey, BatchedImageRequest request) {
if (mRunnable == null) {
mRunnable = new Runnable() {
@Override
public void run() {
for (ImageContainer container : request.mContainers) {
if (container.mListener == null) {
continue;
}
if (request.getError() == null) {
container.mBitmap = request.mResponseBitmap;
container.mListener.onResponse(container, false);
} else {
container.mListener.onErrorResponse(request.getError());
}
}
mRunnable = null;
}
};
// Post the runnable
mHandler.postDelayed(mRunnable, 100);
}
}
•使用ImageLoader默认提供的ImageListener,我认为存在一个缺陷,即图片闪现问题.当为ListView的item设置图片时,需要增加TAG判断.因为对应的ImageView可能已经被回收利用了.
自定义L1缓存类
首先说明一下,所谓的L1和L2缓存分别指的是内存缓存和硬盘缓存.
实现L1缓存,我们可以使用Android提供的Lru缓存类,示例代码如下:
import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
@SuppressWarnings("unused")
public class ImageLruCache implements ImageLoader.ImageCache {
private LruCache<String, Bitmap> mLruCache;
public ImageLruCache() {
this((int) Runtime.getRuntime().maxMemory() / 8);
}
public ImageLruCache(final int cacheSize) {
createLruCache(cacheSize);
}
private void createLruCache(final int cacheSize) {
mLruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
};
}
@Override
public Bitmap getBitmap(String url) {
return mLruCache.get(url);
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
mLruCache.put(url, bitmap);
}
}
您可能感兴趣的文章:Android 中Volley二次封装并实现网络请求缓存Android Volley框架全面解析Android Volley框架使用方法详解Android中Volley框架进行请求网络数据的使用Android开发中使用Volley库发送HTTP请求的实例教程Android的HTTP操作库Volley的基本使用教程Android的HTTP类库Volley入门学习教程Android Volley框架使用源码分享Android 开发中Volley详解及实例Android中volley封装实践记录