一、前言

上篇文章我们分析了网络请求,这篇文章分析对图片的处理操作,如果没看上一篇,可以先看上一篇文章Volley 源码解析之网络请求。Volley 不仅仅对请求网络数据作了良好的封装,还封装了对图片的加载等操作,虽然比不上 GlideFresco,不过可以满足我们的日常使用,从学习者的角度看看是怎么封装的。
上篇文章我们分析了网络请求,这篇文章分析对图片的处理操作,如果没看上一篇,可以先看上一篇文章Volley 源码解析之网络请求。Volley 不仅仅对请求网络数据作了良好的封装,还封装了对图片的加载等操作,虽然比不上 GlideFresco,不过可以满足我们的日常使用,从学习者的角度看看是怎么封装的。

二、简单使用

  1. 使用 ImageRequest 加载图片,用法跟请求网络的用法差不多,只是构造request的参数不太一样:

    1
    2
    3
    4
    5
    6
    7
    String imageUrl = "https://pic1.zhimg.com/80/1a60ca062a1fe2f6d091cdd9749e9c68_hd.jpg";
    RequestQueue queue = Volley.newRequestQueue(this);
    ImageRequest imageRequest = new ImageRequest(imageUrl,
    response -> imageView.setImageBitmap(response),
    0, 0, ImageView.ScaleType.CENTER_CROP, Bitmap.Config.ARGB_8888,
    error -> {});
    queue.add(imageRequest);
    • 第一个参数是图片地址没啥说的
    • 第二个参数是成功的回调,返回一个bitmap
    • 第三和第四个参数则是图片的最大的高度和宽度,0为默认图片大小,如果填写的图片最大的高度和宽度小于图片的实际尺寸则会进行压缩
    • 第五个值就是对图片进行边界缩放
    • 第六个参数是图片的格式,常用的就是 RGB_565ARGB_8888,前者每个像素占2个字节,后者每个像素占4个字节,后者成像质量高,有alpha通道,如果使用的是jpg,不需要alpha通道则可以使用前者; 还有个 ARGB_4444,不过已经废弃了,在4.4以后默认转成 ARGB_8888ALPHA_8 只有透明度,没有颜色值,一般很少使用
    • 最后个参数就是错误的回调
  2. 使用 ImageLoader 加载图片

    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
    String imageUrl = "https://pic1.zhimg.com/80/1a60ca062a1fe2f6d091cdd9749e9c68_hd.jpg";
    RequestQueue queue = Volley.newRequestQueue(this);
    ImageLoader imageLoader = new ImageLoader(queue, new BitmapCache());
    ImageLoader.ImageListener imageListener = ImageLoader.getImageListener(imageView,
    R.mipmap.ic_launcher, R.mipmap.ic_launcher_round);
    imageLoader.get(imageUrl,
    imageListener, 0, 0);
    private class BitmapCache implements ImageLoader.ImageCache{
    private LruCache<String, Bitmap> lruCache;

    public BitmapCache() {
    int maxSize = 10 * 1024 * 1024;
    lruCache = new LruCache<String, Bitmap>(maxSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
    return bitmap.getRowBytes() * bitmap.getHeight();
    }
    };
    }

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

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
    lruCache.put(url, bitmap);
    }
    }
  3. 使用 NetworkImageView 加载图片

    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
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:layout_marginTop="88dp"
    android:layout_marginEnd="8dp"
    android:text="Button"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

    <com.android.volley.toolbox.NetworkImageView
    android:id="@+id/imageView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:layout_marginTop="32dp"
    android:layout_marginEnd="8dp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/textView"
    tools:srcCompat="@tools:sample/avatars" />
    </android.support.constraint.ConstraintLayout>
    1
    2
    3
    4
    5
    String imageUrl = "https://pic1.zhimg.com/80/1a60ca062a1fe2f6d091cdd9749e9c68_hd.jpg";
    NetworkImageView networkImageView = findViewById(R.id.imageView);
    networkImageView.setDefaultImageResId(R.mipmap.ic_launcher);
    networkImageView.setErrorImageResId(R.mipmap.ic_launcher_round);
    networkImageView.setImageUrl(imageUrl, imageLoader);

三、源码分析

一、 ImageRequest 分析

