前言
在上一篇文章中,我们一起深入探究了 Volley 的缓存机制,通过源码分析对缓存的工作原理进行了了解,这篇文章将带大家一起探究「Volley 图片加载的实现」,图片加载跟缓存还是有比较紧密的联系的,建议大家先去看下:Android Volley 源码解析(二),探究缓存机制。
这是 Volley 源码解析系列的最后一篇文章,今天我们通过以基本用法和源码分析相结合的方式来进行,当然本文的源码还是建立在第一篇源码分析的基础上的,还没有看过这篇文章的朋友,建议先去阅读:Android Volley 源码解析(一),网络请求的执行流程。
一、图片加载的基本用法
在进行源码解析之前,我们先来看一下 Volley 中有关图片加载的基本用法。
1.1 ImageRequest 的用法
ImageRequest 和 StringRequest 以及 JsonRequest 都是继承自 Request,因此他们的用法也基本是相同的,首先需要获取一个 RequestQueue 对象:
RequestQueue mQueue = Volley.newRequestQueue(context);
复制代码
接着 new 出一个 ImageRequest 对象:
private static final String URL = "http://ww4.sinaimg.cn/large/610dc034gw1euxdmjl7j7j20r2180wts.jpg";
ImageRequest imageRequest = new ImageRequest(URL, new Response.Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
imageView.setImageBitmap(response);
}
}, 0, 0, ImageView.ScaleType.CENTER_CROP, Bitmap.Config.RGB_565, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
}
});
复制代码
可以看到 ImageRequest 接收六个参数:
1、图片的 URL 地址
2、图片请求成功的回调,这里我们将返回的 Bitmap 设置到 ImageView 中
3、4 分别用于指定允许图片最大的宽度和高度,如果指定的网络图片的宽度或高度大于这里的值,就会对图片进行压缩,指定为 0 的话,表示不管图片有多大,都不进行压缩
5、指定图片的属性,Bitmap.Config 下的几个常量都可以使用,其中 ARGB_8888 可以展示最好的颜色属性,每个图片像素像素占 4 个字节,RGB_565 表示每个图片像素占 2 个字节
6、图片请求失败的回调
最后将这个 ImageRequest 添加到 RequestQueue 就行了
mQueue.add(imageRequest);
复制代码
1.2 ImageLoader 的用法
ImageLoader 其实是对 ImageRequest 的封装,它不仅可以帮我们对图片进行缓存,还可以过滤掉重复的链接,避免重复发送请求,因此 ImageLoader 要比 ImageRequest 更加高效。
ImageLoader 的用法,主要分为以下四步:
1、创建 RequestQueue 对象 2、创建一个 ImageLoader 对象 3、获取一个 ImageListener 对象 4、调用 ImageLoader 的 get() 方法记载图片
RequestQueue requestQueue = Volley.newRequestQueue(this);
ImageLoader imageLoader = new ImageLoader(requestQueue, new ImageLoader.ImageCache() {
@Override
public Bitmap getBitmap(String url) {
return null;
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
}
});
ImageLoader.ImageListener listener = ImageLoader.getImageListener(mIvShow, R.mipmap.ic_launcher, R.mipmap.ic_launcher_round);
imageLoader.get(URL, listener);
复制代码
可以看到 ImageLoader 的构造函数接收两个参数,第一个参数就是 RequestQueue 对象,第二个参数是 ImageCache,我们这里直接 new 出一个空的 ImageCache 实现就行了。
在 ImageListener 中传入所加载图片的 URL,以及图片占位符和加载失败后显示的图片,最后调用 ImageLoader.get() 方法便能进行图片的加载。
1.3 NetworkImageView
除了以上两种方式之外,Volley 还提供了第三种方式来加载网络图片,NetworkImageView 是一个继承自 ImageView 的自定义 View,在 ImageView 的基础上拓展加载网络图片的功能。NetworkImageView 的用法还是比较简单的。大致可以分为 4 步:
1、创建一个 RequestQueue 对象 2、创建一个 ImageLoader 对象 3、在代码中获取 NetworkImageView 的实例 4、设置要加载的图片地址
如下所示:
RequestQueue requestQueue = Volley.newRequestQueue(this);
ImageLoader imageLoader = new ImageLoader(requestQueue, new ImageLoader.ImageCache() {
@Override
public Bitmap getBitmap(String url) {
return null;
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
}
});
networkImageView.setImageUrl(URL, imageLoader);
复制代码
二、ImageRequest 源码解析
在上一节中介绍了 Volley 图片加载的三种方法,从这节开始我们结合源码来分析 Volley 中图片加载的实现,就从 ImageRequest 开始吧。
我们在 Android Volley 源码解析(一),网络请求的执行流程 这篇文章中讲到,网络请求最终会将从服务器返回的结果封装成 NetworkResponse 然后传给 Request 进行处理。而 ImageRequest 的工作,其实就是将 NetworkResponse 解析成包含 Bitmap 的 Response,最后再回调出去。
我们要进行分析的,也就是这个过程。
可以看到 parseNetworkResponse 中只有一个 doParse() 方法
@Override
protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
synchronized (sDecodeLock) {
try {
return doParse(response);
} catch (OutOfMemoryError e) {
return Response.error(new ParseError(e));
}
}
}
复制代码
就让我们看看 doParse() 里面究竟进行了什么操作
private Response<Bitmap> doParse(NetworkResponse response) {
byte[] data = response.data;
BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
Bitmap bitmap = null;
if (mMaxWidth == 0 && mMaxHeight == 0) {
decodeOptions.inPreferredConfig = mDecodeConfig;
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
} else {
// ① 获取 Bitmap 原始的宽和高
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 desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
actualHeight, actualWidth, mScaleType);
// ③ 根据我们想要的宽和高得到对应的 Bitmap
decodeOptions.inJustDecodeBounds = false;
decodeOptions.inSampleSize =
findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
Bitmap tempBitmap =
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
// ④ 如果 Bitmap 不为 bull 而且宽或高大于目标宽高的话,再一次压缩
if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
tempBitmap.getHeight() > desiredHeight)) {
bitmap = Bitmap.createScaledBitmap(tempBitmap,
desiredWidth, desiredHeight, true);
tempBitmap.recycle();
} else {
bitmap = tempBitmap;
}
}
// ⑤ 将得到的 包含 Bitmap 的 Response 回调出去
if (bitmap == null) {
return Response.error(new ParseError(response));
} else {
return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
}
}
复制代码
代码比较长,我们分为 5 步来看
① 获取 Bitmap 原始的宽和高
通过 BitmapFactory 将传入的 NetworkResponse 中的 data 转换成对应的 Bitmap,然后通过设置 BitmapOptions.inJustDecodeBounds = true,得到 Bitmap 的原始宽和高,这里补充一下,当 BitmapOptions.inJustDecodeBounds = true 的时候,BitmapFactory.decode 并不会真的返回一个 bitmap 给你,它仅仅会把一些图片的大小信息(如宽和高)返回给你,而不会占用太多的内存。
② 计算我们真正想要的宽和高
应该还记得我们构建 ImageRequest 的时候传入的参数吧,那 6 个参数里面,包含两个分别指定图片最大宽和高的参数,我们将传入的图片最大宽和高以及 Bitmap 真实的宽和高,通过 getResizedDemension() 方法计算出比较合适的图片显示宽高,代码如下:
private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
int actualSecondary, ScaleType scaleType) {
if ((maxPrimary == 0) && (maxSecondary == 0)) {
return actualPrimary;
}
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 == ScaleType.CENTER_CROP) {
if ((resized * ratio) < maxSecondary) {
resized = (int) (maxSecondary / ratio);
}
return resized;
}
if ((resized * ratio) > maxSecondary) {
resized = (int) (maxSecondary / ratio);
}
return resized;
}
复制代码
③ 根据我们想要的宽和高得到对应的 Bitmap
DecodeOptions.inJustDecodeBounds = true 代表将一个真正的 Bitmap 返回给你, DecodeOptions.inSampleSize 代表图片的采样率,是跟图片压缩有关的参数,如果 inSampliSize = 2 则代表将原先图片的宽和高分别减小为原来的 1/2,以此类推。
decodeOptions.inJustDecodeBounds = false;
decodeOptions.inSampleSize =
findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
Bitmap tempBitmap =
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
复制代码
// 计算采样率的方法
static int findBestSampleSize(
int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
double wr = (double) actualWidth / desiredWidth;
double hr = (double) actualHeight / desiredHeight;
double ratio = Math.min(wr, hr);
float n = 1.0f;
while ((n * 2) <= ratio) {
n *= 2;
}
return (int) n;
}
复制代码
④ 如果 Bitmap 不为 bull 而且宽或高大于目标宽高的话,再一次压缩
if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
tempBitmap.getHeight() > desiredHeight)) {
bitmap = Bitmap.createScaledBitmap(tempBitmap,
desiredWidth, desiredHeight, true);
tempBitmap.recycle();
} else {
bitmap = tempBitmap;
}
复制代码
⑤ 将得到的包含 Bitmap 的 Response 回调出去
if (bitmap == null) {
return Response.error(new ParseError(response));
} else {
return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
}
复制代码
三、ImageLoader 源码解析
我们在上面说到 ImageLoader 的用法,主要分为四步:
1、创建 RequestQueue 对象 2、创建一个 ImageLoader 对象 3、获取一个 ImageListener 对象 4、调用 ImageLoader 的 get() 方法加载图片
那我们就从它的用法入手,一步一步分析究竟是怎么实现的。
创建 RequestQueue 在之前已经讲过,可以参考这篇文章:Android Volley 源码解析(一),网络请求的执行流程,我们看下 ImageLoader 的构造方法:
public ImageLoader(RequestQueue queue, ImageCache imageCache) {
mRequestQueue = queue;
mCache = imageCache;
}
复制代码
可以看到构造方法将 RequestQueue 和 ImageCache 赋值给当前实例的成员变量,我们接着看 ImageListener 获取,ImageListener 是通过 ImageLoader.getImageListener() 方法获取的:
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);
}
}
};
}
复制代码
可以看到在这里面主要是将回调出来的 Bitmap 设置给对应的 ImageView,以及做一些图片加载的容错处理。
最后重点来了,ImageLoader 的 get() 方法是 ImageLoader 类最复杂的方法,也是最核心的方法,我们一起来看看吧:
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight, ScaleType scaleType) {
// 如果当前不是在主线程就抛出异常(UI 操作必须在主线程进行)
throwIfNotOnMainThread();
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
// 从缓存中取出对应的 Bitmap,如果 Bitmap 不为 null,直接回调 imageListener 将 Bitmap 设置给 ImageView
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);
imageListener.onResponse(imageContainer, true);
// 判断该请求是否是否在缓存队列中
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
request.addContainer(imageContainer);
return imageContainer;
}
// 如果在缓存中并没有找到该请求,便进行一次网络请求,
Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
cacheKey);
mRequestQueue.add(newRequest);
// 将请求进行缓存
mInFlightRequests.put(cacheKey,
new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
复制代码
首先进行了当前线程的判断,如果不是主线程的话,就直接抛出错误。
private void throwIfNotOnMainThread() {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
}
}
复制代码
然后从缓存中取出对应的 Bitmap,如果 Bitmap 不为 null,直接回调 ImageListener 将 Bitmap 设置给对应的 ImageView。
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container, true);
return container;
}
复制代码
然后根据 Url 从缓存队列中取出 Request
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
request.addContainer(imageContainer);
return imageContainer;
}
复制代码
如果在缓存中并没有找到该请求,便进行一次网络请求
Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
cacheKey);
复制代码
可以看到 ImageLoader 调用了 makeImageReqeust() 方法来构建 Request,我们来看看他是怎么实现的:
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() 方法,将 Bitmap 进行缓存,以及将缓存队列中 cacheKey 对应的 BatchedImageRequest 移除掉,最后调用 batchResponse() 方法。
protected void onGetImageSuccess(String cacheKey, Bitmap response) {
mCache.putBitmap(cacheKey, response);
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
if (request != null) {
request.mResponseBitmap = response;
batchResponse(cacheKey, request);
}
}
复制代码
在 batchResponse() 方法中,在主线程里面将 Bitmap 回调给 ImageListner,然后将 Bitmap 设置给 ImageView,这样便完成了图片加载的全部过程。
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;
}
};
mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
}
}
复制代码
四、NetworkImageView 源码解析
NetworkImageView 是一个内部使用 ImageLoader 来进行加载网络图片的自定义 View,我们在上面提到,NetworkImageView 的使用方法主要分为四步:
1、创建一个 RequestQueue 对象 2、创建一个 ImageLoader 对象 3、在代码中获取 NetworkImageView 的实例 4、调用 setImageUrl() 方法来设置要加载的图片地址
其中最后一步是 NetworkImageView 的核心,我们来看看 setImageUrl() 内部是怎么实现的吧:
public void setImageUrl(String url, ImageLoader imageLoader) {
mUrl = url;
mImageLoader = imageLoader;
loadImageIfNecessary(false);
}
复制代码
只有简单的三行代码,想必主要的逻辑就在 loadImageIfNecessary() 这个方法里面,我们点进去看一下:
void loadImageIfNecessary(final boolean isInLayoutPass) {
// 如果 URL 为 null,则取消该请求
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
setDefaultImageOrNull();
return;
}
// 如果该 NetworkImageView 之前已经掉用过 setImageUrl(),
// 判断当前的 Url 跟之前请求的 URL 是否相同
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
if (mImageContainer.getRequestUrl().equals(mUrl)) {
return;
} else {
mImageContainer.cancelRequest();
setDefaultImageOrNull();
}
}
// 通过 ImageLoader 进行图片加载
mImageContainer = 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 (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);
}
复制代码
代码还是相对比较清晰的,先进行一些容错性的处理,然后调用 ImageLoader 来获取对应的 bitmap,最后将其设置给 NetworkImageView.
总结
Volley 源码解析系列,到这里就全部结束了,这是我写过最长的系列文章了,从一开始 Volley 源码的阅读,到之后的代码整理以及现在的文章输出,花了我差不多一个星期的时间,不过对于网络加载和图片加载有了更深的理解。能完整看到这里的都是真爱啊,谢谢大家了。