第三方库SDWebImag②请求-①避免重复请求问题

必备知识架构-第三方库SDWebImag②请求-①避免重复请求问题

[toc]

iOS 如何避免在短时间内频繁发出相同的网络请求?

SDWebImage框架特征

  • 类别UIImageView,UIButton,MKAnnotationView- - 添加Web图像和高速缓存管理
  • 异步图像下载器
  • 具有自动缓存到期处理的异步内存+磁盘映像缓存
  • 背景图像解压缩
  • 保证相同的URL不会被下载多次
  • 保证虚假网址不会重复重试
  • 保证主线程永远不会被阻止
  • 使用GCD和ARC

一、SDWebImage是如何避免在短时间内频繁发出相同的网络请求?

SDWebImage是如何避免在短时间内频繁发出相同的网络请求?

设想:一个列表所有的图片请求都是同一个url。

答:

1、如何确保不生成重复的网络请求

图片下载的回调信息存储在SDWebImageDownloader类的URLOperations属性中,该属性是一个字典,key是图片的URL地址,value则是一个SDWebImageDownloaderOperation对象,包含每个图片的多组回调信息。由于我们允许多个图片同时下载,因此可能会有多个线程同时操作URLOperations属性。需要保证URLOperations操作(添加、删除)的线程安全性。

为了避免同一个URL在被多次任务请求的时候,进行多次的重复网络下载。

1、将下载地址URL与其对应的网络下载请求,通过下载管理器SDWebImageDownloader的URLOperations属性管理起来。(该属性是一个字典,key是图片的URL地址,value是operation)

2、我们并不会对每次操作一进来就进行创建请求任务,而是先通过之前缓存的下载任务URLOperations,通过URL寻找是否有该操作了,①如果有则不创建,而是直接取出来使用;②如果没有才创建operation来使用,并添加到URLOperations中。以备后续有同样URL请求的时候,能会从URLOperations中得到operation,就不会导致重复创建和添加到队列中了。(通过URLOperations,我们以此保证一个URL同时在被请求多次的情况下,生成/取到的是同一个,也只有一个SDWebImageDownloaderOperation,从而也就只会被下载一次。)。

在SDWebImage版本5.8.1中的相关代码如下:

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
SD_LOCK(self.operationsLock);	// 6、加锁,防止添加过程中,又有数据需要添加,保证线程安全
id downloadOperationCancelToken;
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url]; // 1、从进行中的 URLOperations 获取
if (!operation || operation.isFinished || operation.isCancelled) {
operation = [self createDownloaderOperationWithUrl:url options:options context:context]; // 2、如果之前没有下载操作,则创建,并加入到 URLOperations 中
// ....省略一堆代码
@weakify(self);
operation.completionBlock = ^{
@strongify(self);
if (!self) {
return;
}
SD_LOCK(self.operationsLock);
[self.URLOperations removeObjectForKey:url]; // 3、下载完成后,记得删除操作
SD_UNLOCK(self.operationsLock);
};
self.URLOperations[url] = operation; // 4、保存到 URLOperations 中,用于判断
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
[self.downloadQueue addOperation:operation]; // 4、添加操作

} else {
// ....省略一堆代码
@synchronized (operation) {
// 5、如果之前有下载操作,则不用创建操作,但是仍然需要保存回调,不然其他位置的图片的请求就丢失了(虽然说它们是同一张图片)
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}
}
SD_UNLOCK(self.operationsLock); // 6、解锁

3、问:重复的url请求,没有创建operation了,那operation的回调怎么办?

答:虽然重复的URL只有一个SDWebImageDownloaderOperation。但是这个opeartion的callbackBlocks是个数组,所有的回调都用这个数组保存起来的,所以不会丢失的(这里callbackBlocks数组里存放字典,对应进行时候的ProgressCallback回调和结束时候的CompletedCallback回调)。

4、问:对同一个operation的callbackBlocks数组操作,有什么要注意的?

答:因为不管同个url不管有没有新的operation生成,我们都会有一个当前任务对应downloadOperationCancelToken生成,来给这个operation添加回调。即同一个url所对应opeartion里的callbackBlocks数组,在有多次图片任务时候,就会有多次操作callbackBlocks的情况。所以对callbackBlocks的操作,也要保证线程安全。

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
// SDWebImageDownloader.m

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
// ....省略一堆代码

SD_LOCK(self.operationsLock);
id downloadOperationCancelToken;
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
if (!operation || operation.isFinished || operation.isCancelled) {
// ....省略一堆代码
} else {
// When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue)
// So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes.
@synchronized (operation) {
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}
// ....省略一堆代码
}
SD_UNLOCK(self.operationsLock);