首先我们分析 ImageRequest,直接分析这个类,代码不多,直接继承 Request,那么不用说跟上一篇我们分析的网络请求的request 大体相同,不同的是这个是请求图片,如果我们需要自定义大小那么这里就对图片进行了裁剪以满足我们的大小:

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
public class ImageRequest extends Request<Bitmap> {
//图片请求的超时时间,单位毫秒
public static final int DEFAULT_IMAGE_TIMEOUT_MS = 1000;

//图片请求的默认重试次数
public static final int DEFAULT_IMAGE_MAX_RETRIES = 2;

//发生冲突时的默认重传延迟增加数,和TCP协议有关系,退避算法,短时间的重复请求失败还会是失败
public static final float DEFAULT_IMAGE_BACKOFF_MULT = 2f;

//对mListener加锁,保证线程安全,避免取消的时候同时执行分发
private final Object mLock = new Object();

@GuardedBy("mLock")
@Nullable
private Response.Listener<Bitmap> mListener;

private final Config mDecodeConfig;
private final int mMaxWidth;
private final int mMaxHeight;
private final ScaleType mScaleType;

//Bitmap 的同步解析锁,保证一个时间内只有一个Bitmap被加载到内存进行解析,避免多个同时解析oom
private static final Object sDecodeLock = new Object();


public ImageRequest(
String url,
Response.Listener<Bitmap> listener,
int maxWidth,
int maxHeight,
ScaleType scaleType,
Config decodeConfig,
@Nullable Response.ErrorListener errorListener) {
super(Method.GET, url, errorListener);
setRetryPolicy(
new DefaultRetryPolicy(
DEFAULT_IMAGE_TIMEOUT_MS,
DEFAULT_IMAGE_MAX_RETRIES,
DEFAULT_IMAGE_BACKOFF_MULT));
mListener = listener;
mDecodeConfig = decodeConfig;
mMaxWidth = maxWidth;
mMaxHeight = maxHeight;
mScaleType = scaleType;
}

@Deprecated
public ImageRequest(
String url,
Response.Listener<Bitmap> listener,
int maxWidth,
int maxHeight,
Config decodeConfig,
Response.ErrorListener errorListener) {
this(
url,
listener,
maxWidth,
maxHeight,
ScaleType.CENTER_INSIDE,
decodeConfig,
errorListener);
}

@Override
public Priority getPriority() {
return Priority.LOW;
}

//根据ScaleType设置图片大小
private static int getResizedDimension(
int maxPrimary,
int maxSecondary,
int actualPrimary,
int actualSecondary,
ScaleType scaleType) {

// 如果主要值和次要的值为0,就返回实际值,如果我们计算宽度的期望值,
那么主要值就是宽度,高度就是次要值,反之亦然
if ((maxPrimary == 0) && (maxSecondary == 0)) {
return actualPrimary;
}

// 如果为ScaleType.FIT_XY,填充整个矩形,忽略比值;
即如果主要的值为0则返回实际值,否则返回传入的值
if (scaleType == ScaleType.FIT_XY) {
if (maxPrimary == 0) {
return actualPrimary;
}
return maxPrimary;
}

// 如果主要的值为0,则通过比例值计算出主要的值返回
if (maxPrimary == 0) {
double ratio = (double) maxSecondary / (double) actualSecondary;
return (int) (actualPrimary * ratio);
}

// 次要的值为0,下面的比例调整就是多余的,那么直接返回主要的值,
if (maxSecondary == 0) {
return maxPrimary;
}

// 图片真实尺寸大小的比例,通过这个比例我们可以计算出次要的最大值,通
过计算出的值和我们传递进来的值做比较
double ratio = (double) actualSecondary / (double) actualPrimary;
int resized = maxPrimary;

// 如果是ScaleType.CENTER_CROP,填充整个矩形,保持长宽比,这里的宽高值相等或者大于传入的宽高尺寸
if (scaleType == ScaleType.CENTER_CROP) {
// 小于传入的次要最大值,则返回通过比例计算的最大值,这里相当于把resized 值增大
if ((resized * ratio) < maxSecondary) {
resized = (int) (maxSecondary / ratio);
}
return resized;
}
// 其它scaleType值,如果计算的值大于次要值,那么resized 值减小
if ((resized * ratio) > maxSecondary) {
resized = (int) (maxSecondary / ratio);
}
return resized;
}

//解析response
@Override
protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
synchronized (sDecodeLock) {
try {
return doParse(response);
} catch (OutOfMemoryError e) {
VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
return Response.error(new ParseError(e));
}
}
}

