盒子
盒子
文章目录
  1. 前言
  2. ImageLoader
    1. get
    2. makeImageRequest
    3. onGetImageSuccess && onGetImageError
    4. batchResponse
    5. 默认的getImageListener实现
    6. 实现自己的ImageListener接口
    7. ImageCache
  3. NetWorkImageView
  4. 吐槽
  5. 总结

Android Volley源码分析(二)

前言

上一次分析了关于Volley最核心的原理,对Volley如何处理网络请求做了一个全面的剖析。还没有了解的可以先了解一下Android Volley源码分析(一),这次我再来对它的图片加载原理进行自我讲解,希望能帮助到一部分人,也能巩固自己所学的知识,不足之处欢迎指出!

ImageLoader

对于图片请求,是少不了对ImageLoader的使用,它是一个图片装载器,对图片请求的管理。下面来看下使用方式:

1
private static final ImageLoader imageLoader = new ImageLoader(VolleyUtil.volleyQueue, new ImageCache(CACHE_SIZE));

它有两个参数分别为RequestQueueImageCache,对于第一个参数我们再熟悉不过了,至于第二个参数是为了构建图片缓存的,如不构建则缓存自然就失效了。现在我们进入ImageLoader查看它的源码,看它到底做了什么?

1
2
3
4
public ImageLoader(RequestQueue queue, ImageCache imageCache) {
mRequestQueue = queue;
mCache = imageCache;
}

进去发现它提供了一个get方法,对图片的处理就是在这里,如果使用过Volley图片加载的都知道我们都要调用它的get的方法,那么它到底又做了什么呢?

get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight, ScaleType scaleType) {

// only fulfill requests that were initiated from the main thread.
throwIfNotOnMainThread();

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) {
// 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 =
new ImageContainer(null, requestUrl, cacheKey, imageListener);

// Update the caller to let them know that they should use the default bitmap.
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.
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);

mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey,
new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}

根据上面的源码,它传递了几个参数,其中包括请求的url、监听器ImageListener、返回的图片宽高与缩放类型,ImageListener是一个接口,是为了对响应不同处理的回调,包括onResponseonErrorResponse。下面看主要代码,首先判断该方法调用是否在主线程,不是则抛出异常,对于对主线程的判断应该很容易想到,因为要对请求的响应做处理更新图片等,对UI的操作都要在主线程,所以自然要在主线程进行更新。下一步都是一贯的作风,(10行代码)先判断缓存中是否存在该请求的Bitmap,如果存在则构造一个ImageContainer,将必要信息传入,这是一个容器用来保存图片的信息,包括

  • Bitmap请求的图片
  • ImageListener 监听器,包括后续回调的onResponseonErrorResponse方法
  • mCacheKey 缓存的key
  • mRequestUrl 请求的url

然后调用imageListeneronResponse方法。不存在与会为其创建一个ImageContainer为了保存后续的Bitmpa,同时我们会发现它也会先调用onResponse这一步并不是设置加载的图片,而是设置默认正在加载的图片(如果你设置了的话)。(25行代码)如果该请求正在被执行,将当前的ImageContainer加入到一个LinkedList中为le后续响应的递送;下面才是真在从网络上获取(34行代码)构造了一个Request并将其加入到了请求队列中,下面我们来看下这个makeImageRequest实现了什么

makeImageRequest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}
});
}

现在明白了吧,其实就是使用了ImageRequest,与StringRequest类似,在上一篇文章Android Volley源码分析(一)已经讲了,不管是直接使用缓存还是网络请求,在最后都会将结果交由DeliveryResponse进行递送,根据响应情况分别调用监听器的onResponseonErrorResponse方法。那么其中的onGetImageSuccessonGetImageError方法又做了什么呢?

onGetImageSuccess && onGetImageError

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}

做的事无非就是将Bitmap加入缓存中同时更新请求中的Bitmap以便后续batchResponse的批处理操作

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void onGetImageError(String cacheKey, VolleyError error) {
// Notify the requesters that something failed via a null result.
// Remove this request from the list of in-flight requests.
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);

if (request != null) {
// Set the error for this request
request.setError(error);

// Send the batched response
batchResponse(cacheKey, request);
}
}

onGetImageError也类似,只不过是错误的响应无需加入到缓存中,但还是要进行batchResponse,既然如此那我们继续进入batchResponse源码。

batchResponse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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);
}
}

通过代码发现其中有一个run方法,里面根据图片容器ImageContainer中的信息分别调用监听器中的onResponseonErrorResponse方法,这就是我们前面get中所传递的ImageListener接口。其中方法的实现可以由我们自己来实现。最后通过mHandler(作用在主线程)进行处理。
对于ImageListener接口,也有默认的实现getImageListener

默认的getImageListener实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}
}
};
}

在这里我们能看到我们最喜欢看到的代码:对ImageView进行图片填充。但我们只使用默认的实现可能会存在许多问题,例如多条目的图片加载会复用item如果不对其进行特殊处理会很容易产生多图片加载时的闪烁与异位问题。那要如何解决呢?其实我们要做的就是不让他直接就处理返回的结果,而是要进行特殊的判断,既然如此,我们只有自己实现接口中的方法,自己来实现业务逻辑。

