02 ImageLoader 请求队列

01 基础框架
02 请求队列
03 三级缓存
04 图片加载
05 常见问题
06 项目源码

一、请求参数

为了追求单一职责原则,上篇博客将 ImageLoader 类拆出 3 个类,这种情况往往导致方法调用层次加深,参数传递多次。比如:

1
2
3
4
5
Client
——> ImageLoader.displayImage(url, imageView)
——> RequestThreadPool.addRequest(url, imageView)
——> Loader.load(url, imageView)
——> Loader.downloadImage(url)

其中,参数 url 和 imageView 就传递了多次。如果后期需求变更,要新增参数,比如新增一个回调监听器 listener,那么整个方法调用链上的代码都要大幅修改。这时,我们一般将多个参数封装在一个对象里,这样就可以减少很多修改操作。此处。我们将参数 url 和 imageView 封装在请求参数 BitmapRequest 类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BitmapRequest {
public ImageView imageView;
public String imageUri;
public BitmapRequest(ImageView imageView, String uri) {
this.imageView = imageView;
this.imageUri = uri;
imageView.setTag(uri);
}
// image view 的 tag 与 uri 是否相等
public boolean isImageViewTagValid() {
return imageView != null && imageView.getTag().equals(imageUri);
}
}

对应的方法调用链的参数也需要替换成 BitmapRequest,如 ImageLoader 的 displayImage 方法:

1
2
3
4
public void displayImage(String uri, ImageView imageView) {
BitmapRequest request = new BitmapRequest(imageView, uri);
mThreadPool.addRequest(request);
}

ImageLoader 将用户传递的各种参数封装为 BitmapRequest 对象并沿着方法调用依次传递。为了保证程序的一致性,BitmapRequest 也作为缓存的 key。ImageCache 代码更改如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ImageCache {
private LruCache<BitmapRequest, Bitmap> mImageCache;
// 代码省略...
public void put(BitmapRequest key, Bitmap value) {
mImageCache.put(key, value);
}
public Bitmap get(BitmapRequest key) {
return mImageCache.get(key);
}
public void remove(BitmapRequest key) {
mImageCache.remove(key);
}
}

避免啰嗦,就不再给出其他参数修改代码。

二、请求队列

在 RequestThreadPool 类中,我们使用了 ExecutorService.submit(new Runnable(){}) 方法。虽然使用了线程池,支持多个异步加载任务,但是我们无法控制它。比如取消加载任务,修改加载顺序等,而且我们这样的异步加载代码显得非常“丑陋”,阅读性差。具体代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class RequestThreadPool {
// 线程池,线程数量为 CPU 的数量
private ExecutorService mExecutorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors());
public void addRequest(final BitmapRequest request) {
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Loader.getInstance().load(request);
}
});
}
}

有没有更好的实现方式呢?当然有,而且 ANDROID 本身就提供了一个很棒的设计案例,那就是 Handler。那 Handler 的机制是什么样的呢?先来看下图。

在使用 Handler 的时候,在 Handler 所创建的线程需要维护一个唯一的 Looper 对象,每个线程对应一个 Looper,每个线程的 Looper 通过 ThreadLocal 来保证;Looper 对象的内部又维护有唯一的一个 MessageQueue,所以一个线程可以有多个 Handler,但是只能有一个 Looper 和一个 MessageQueue。

Message 在 MessageQueue 不是通过一个列表来存储的,而是将传入的 Message 存入到了上一个 Message 的 next 中,在取出的时候通过顶部的 Message 就能按放入的顺序依次取出 Message。

Looper 对象通过 loop() 方法开启了一个死循环,不断地从 looper 内的 MessageQueue 中取出 Message,然后通过 handler 将消息分发传回 handler 所在的线程。

阐述完 Handler 消息机制,接下来就是模仿它实现请求队列。如下图所示:

  • 首先将 RequestThreadPool 改名为 RequestQueue,相当于 Looper,其内部维护一个存储图片加载请求(BitmapRequest)的队列(LinkedBlockingQueue),向外部提供一个添加图片加载请求的接口(addRequest);
  • RequestQueue 再维护一个线程池(RequestDispatcher[]),数量默认为:CPU 核心数 + 1个分发线程数,其中每个线程 RequestDispatcher 相当于 Handler,不断地从请求队列中获取 BitmapRequest,并执行加载任务。
  • 实现 RequestDispatcher 类,在相应的条件下无限轮询请求队列(LinkedBlockingQueue),拿到具体图片加载请求(BitmapRequest)后去执行网络加载任务。