//解析的地方
private Response<Bitmap> doParse(NetworkResponse response) {
byte[] data = response.data;
BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
Bitmap bitmap = null;
//如果最大宽度和最大高度都传入的为0,直接解析成一个bitmap
if (mMaxWidth == 0 && mMaxHeight == 0) {
decodeOptions.inPreferredConfig = mDecodeConfig;
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
} else {
//如果要调整图片的大小,首先获取图片真实的尺寸大小,首先设置inJustDecodeBounds为true,不加载到内存但是可以获取图像的宽高
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);

// 计算出采样值,2的倍数
decodeOptions.inJustDecodeBounds = false;
decodeOptions.inSampleSize =
findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

// 如果采样率计算出的值为1的话,那么就没有尺寸压缩,tempBitmap的宽高值就是图片的
真实值,那么这里就需要缩放到满足我们上面计算出来的值
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));
}
}

@Override
public void cancel() {
super.cancel();
synchronized (mLock) {
mListener = null;
}
}

@Override
protected void deliverResponse(Bitmap response) {
Response.Listener<Bitmap> listener;
synchronized (mLock) {
listener = mListener;
}
if (listener != null) {
listener.onResponse(response);
}
}

//计算合适的采样率
@VisibleForTesting
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 的高效加载。首先我们获取到返回的 response 进行解析,然后根据传递的期望宽高以及图片格式生成 Bitmap 返回,对于我们传入的宽高会按比例裁剪,不是直接使用裁剪到合适的值,不然会有拉伸,最后再回调给用户。

二、ImageLoader 分析

我们直接先看构造方法,看所有的关键地方,不重要的就不分析了

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

没啥说的,就是赋值,一个是请求队列,一个是图片缓存的自己实现,这个是在内部把请求添加到请求队列,所以直接传递进去,第二个参数缓存,我们可以自己实现,一般使用 LruCache 实现。
接下来我们接着看 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 进行设置图像,然后分别提供一个默认和请求失败的占位图,刚开始设置的时候还没有请求到 Bitmap,所以最开始设置的事默认图。
首先看两个变量,后面需要用到:

1
2
3
4
//相同URL正在请求中存储的map
private final HashMap<String, BatchedImageRequest> mInFlightRequests = new HashMap<>();
//相同URL请求结果存储的map
private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<>();

接下来我们看看关键的一步

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
    @MainThread
public ImageContainer get(
String requestUrl,
ImageListener imageListener,
int maxWidth,
int maxHeight,
ScaleType scaleType) {

// 检查当前线程是否在主线程,只满足从主线程发起的请求
Threads.throwIfNotOnMainThread();

//根据url、width、height、scaleType拼接的缓存key
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);

// 从缓存中查找bitmap
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
// 有相应的缓存那么则返回一个ImageContainer,包括其中的bitmap
ImageContainer container =
new ImageContainer(
cachedBitmap, requestUrl, /* cacheKey= */ null, /* listener= */ null);
// 直接调用onResponse,把bitmap设置给imageView
imageListener.onResponse(container, true);
return container;
}

// 缓存中没有查找到,那么我们直接获取,首先new一个ImageContainer
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener);

// 更新调用的地方,使用默认的图片先设置
imageListener.onResponse(imageContainer, true);
//检查是否有相同的cacheKey请求正在运行
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
// 如果相同的请求正在运行,那么不需要重复请求,只需要将这个实例化
的imageContainer添加到BatchedImageRequest的mContainers中,然后请
求结束后对所有添加到集合中的imageContainer依次回调
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;
}

