/ Android源码学习  

Volley 中的ImageLoader源码学习笔记

转载请附原文链接: Volley 中的ImageLoader源码学习笔记

本篇笔记建立在上一篇分析 Volley 源码的基础上,如果你对 Volley 的源码还不了解可以看看上一篇笔记, 传送门

在 Android 中还有一大头疼的问题就是图片的加载, Volley 中封装了一个 ImageLoader 的类可以用来加载图片。本篇笔记就来分析一下 Volley 是如何处理图片加载的。

主要涉及的类:

  • ImageLoader (Volley中的)
  • ImageRequest

创建 ImageLoader

使用首先要创建一个 ImageLoader 对象,传入一个 请求队列,和一个 ImageCache。

public ImageLoader(RequestQueue queue, ImageCache imageCache) {
mRequestQueue = queue;
mCache = imageCache;
}

ImageCache 是一个接口,里面有两个方法,一个从缓存中获取图片,另一个将图片放入缓存中。

public interface ImageCache {
public Bitmap getBitmap(String url);
public void putBitmap(String url, Bitmap bitmap);
}

作者在这里写了一个注释,建议使用 LruCache 来实现这个接口。

ImageCache 作为三级缓存中的第一级缓存,也就是内存缓存

获取图片

通过 get() 方法可以传入一个图片的 url 并通过 ImageListener 来获取图片。这里再提一嘴,Volley 的作者注释真的写得很清楚啊,基本上把逻辑都说的很明白。这个方法要求在主线程调用,因为显示图片需要在主线程嘛

public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight, ScaleType scaleType) {
// only fulfill requests that were initiated from the main thread.
// 如果不在主线程调用,抛异常
throwIfNotOnMainThread();
// 获取到缓存的 key(Volley 自己拼的字符串作为唯一的 key)
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
// Try to look up the request in the cache of remote images.
// 在一级缓存中去找图片
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
// 找到了图片,将图片放到 ImageContainer里面,回调监听
// Return the cached bitmap.
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container, true);
return container;
}
// The bitmap did not exist in the cache, fetch it!
// 到这里说明一级缓存里面是没有图片的,创建一个新的 ImageContainer
// 这时候按照三级缓存的步骤应该去磁盘上寻找图片
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener);
// Update the caller to let them know that they should use the default bitmap.
// 这里回调的原因,是为了告诉调用者没有在内存中找到图片
// 需要从本地或者网络获取图片,回调可以设置一个 placeholder 占位图
imageListener.onResponse(imageContainer, true);
// Check to see if a request is already in-flight.
// 检查是否已经有一个相同的请求正在等待处理(加载同一张图片)
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
// If it is, add this request to the list of listeners.
// 如果有,则将该 imageContainer 加入到这个请求的监听列表中去
request.addContainer(imageContainer);
return imageContainer;
}
// The request is not already in flight. Send the new request to the network and
// track it.
// 到这里说明没有重复的请求,那么就要创建一个新的请求了
Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,cacheKey);
// 添加到请求队列中,剩下的事情就跟上一篇分析Volley中的处理流程一样的
// 也就是剩下的两级缓存
mRequestQueue.add(newRequest);
// 添加到正在处理的集合中
mInFlightRequests.put(cacheKey,
new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}

如果没有在一级缓存中获取到图片会创建一个请求

protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight,
ScaleType scaleType, final String cacheKey) {
// 创建一个 ImageRequest
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);
}
});
}

当成功获取到图片后,首先把图片放到内存缓存中去

protected void onGetImageSuccess(String cacheKey, Bitmap response) {
// cache the image that was fetched.
mCache.putBitmap(cacheKey, response);
// remove the request from the list of in-flight requests.
// 把这个请求从正在处理的集合中移除
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
if (request != null) {
// Update the response bitmap.
// 获取到图片
request.mResponseBitmap = response;
// Send the batched response
batchResponse(cacheKey, request);
}
}

接下来就是将图片发送到各个监听中去。

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);
}
}

ImageContainer 图片数据容器

一次加载图片会出现多种情况,可能从三级缓存中的任意一级中获取,这样就会有多种数据,因此封装了一个 ImageContainer 来为一次单独的加载请求持有所有的数据。

  • mBitmap — 图片
  • mListener — 监听
  • mCacheKey — 缓存key
  • mRequestUrl — 请求url

有三个方法分别是获取图片,获取url,和取消请求。

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 {
// check to see if it is already batched for delivery.
request = mBatchedResponses.get(mCacheKey);
if (request != null) {
request.removeContainerAndCancelIfNecessary(this);
if (request.mContainers.size() == 0) {
mBatchedResponses.remove(mCacheKey);
}
}
}
}

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);
}
}
};
}

