必备知识架构-第三方库SDWebImag②请求-①避免重复请求问题
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