这个方法是一个重点,主要流程是首先看看缓存里面有没有缓存的 Bitmap,来源于我们自己实现的缓存策略,我们使用的是内存缓存的话这里就是一级缓存;如果有直接调用 onResponse 方法设置图片,如果没有,首先实例化 ImageContainer,涉及到了几个类,接下来就看是否有相同的请求,如果有则添加到一个集合中,请求下来统一处理;如果没有那么则构造一个 Request,通过 RequestQueue 去获取网络图片,可能是网络请求也有可能是磁盘缓存的,这里就是二级缓存,然后添加到正在请求的集合中。
接下来看一看 ImageContainer 这个类,这个类就是图像请求的一个容器对象

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
public class ImageContainer {
//imageview加载的Bitmap
private Bitmap mBitmap;

//图片加载成功和失败的监听
private final ImageListener mListener;

//缓存的key
private final String mCacheKey;

//请求指定的URL
private final String mRequestUrl;

public ImageContainer(
Bitmap bitmap, String requestUrl, String cacheKey, ImageListener listener) {
mBitmap = bitmap;
mRequestUrl = requestUrl;
mCacheKey = cacheKey;
mListener = listener;
}

//取消请求
@MainThread
public void cancelRequest() {
Threads.throwIfNotOnMainThread();

if (mListener == null) {
return;
}
//从正在请求的集合获取一个批处理request
BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
if (request != null) {
//如果取到request,那么首先从mContainers移除当前的这个ImageContainer,
如果移除后集合为空一个ImageContainer也没有了,那么则取消掉这个请求并返回 true
boolean canceled = request.removeContainerAndCancelIfNecessary(this);
if (canceled) {
//取消了请求则从正在请求的集合中移除BatchedImageRequest
mInFlightRequests.remove(mCacheKey);
}
} else {
// 如果已经请求完成添加到批处理中准备处理分发
request = mBatchedResponses.get(mCacheKey);
if (request != null) {
//首先从mContainers移除当前的这个ImageContainer
request.removeContainerAndCancelIfNecessary(this);
if (request.mContainers.size() == 0) {
//如果集合中一个ImageContainer都没有,则从等待处理的
response中移除掉这个BatchedImageRequest
mBatchedResponses.remove(mCacheKey);
}
}
}
}

public Bitmap getBitmap() {
return mBitmap;
}

/** Returns the requested URL for this container. */
public String getRequestUrl() {
return mRequestUrl;
}
}

这个类主要包含是一个图片的容器对象,里面包括了 bitmap、监听器、缓存的key以及请求的 URL,每个请求都会先组装这个类,然后添加到一个 BatchedImageRequest的mContainers

接下来我们看看真正发起请求的地方:

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

//图片请求成功
protected void onGetImageSuccess(String cacheKey, Bitmap response) {
// 添加到以及缓存中
mCache.putBitmap(cacheKey, response);

// 从正在运行的请求列表中删除这个请求
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);

if (request != null) {
//更新BatchedImageRequest的bitmap
request.mResponseBitmap = response;
//发送一个批处理请求,将多个相同的请求进行分发
batchResponse(cacheKey, request);
}
}

//图片请求失败,跟上面成功处理大致类似
protected void onGetImageError(String cacheKey, VolleyError error) {
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);

if (request != null) {
//设置这个请求的错误
request.setError(error);

batchResponse(cacheKey, request);
}
}

这里执行网络请求还是调用我们上面分析的 ImageRequest 方法,而且在回调中分别对成功和失败在进行了一次处理。
接下来我们看看这个批量处理图片的 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
34
35
36
37
private void batchResponse(String cacheKey, BatchedImageRequest request) {
//首先添加到这个map中,表明现在进入了批处理中
mBatchedResponses.put(cacheKey, request);
// 如果还没有进行处理,那么我们则开始一个新的任务
if (mRunnable == null) {
mRunnable =
new Runnable() {
@Override
public void run() {
//循环mBatchedResponses的所有值
for (BatchedImageRequest bir : mBatchedResponses.values()) {
//循环BatchedImageRequest的mContainers的值
for (ImageContainer container : bir.mContainers) {
//如果有的请求取消了,在收到请求的
响应后还没有分发之前那么跳过循环下一个
if (container.mListener == null) {
continue;
}
// 如果不是请求错误则调用onResponse
if (bir.getError() == null) {
container.mBitmap = bir.mResponseBitmap;
container.mListener.onResponse(container, false);
} else {
//请求报错则调用onErrorResponse设置一个错误的图片展示 container.mListener.onErrorResponse(bir.getError());
}
}
}
//清除所有响应的BatchedImageRequest
mBatchedResponses.clear();
//置为null,通过是否为null判断当前是否正在处理
mRunnable = null;
}
};
// 将这个post投递到主线程去执行
mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
}
}

这段代码也很简单,不过有个地方有个比较奇怪的地方,为啥是使用双层循环,为啥不直接使用内层的循环;个人认为有可能是这样,首先这个 mBatchedResponses 刚开始进来添加了相同的key的请求的 BatchedImageRequest,那么存在正在分发的时候又有不同的key的请求进来了,因为正在处理的时候 runnable 不为 null,则后续添加的有可能不能分发,所以要遍历这个 map中所有的请求。

三、 NetworkImageView 分析