实现代码如下:

RequestDispatcher.java

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
public class RequestDispatcher extends Thread {
// 网络请求队列
private BlockingQueue<BitmapRequest> mBitmapRequestQueue;
public RequestDispatcher(BlockingQueue<BitmapRequest> queue) {
mBitmapRequestQueue = queue;
}
@Override
public void run() {
try {
while (!this.isInterrupted()) {
final BitmapRequest request = mBitmapRequestQueue.take();
if (request.isCancel) {
continue;
}
Loader.getInstance().load(request);
}
} catch (InterruptedException e) {
Log.i("", "### 请求分发器退出");
}
}
}

RequestDispatcher 是一个线程实现类,主要负责轮询请求队列,获取到实际请求后,调用 Loader 实例去加载网络图片。

RequestQueue.java

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
public class RequestQueue {
private BlockingQueue<BitmapRequest> mBitmapRequestQueue =
new LinkedBlockingQueue<>();
// 默认的核心数:CPU 核心数 + 1个分发线程数
public static int DEFAULT_CORE_NUM =
Runtime.getRuntime().availableProcessors() + 1;
private int mDispatcherNum;
private RequestDispatcher[] mDispatchers = null;
protected RequestQueue() {
this(DEFAULT_CORE_NUM);
}
protected RequestQueue(int coreNum) {
mDispatcherNum = coreNum;
}
// 维护线程池
private final void startDispatchers() {
mDispatchers = new RequestDispatcher[mDispatcherNum];
for (int i = 0; i < mDispatcherNum; i++) {
mDispatchers[i] = new RequestDispatcher(mBitmapRequestQueue);
mDispatchers[i].start();
}
}
public void start() {
stop();
startDispatchers();
}
public void stop() {
if (mDispatchers != null && mDispatchers.length > 0) {
for (int i = 0; i < mDispatchers.length; i++) {
mDispatchers[i].interrupt();
}
}
}
public void addRequest(BitmapRequest request) {
if (!mBitmapRequestQueue.contains(request)) {
mBitmapRequestQueue.add(request);
}
}
}

RequestQueue 维护一个请求队列和线程池,在添加加载请求之前必须调用 start 方法来初始化线程池并启动各个线程。

ImageLoader.java

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
public class ImageLoader {
// ImageLoader 实例
private static ImageLoader sInstance;
// 图片内存缓存
private ImageCache mImageCache = new ImageCache();
// 网络请求队列
private RequestQueue mImageQueue;
private ImageLoader() {}
public static ImageLoader getInstance() {
if (sInstance == null) {
synchronized (ImageLoader.class) {
if (sInstance == null) {
sInstance = new ImageLoader();
}
}
}
return sInstance;
}
public ImageLoader init() {
mImageQueue = new RequestQueue();
mImageQueue.start();
return this;
}
public ImageCache getCache() {
return mImageCache;
}
public void displayImage(String uri, ImageView imageView) {
BitmapRequest request = new BitmapRequest(imageView, uri);
mImageQueue.addRequest(request);
}
}

测试代码:

1
2
3
4
5
String url1 = "https://mtl.gzhuibei.com/images/img/19816/1.jpg";
String url2 = "https://mtl.gzhuibei.com/images/img/19816/5.jpg";
ImageLoader loader = ImageLoader.getInstance();
loader.init().displayImage(url1, imageView1);
loader.displayImage(url2, imageView2);

三、请求策略

在上面实现的代码中,加载请求会被封装成一个 Request 对象添加到请求队列中,默认情况下 ImageLoader 会按照先后顺序加载图片。但是现实中,我们可能需要最后添加到队列的请求先被执行。例如,在滚动 ListView 时,最后一项肯定是最晚被加载的,此时它却显示在屏幕上的,而其它优先被加载的请求却不在屏幕显示范围。当需求是在屏幕上显示的 Item View 的图片优先被加载,我们就需要 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
/**
* 加载策略接口
*/
public interface LoadPolicy {
int compare(BitmapRequest request1, BitmapRequest request2);
}
/**
* 顺序加载策略
*/
public class SerialPolicy implements LoadPolicy {
@Override
public int compare(BitmapRequest request1, BitmapRequest request2) {
return request1.serialNum - request2.serialNum;
}
}
/**
* 逆序加载策略
*/
public class ReversePolicy implements LoadPolicy {
@Override
public int compare(BitmapRequest request1, BitmapRequest request2) {
return request2.serialNum - request1.serialNum;
}
}