实现自己的ImageListener接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static ImageLoader.ImageListener getImageListener(final ImageView view, final String url, final int defaultImageResId
, final int errorImageResId) {
return new ImageLoader.ImageListener() {
@Override
public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) {
if (response.getBitmap() != null) {
//防止未设置默认加载前的显示图片而导致的图片错乱问题
String preUrl = (String) view.getTag();
if (preUrl != null && preUrl.trim().equals(url)) {
if (!isImmediate && defaultImageResId != 0) {
//使用网络下载的图片
TransitionDrawable transitionDrawable = new TransitionDrawable(
new Drawable[]{
App.mContext.getResources().getDrawable(defaultImageResId),
new BitmapDrawable(App.mContext.getResources(),
response.getBitmap())
}
);
transitionDrawable.setCrossFadeEnabled(true);
view.setImageDrawable(transitionDrawable);
transitionDrawable.startTransition(100);
} else {
//使用缓存图片
view.setImageBitmap(response.getBitmap());
}
}
} else {
if (defaultImageResId != 0)
view.setImageResource(defaultImageResId);
}
}

@Override
public void onErrorResponse(VolleyError error) {
if (errorImageResId != 0)
view.setImageResource(errorImageResId);
}
};
}

如代码中的注释所示,对View其进行设置Tag可以是url。只有符合当前位置的url链接的Bitmap才能填充当前的View,其实要完美解决是有一个前提的,设置了默认的显示图片,如果你不设置默认图片的显示,虽然异位的情况解决了,但可能还是会导致图片的闪烁。只不过现在图片加载都会设置默认的图片显示,所以基本就不存这种问题了。

ImageCache

关于缓存在ImageLoader中提供了ImageLoader.ImageCache接口,就两个方法getBitmapputBitmap,如果细心的话前面对缓存的处理都是通过这两个方法来获取与保存的。所以要实现缓存的话就必须实现该接口,并实现其中的方法。不过对缓存的管理我们可以继承LruCache算法来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ImageCache extends LruCache<String, Bitmap> implements ImageLoader.ImageCache {
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other caches,
* this is the maximum sum of the sizes of the entries in this cache.
*/

public ImageCache(int maxSize) {
super(maxSize);
}

@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}

@Override
public Bitmap getBitmap(String url) {
return get(url);
}

@Override
public void putBitmap(String url, Bitmap bitmap) {
put(url, bitmap);
}
}

对于缓存maxSize的大小可以取内存阀值的1/8

1
2
private static final int CACHE_SIZE = 1024 * 1024 * ((ActivityManager) App.mContext.
getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass() / 8;

NetWorkImageView

Volley还提供了一个专门的网络图片加载的控件,可以直接将xml文件中的ImageView替换成NetWorkImageView,该控件也是继承了ImageView只不过它对专门对使用Volley图片加载进行了封装。使用时通过setDefaultImageResIdsetErrorImageResId设置默认与错误显示图片,调用则是直接setImageUrl,还是来看下源码

1
2
3
4
5
6
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);
}

参数url与我们上面提到的ImageLoader,说明不管使用哪种方式ImageLoader都是必须的。再看下loadImageIfNecessary做了什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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;
}

// if the URL to be loaded in this view is empty, cancel any old requests and clear the
// currently loaded image.
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
setDefaultImageOrNull();
return;
}

// if there was an old request in this view, check if it needs to be canceled.
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
if (mImageContainer.getRequestUrl().equals(mUrl)) {
// if the request is from the same URL, return.
return;
} else {
// if there is a pre-existing request, cancel it if it's fetching a different 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;

// The pre-existing content of this view didn't match the current URL. Load the new image
// from the network.
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;
}

代码比较多,但都是很容易理解的,前面一部分都是对宽度与高度的计算,使图片能适应控件的大小。我们先来找到我们所熟悉的(48行代码),它就是前面的get方法以及ImageListener的实现,这里相信现在不用多说了,它调用的源码都在前面讲了。再向上走(32行代码)发现没,它将当前NetWorkImageView中的ImageContainer中保存的url与请求的url相比较,是不是与前面所说的防闪烁与异位的处理相似呢?你看,如果url相同的话说明当前控件上所显示的图片与结果吻合,就直接返回不做处理;否则mImageContainer.cancelRequest()取消原来的请求,并且setDefaultImageOrNull(),该方法的作用看方法名就知道:如果设置了默认图片就显示默认图片,没有就设置空图片不显示。后面的处理就是上面的get处理了,再进行网络请求。所以NetWorkImageView使用起来相对于更简单,更容易上手。

吐槽

其实上面的两种方式加载图片最终都是调用的ImageRequest,而ImageRequest是继承与Request,所以还是走到了前面那篇文章所讲的内容。同理再去看StringRequestJsonObjectRequestJsonArrayRequest都是类似的,都是重写deliverResponseparseNetworkResponse方法实现不同数据类型的请求。同样这两个方法又回到了Android Volley源码分析(一)中所分析的内容。

总结

这里对Volley加载图片的使用进行简单的概括

  • 创建RequestQueue
  • 创建ImageLoader
  • 实现缓存。继承LruCache实现ImageLoaderImageLoader.ImageCache接口
  • 如果使用ImageView则实现自己的ImageLoader.ImageListener,调用get方法
  • 如果使用NetWorkImageView则直接调用setImageUrl方法
支持一下
赞赏是一门艺术