这是一个继承 ImageView 的自定义 view

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
public class NetworkImageView extends ImageView {
private String mUrl;
//设置默认的图片
private int mDefaultImageId;
//设置请求错误的时候显示的图片
private int mErrorImageId;

private ImageLoader mImageLoader;
private ImageContainer mImageContainer;

public NetworkImageView(Context context) {
this(context, null);
}

public NetworkImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public NetworkImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}

//这个方法就是设置我们的url
@MainThread
public void setImageUrl(String url, ImageLoader imageLoader) {
Threads.throwIfNotOnMainThread();
mUrl = url;
mImageLoader = imageLoader;
// 我们的url可能已经更改,那么我们则需要判断是否需要加载
loadImageIfNecessary(/* isInLayoutPass= */ false);
}

public void setDefaultImageResId(int defaultImage) {
mDefaultImageId = defaultImage;
}

public void setErrorImageResId(int errorImage) {
mErrorImageId = errorImage;
}

//如果视图尚未加载图像,那么我们则去加载它
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;
}

//如果不知道视图的大小并且不是WRAP_CONTENT就暂停加载
boolean isFullyWrapContent = wrapWidth && wrapHeight;
if (width == 0 && height == 0 && !isFullyWrapContent) {
return;
}

// 如果url为空的话则请取消所有的请求,包括以前的请求,假如请求两次
最后次的url为null,这时候还没请求完成,肯定以最后次为准
if (TextUtils.isEmpty(mUrl)) {
if (mImageContainer != null) {
mImageContainer.cancelRequest();
mImageContainer = null;
}
//设置默认的图片
setDefaultImageOrNull();
return;
}

// 检查是否取消以前的请求
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
if (mImageContainer.getRequestUrl().equals(mUrl)) {
// 如果请求和以前相同则没必要再次请求
return;
} else {
// 如果存在正在请求的url并且请求url不同,那么取消正在请求的url
mImageContainer.cancelRequest();
setDefaultImageOrNull();
}
}

// 计算最大宽高,如果设置WRAP_CONTENT那么则图片是多大就是多大,其它
情况则直接使用布局的宽高,如果设置了具体的值就有可能裁剪
int maxWidth = wrapWidth ? 0 : width;
int maxHeight = wrapHeight ? 0 : height;

// 使用ImageLoader来请求图像,上面已经分析了,最终返回一个ImageContainer
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) {
//isImmediate:在网络请求过程中调用的时候为true,可以用来
区分是否是取的缓存图像还是网络图像加载
isInLayoutPass:如果通过onLayout调用此函数,
则为true,否则为false
这个if的意思就是,如果是缓存图像并且是在布局中调用那么则发送
到主线程并延迟设置图像,因为可能多次调用
if (isImmediate && isInLayoutPass) {
post(
new Runnable() {
@Override
public void run() {
onResponse(response, /* isImmediate= */ false);
}
});
return;
}
//请求成功加载图片
if (response.getBitmap() != null) {
setImageBitmap(response.getBitmap());
} else if (mDefaultImageId != 0) {
setImageResource(mDefaultImageId);
}
}
},
maxWidth,
maxHeight,
scaleType);
}

private void setDefaultImageOrNull() {
if (mDefaultImageId != 0) {
setImageResource(mDefaultImageId);
} else {
setImageBitmap(null);
}
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
loadImageIfNecessary(/* isInLayoutPass= */ true);
}

//当imageview销毁的时候,取消请求并且清除ImageContainer以便重新加载图像
@Override
protected void onDetachedFromWindow() {
if (mImageContainer != null) {
// If the view was bound to an image request, cancel it and clear
// out the image from the view.
mImageContainer.cancelRequest();
setImageBitmap(null);
// also clear out the container so we can reload the image if necessary.
mImageContainer = null;
}
super.onDetachedFromWindow();
}

@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
invalidate();
}
}

这个类没啥好分析的,就是利用前两个类来完成请求,只不过方便的是直接在xml中使用,使用 ImageLoader 请求的 Bitmap 设置给 NetworkImageView

四、总结

三种不同的方式都可以完成图片的加载,不过后面的方式都比较依赖前面的 ImageRequest,毕竟还是要这个类去完成网络请求操作;在使用中,根据不同的场景选择不同的方式使用。不过我建议使用 ImageLoader 来加载图片,可以自己设置缓存,两级缓存,一级内存缓存,一级 volley 请求时候的磁盘缓存。总体来讲封装的很不错,对一些细节处理的比较好,比如相同的请求、图片的裁剪等,值得我们学习的地方很多。

参考

Android Volley完全解析(二),使用Volley加载网络图片
Volley 源码解析