首先定义了一个 LoadPolicy 接口,在这个接口中有一个 compare 方法,用来对比两个请求。我们默认实现了顺序加载、逆序加载两个策略,因为每个请求都要有一个序列号,这个序列号以递增的形式增长,越晚加入队列的请求序列号越大,而我们的请求队列也必须是优先级队列;为实现对这些请求的排序处理,我们需要在图片加载请求类中实现 Comparable 接口。

使用 AtomicInteger 类给每一个请求分配一个序列号,再使用优先级队列(PriorityBlockingQueue)来维持图片加载队列,PriorityBlockingQueue 会根据 BitmapRequest 的 compare 策略来决定 BitmapRequest 的顺序。RequestQueue 内部会启动用户指定数量的线程来从请求队列中读取请求,分发线程不断地从队列中读取请求,然后进行图片加载处理。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RequestQueue {
// 请求队列 [ Thread-safe ]
private BlockingQueue<BitmapRequest> mBitmapRequestQueue =
new PriorityBlockingQueue<>();
// 请求的序列化生成器
private AtomicInteger mSerialNumGenerator = new AtomicInteger(0);
// 代码省略...
public void addRequest(BitmapRequest request) {
if (!mBitmapRequestQueue.contains(request)) {
request.serialNum = this.generateSerialNumber();
mBitmapRequestQueue.add(request);
}
}
// 为每个请求生成一个系列号
private int generateSerialNumber() {
return mSerialNumGenerator.incrementAndGet();
}
}

BitmapRequest 增加请求序列号(serialNum)和加载策略(mLoadPolicy),并实现 Comparable 接口中的 compareTo 方法。

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
public class BitmapRequest implements Comparable<BitmapRequest> {
// 代码省略...
/**
* 请求序列号
*/
public int serialNum = 0;
/**
* 加载策略
*/
public LoadPolicy mLoadPolicy = new SerialPolicy();
@Override
public int compareTo(BitmapRequest another) {
return mLoadPolicy.compare(this, another);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((imageUri == null) ? 0 : imageUri.hashCode());
result = prime * result + ((imageView == null) ? 0 : imageView.hashCode());
result = prime * result + serialNum;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
BitmapRequest other = (BitmapRequest) obj;
if (imageUri == null) {
if (other.imageUri != null)
return false;
} else if (!imageUri.equals(other.imageUri))
return false;
if (imageView == null) {
if (other.imageView != null)
return false;
} else if (!imageView.equals(other.imageView))
return false;
if (serialNum != other.serialNum)
return false;
return true;
}
}

向外部提供设置请求加载接口:

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
public class ImageLoader {
// 代码省略...
// 请求加载策略
private LoadPolicy mLoadPolicy;
public ImageLoader init() {
init(new SerialPolicy());
return this;
}
public ImageLoader init(LoadPolicy policy) {
mLoadPolicy = policy;
mImageQueue = new RequestQueue();
mImageQueue.start();
return this;
}
public void displayImage(String uri, ImageView imageView) {
BitmapRequest request = new BitmapRequest(imageView, uri);
request.mLoadPolicy = mLoadPolicy;
mImageQueue.addRequest(request);
}
}

我们在 init 方法中初始化了加载策略配置信息,后续我们额外提供配置参数,如加载中的图片、加载失败的图片、缓存策略等等,那么要重载多少个 init 方法?如果提供 setter 和 getter 方法,那么这些方法是不是都在 displayImage 方法调用之前调用?

四、配置参数


前文提到 ImageLoader 初始化配置参数是,无论是重载 init 方法还是使用多个 setter 方法,都会使得用户的使用成本很高。暴露过多函数,会让用户在每次调用函数时都要仔细选择,还要把握函数调用时机。比如在已经初始化了一个指定线程数量的线程池的情况下,用户再调用 setThreadCount 时应该如何处理呢?为此我们需要对程序做一些限制,让用户只能在初始化时配置这些参数。