// 虽然重复的URL只有一个SDWebImageDownloaderOperation。但是SDWebImageDownloadToken是每个URL都会有一个的,只是他们的SDWebImageDownloaderOperation是同一个。
SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
token.url = url;
token.request = operation.request;
token.downloadOperationCancelToken = downloadOperationCancelToken;

return token;
}

对callbackBlocks的操作,也要保证线程安全,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SDWebImageDownloaderOperation.m

@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
@synchronized (self) {
[self.callbackBlocks addObject:callbacks];
}
return callbacks;
}

2、它是如何保证请求管理的线程安全

说明:由于我们允许多个任务同时进行,也就造成了会有多个线程同时操作URLOperations属性。为了保证URLOperations操作(添加、删除)的线程安全性,我们添加了一个锁,且考虑到各种具备锁功能的性能问题,这里我们使用信号量semaphore。所以,我们控制线程安全的锁是使用信号量实现的operationsLock,且其初始值为1,用它来确保同一时间只有一个线程操作URLOperations属性

在SDWebImage版本5.8.1中的相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SDWebImageDownloader.m

- (nonnull instancetype)init {
return [self initWithConfig:SDWebImageDownloaderConfig.defaultDownloaderConfig];
}

- (instancetype)initWithConfig:(SDWebImageDownloaderConfig *)config {
self = [super init];
if (self) {
// ....省略一堆代码
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = _config.maxConcurrentDownloads;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
_URLOperations = [NSMutableDictionary new];
// ....省略一堆代码
_operationsLock = dispatch_semaphore_create(1);
// ....省略一堆代码
}
return self;
}

3、其它知识说明

3.1、SDWebImageDownloader 是什么?(异步的图片下载器)

SDWebImageDownloader是一个异步的图片下载器,它是一个单例类,主要负责图片的下载操作的管理。

图片的下载是放在一个NSOperationQueue操作队列中来完成的,默认情况下,队列最大并发数是6。

1
2
3
4
5
6
7
8
9
10
11
12
13
// SDWebImageDownloader.m
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;


_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = _config.maxConcurrentDownloads;

// SDWebImageDownloaderConfig.h
/**
* The maximum number of concurrent downloads.
* Defaults to 6.
*/
@property (nonatomic, assign) NSInteger maxConcurrentDownloads;

如果需要的话,我们可以通过修改maxConcurrentDownloads属性来修改并发下载数。

二、其他版本的SDWebImage的代码解析

1、SDWebImageDownloader 是什么?(异步的图片下载器)

其声明及定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
//SDWebImageDownloader.m

@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;

- (id)init {
if ((self = [super init])) {
...
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT); // 并发队列
...
}
return self;
}

每有一个图片下载请求,SDWebImageDownloader图片下载管理器就会生成一个继承自NSOperation的下载操作SDWebImageDownloaderOperation,并添加到下载队列downloadQueue中。

[sself.downloadQueue addOperation:operation];

该队列允许修改最大并发数。

1、由于我们允许多个任务同时进行,也就造成了会有多个线程同时操作URLOperations属性。为了保证URLOperations操作(添加、删除)的线程安全性,我们添加了线程操作的信号量operationsLock,其初始值为1,来确保同一时间只有一个线程操作URLOperations属性,我们以添加操作为例,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LOCK(self.operationsLock);
SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
if (!operation) {
operation = createCallback();
__weak typeof(self) wself = self;
operation.completionBlock = ^{
__strong typeof(wself) sself = wself;
if (!sself) {
return;
}
LOCK(sself.operationsLock);
[sself.URLOperations removeObjectForKey:url];
UNLOCK(sself.operationsLock);
};
[self.URLOperations setObject:operation forKey:url];
}
UNLOCK(self.operationsLock);

2、同样由于为了避免同一个URL的图片被下载多次,所以我们并不会对每次操作一进来就进行创建请求任务,而是先通过之前缓存的下载任务URLOperations中通过URL寻找是否有该操作了,如果有则不创建,如果没有才创建。因此,这里就会有一个URL同时在被请求多次的情况下,生成/取到的是同一个SDWebImageDownloaderOperation。

三、其他

对于下载任务的执行,SDWebImage还允许我们设置是默认的先进先出还是后进先出。该功能的实现,其自然是通过队列操作的依赖来完成的(其中lastAddedOperation指的是上一次的操作)。源代码如下:

1
2
3
4
5
6
[sself.downloadQueue addOperation:operation];
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}

其他参考文章

END