图片的处理

我们知道在 Android 中加载图片,很容易发生 OOM 的情况,所以对图片的处理是非常有必要的。在 ImageLoader 中似乎没有看到任何处理图片的代码。实际上是在 ImageRequest 里面处理的,在拿到 Response 后就对图片进行了处理。

有时候我们可能只需要展示100*100 大小的图片,但是原图可能非常大,如果全部加载到内存中必然是浪费了空间并且可能造成内存溢出。所以我们可以根据要展示的实际大小来缩放图片,这样可以节省很大一部分内存空间。

首先科普一个小技巧,我们知道可以通过 BitmapFactory 来创建一个 Bitmap 对象,但是 BitmapFactory 会预先将这个 Bitmap 加载到内存中,实际上我们还需要对 Bitmap 进行处理,这样占用了无效的内存。BitmapFactory 在创建的时候可以传入一个 Options 对象来改变一些参数。

BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
decodeOptions.inJustDecodeBounds = true;

这个对象有一个属性是 inJustDecodeBounds ,从字面理解是只解码边界。这个属性设置为 true 后,BitmapFactory 返回的 Bitmap 为 null,并没有解码,但是图片的宽高等属性是可以获取到的。

int actualWidth = decodeOptions.outWidth;
int actualHeight = decodeOptions.outHeight;

这个技巧可以允许我们在不占用内存的情况下获取到图片的实际宽高,从而进行缩放。Options 中还有一个参数

inSampleSize — 采样率,默认为1。表示从 n 个像素中获取一个像素

这个参数需要注意几点

  • 值越大,缩放越小。
  • 如果这个值小于1,在处理的时候会设置为1。(因为不可能从一个像素中取半个吧,囧)
  • 值尽可能接近 2 的倍数,因为如果不是 2 的倍数,在底层方法中会向下取值为 2 的倍数

知道这个技术后就可以通过图片的实际宽高和需要展示的宽高算出缩放比(采样率)

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 {
// If we have to resize this image, first get the natural bounds.
decodeOptions.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
int actualWidth = decodeOptions.outWidth;
int actualHeight = decodeOptions.outHeight;
// Then compute the dimensions we would ideally like to decode to.
// 计算调整后的图片尺寸(根据 ScaleType 等)
int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
actualWidth, actualHeight, mScaleType);
int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
actualHeight, actualWidth, mScaleType);
// Decode to the nearest power of two scaling factor.
decodeOptions.inJustDecodeBounds = false;
// TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
// decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
// 根据调整后的尺寸算出采样率
decodeOptions.inSampleSize =
findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
Bitmap tempBitmap =
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
// If necessary, scale down to the maximal acceptable size.
// 如果调整后的图片还是大于了目标尺寸,再缩放一次
if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
tempBitmap.getHeight() > desiredHeight)) {
bitmap = Bitmap.createScaledBitmap(tempBitmap,
desiredWidth, desiredHeight, true);
tempBitmap.recycle();
} else {
bitmap = tempBitmap;
}
}
if (bitmap == null) {
return Response.error(new ParseError(response));
} else {
return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
}
}

看看 Volley 是怎么计算目标尺寸的,这个方法传入五个参数

private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,int actualSecondary, ScaleType scaleType) {
// If no dominant value at all, just return the actual.
// 没有调整
if ((maxPrimary == 0) && (maxSecondary == 0)) {
return actualPrimary;
}
// If ScaleType.FIT_XY fill the whole rectangle, ignore ratio.
// 如果是 fit_xy ,则直接返回尺寸,忽略掉比率
if (scaleType == ScaleType.FIT_XY) {
if (maxPrimary == 0) {
return actualPrimary;
}
return maxPrimary;
}
// If primary is unspecified, scale primary to match secondary's scaling ratio.
// 如果最大的主值为空,用第二个值来计算比率,再计算尺寸
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.CENTER_CROP fill the whole rectangle, preserve aspect ratio.
// 如果缩放类型是 center_crop,则要保持宽高比
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;
}

最后是计算采样率的方法,用实际的宽高分别计算比率,取最小的那个值,并且尽量接近2的倍数。

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;
}

结语

通过学习 Volley 中的 ImageLoader 源码,对于图片的处理的理解更加深入,相信就算是自己来写图片加载,也不会有原来那么头疼。在本篇笔记中提到了 LruCache,这是 Android 官方提倡的缓存方式,因为现在对于软引用和弱饮用也会直接回收,不再可靠。在下一篇源码中将分析 LruCache 源码,看它是如果来解决缓存问题的。