要封装参数,还要考虑初始化顺序等问题,只像 BitmapRequest 那样简单的封装参数是不行的。我们可以使用 Builder 模式来构建一个不可变的配置对象,并且将这个对象注入到 ImageLoader 中,也就是说它只能在构建时设置各个参数,一旦你调用 build() 或者类似方法构建对象后,它的属性就不可再修改,因为它没有 setter 方法,字段也都是隐藏的,用户只能在初始化时一次性构建这个配置对象,然后注入给 ImageLoader,ImageLoader 根据配置对象进行初始化。这样,像 setThreadCount、setLoadPolicy 等方法就不需要出现在 ImageLoader 中了,用户可见的函数就会少很多,ImageLoader 的使用成本也随之降低了。

修改后的 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
public class ImageLoader {
// ...
// 图片加载配置对象
private ImageLoaderConfig mConfig;
public void init(ImageLoaderConfig config) {
mConfig = config;
mCache = mConfig.imageCache;
checkConfig();
mImageQueue = new RequestQueue(mConfig.threadCount);
mImageQueue.start();
}
private void checkConfig() {
if (mConfig == null) {
throw new RuntimeException("The config of SimpleImageLoader " +
"is Null, please call the init(ImageLoaderConfig " +
"config) method to initialize");
}
if (mConfig.loadPolicy == null) {
mConfig.loadPolicy = new SerialPolicy();
}
if (mCache == null) {
mCache = new ImageCache();
}
}
}

其中的参数设置代码都封装到了 ImageLoaderConfig 和 Builder 对象中,代码如下:

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
public class ImageLoaderConfig {
// 图片缓存配置对象
ImageCache imageCache = new ImageCache();
// 图片加载中显示的图片 id
int loadingImageId;
// 加载失败时显示的图片 id
int loadFailedImageId;
// 图片加载策略
LoadPolicy loadPolicy;
// 线程数量,默认为 CPU 数量 + 1
int threadCount = Runtime.getRuntime().availableProcessors() + 1;
private ImageLoaderConfig() {}
/**
* 配置类的 Builder
*/
public static class Builder {
// 图片缓存配置对象
ImageCache imageCache = new ImageCache();
// 图片加载中显示的图片 id
int loadingImageId;
// 加载失败时显示的图片 id
int loadFailedImageId;
// 图片加载策略
LoadPolicy loadPolicy;
// 线程数量,默认为 CPU 数量 + 1
int threadCount = Runtime.getRuntime().availableProcessors() + 1;
// 设置缓存
public Builder setCache(ImageCache imageCache) {
this.imageCache = imageCache;
return this;
}
// 设置正在加载中显示的图片
public Builder setLoadingImageId(int loadingImageId) {
this.loadingImageId = loadingImageId;
return this;
}
// 设置要加载的图片失败时显示的图片
public Builder setLoadFailedImageId(int loadingFailedImageId) {
this.loadFailedImageId = loadingFailedImageId;
return this;
}
// 设置加载策略
public Builder setLoadPolicy(LoadPolicy loadPolicy) {
this.loadPolicy = loadPolicy;
return this;
}
// 设置线程数量
public Builder setThreadCount(int threadCount) {
this.threadCount = threadCount;
return this;
}
void applyConfig(ImageLoaderConfig config) {
config.imageCache = this.imageCache;
config.loadingImageId = this.loadingImageId;
config.loadFailedImageId = this.loadFailedImageId;
config.loadPolicy = this.loadPolicy;
config.threadCount = this.threadCount;
}
public ImageLoaderConfig create() {
ImageLoaderConfig config = new ImageLoaderConfig();
applyConfig(config);
return config;
}
}
}

通过 ImageLoaderConfig 的构造函数、字段包级私有化,使得外部不能访问内部属性,用户唯一能够设置属性的地方就是通过 Builder 对象了,也就是说用户只能通过 Builder 对象构造 ImageLoaderConfig 对象,这就是构建和表示相分离。

用户的使用代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
String url1 = "https://mtl.gzhuibei.com/images/img/19816/1.jpg";
String url2 = "https://mtl.gzhuibei.com/images/img/19816/5.jpg";
ImageLoader loader = ImageLoader.getInstance();
ImageLoaderConfig config = new ImageLoaderConfig.Builder()
.setThreadCount(4)
.setCache(new ImageCache())
.setLoadPolicy(new ReversePolicy())
.setLoadingImageId(R.drawable.loading)
.setLoadFailedImageId(R.drawable.not_found)
.create();
loader.init(config);
loader.displayImage(url1, imageView1);
loader.displayImage(url2, imageView2);