| | |
| | | */ |
| | | |
| | | #import "SDWebImageManager.h" |
| | | #import <objc/message.h> |
| | | #import "NSImage+WebCache.h" |
| | | #import <objc/message.h> |
| | | |
| | | #define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); |
| | | #define UNLOCK(lock) dispatch_semaphore_signal(lock); |
| | | |
| | | @interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation> |
| | | |
| | | @property (assign, nonatomic, getter = isCancelled) BOOL cancelled; |
| | | @property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock; |
| | | @property (strong, nonatomic, nullable) SDWebImageDownloadToken *downloadToken; |
| | | @property (strong, nonatomic, nullable) NSOperation *cacheOperation; |
| | | @property (weak, nonatomic, nullable) SDWebImageManager *manager; |
| | | |
| | | @end |
| | | |
| | |
| | | @property (strong, nonatomic, readwrite, nonnull) SDImageCache *imageCache; |
| | | @property (strong, nonatomic, readwrite, nonnull) SDWebImageDownloader *imageDownloader; |
| | | @property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs; |
| | | @property (strong, nonatomic, nonnull) dispatch_semaphore_t failedURLsLock; // a lock to keep the access to `failedURLs` thread-safe |
| | | @property (strong, nonatomic, nonnull) NSMutableArray<SDWebImageCombinedOperation *> *runningOperations; |
| | | @property (strong, nonatomic, nonnull) dispatch_semaphore_t runningOperationsLock; // a lock to keep the access to `runningOperations` thread-safe |
| | | |
| | | @end |
| | | |
| | |
| | | _imageCache = cache; |
| | | _imageDownloader = downloader; |
| | | _failedURLs = [NSMutableSet new]; |
| | | _failedURLsLock = dispatch_semaphore_create(1); |
| | | _runningOperations = [NSMutableArray new]; |
| | | _runningOperationsLock = dispatch_semaphore_create(1); |
| | | } |
| | | return self; |
| | | } |
| | |
| | | } else { |
| | | return url.absoluteString; |
| | | } |
| | | } |
| | | |
| | | - (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image { |
| | | return SDScaledImageForKey(key, image); |
| | | } |
| | | |
| | | - (void)cachedImageExistsForURL:(nullable NSURL *)url |
| | |
| | | url = nil; |
| | | } |
| | | |
| | | __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; |
| | | __weak SDWebImageCombinedOperation *weakOperation = operation; |
| | | SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; |
| | | operation.manager = self; |
| | | |
| | | BOOL isFailedUrl = NO; |
| | | if (url) { |
| | | @synchronized (self.failedURLs) { |
| | | isFailedUrl = [self.failedURLs containsObject:url]; |
| | | } |
| | | LOCK(self.failedURLsLock); |
| | | isFailedUrl = [self.failedURLs containsObject:url]; |
| | | UNLOCK(self.failedURLsLock); |
| | | } |
| | | |
| | | if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) { |
| | |
| | | return operation; |
| | | } |
| | | |
| | | @synchronized (self.runningOperations) { |
| | | [self.runningOperations addObject:operation]; |
| | | } |
| | | LOCK(self.runningOperationsLock); |
| | | [self.runningOperations addObject:operation]; |
| | | UNLOCK(self.runningOperationsLock); |
| | | NSString *key = [self cacheKeyForURL:url]; |
| | | |
| | | operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) { |
| | | if (operation.isCancelled) { |
| | | [self safelyRemoveOperationFromRunning:operation]; |
| | | |
| | | SDImageCacheOptions cacheOptions = 0; |
| | | if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory; |
| | | if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync; |
| | | if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages; |
| | | |
| | | __weak SDWebImageCombinedOperation *weakOperation = operation; |
| | | operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) { |
| | | __strong __typeof(weakOperation) strongOperation = weakOperation; |
| | | if (!strongOperation || strongOperation.isCancelled) { |
| | | [self safelyRemoveOperationFromRunning:strongOperation]; |
| | | return; |
| | | } |
| | | |
| | | if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) { |
| | | |
| | | // Check whether we should download image from network |
| | | BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly)) |
| | | && (!cachedImage || options & SDWebImageRefreshCached) |
| | | && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]); |
| | | if (shouldDownload) { |
| | | if (cachedImage && options & SDWebImageRefreshCached) { |
| | | // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image |
| | | // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server. |
| | | [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url]; |
| | | [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url]; |
| | | } |
| | | |
| | | // download if no image or requested to refresh anyway, and download allowed by delegate |
| | |
| | | downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse; |
| | | } |
| | | |
| | | SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) { |
| | | __strong __typeof(weakOperation) strongOperation = weakOperation; |
| | | if (!strongOperation || strongOperation.isCancelled) { |
| | | // `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle |
| | | __weak typeof(strongOperation) weakSubOperation = strongOperation; |
| | | strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) { |
| | | __strong typeof(weakSubOperation) strongSubOperation = weakSubOperation; |
| | | if (!strongSubOperation || strongSubOperation.isCancelled) { |
| | | // Do nothing if the operation was cancelled |
| | | // See #699 for more details |
| | | // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data |
| | | } else if (error) { |
| | | [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url]; |
| | | |
| | | if ( error.code != NSURLErrorNotConnectedToInternet |
| | | && error.code != NSURLErrorCancelled |
| | | && error.code != NSURLErrorTimedOut |
| | | && error.code != NSURLErrorInternationalRoamingOff |
| | | && error.code != NSURLErrorDataNotAllowed |
| | | && error.code != NSURLErrorCannotFindHost |
| | | && error.code != NSURLErrorCannotConnectToHost) { |
| | | @synchronized (self.failedURLs) { |
| | | [self.failedURLs addObject:url]; |
| | | } |
| | | [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url]; |
| | | BOOL shouldBlockFailedURL; |
| | | // Check whether we should block failed url |
| | | if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) { |
| | | shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error]; |
| | | } else { |
| | | shouldBlockFailedURL = ( error.code != NSURLErrorNotConnectedToInternet |
| | | && error.code != NSURLErrorCancelled |
| | | && error.code != NSURLErrorTimedOut |
| | | && error.code != NSURLErrorInternationalRoamingOff |
| | | && error.code != NSURLErrorDataNotAllowed |
| | | && error.code != NSURLErrorCannotFindHost |
| | | && error.code != NSURLErrorCannotConnectToHost |
| | | && error.code != NSURLErrorNetworkConnectionLost); |
| | | } |
| | | |
| | | if (shouldBlockFailedURL) { |
| | | LOCK(self.failedURLsLock); |
| | | [self.failedURLs addObject:url]; |
| | | UNLOCK(self.failedURLsLock); |
| | | } |
| | | } |
| | | else { |
| | | if ((options & SDWebImageRetryFailed)) { |
| | | @synchronized (self.failedURLs) { |
| | | [self.failedURLs removeObject:url]; |
| | | } |
| | | LOCK(self.failedURLsLock); |
| | | [self.failedURLs removeObject:url]; |
| | | UNLOCK(self.failedURLsLock); |
| | | } |
| | | |
| | | BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly); |
| | | |
| | | // We've done the scale process in SDWebImageDownloader with the shared manager, this is used for custom manager and avoid extra scale. |
| | | if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) { |
| | | downloadedImage = [self scaledImageForKey:key image:downloadedImage]; |
| | | } |
| | | |
| | | if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) { |
| | | // Image refresh hit the NSURLCache cache, do not call the completion block |
| | |
| | | |
| | | if (transformedImage && finished) { |
| | | BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage]; |
| | | NSData *cacheData; |
| | | // pass nil if the image was transformed, so we can recalculate the data from the image |
| | | [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil]; |
| | | if (self.cacheSerializer) { |
| | | cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url); |
| | | } else { |
| | | cacheData = (imageWasTransformed ? nil : downloadedData); |
| | | } |
| | | [self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil]; |
| | | } |
| | | |
| | | [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; |
| | | [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; |
| | | }); |
| | | } else { |
| | | if (downloadedImage && finished) { |
| | | [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil]; |
| | | if (self.cacheSerializer) { |
| | | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ |
| | | NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url); |
| | | [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil]; |
| | | }); |
| | | } else { |
| | | [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil]; |
| | | } |
| | | } |
| | | [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; |
| | | [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url]; |
| | | } |
| | | } |
| | | |
| | | if (finished) { |
| | | [self safelyRemoveOperationFromRunning:strongOperation]; |
| | | [self safelyRemoveOperationFromRunning:strongSubOperation]; |
| | | } |
| | | }]; |
| | | operation.cancelBlock = ^{ |
| | | [self.imageDownloader cancel:subOperationToken]; |
| | | __strong __typeof(weakOperation) strongOperation = weakOperation; |
| | | [self safelyRemoveOperationFromRunning:strongOperation]; |
| | | }; |
| | | } else if (cachedImage) { |
| | | __strong __typeof(weakOperation) strongOperation = weakOperation; |
| | | [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url]; |
| | | [self safelyRemoveOperationFromRunning:operation]; |
| | | [self safelyRemoveOperationFromRunning:strongOperation]; |
| | | } else { |
| | | // Image not in cache and download disallowed by delegate |
| | | __strong __typeof(weakOperation) strongOperation = weakOperation; |
| | | [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url]; |
| | | [self safelyRemoveOperationFromRunning:operation]; |
| | | [self safelyRemoveOperationFromRunning:strongOperation]; |
| | | } |
| | | }]; |
| | | |
| | |
| | | } |
| | | |
| | | - (void)cancelAll { |
| | | @synchronized (self.runningOperations) { |
| | | NSArray<SDWebImageCombinedOperation *> *copiedOperations = [self.runningOperations copy]; |
| | | [copiedOperations makeObjectsPerformSelector:@selector(cancel)]; |
| | | [self.runningOperations removeObjectsInArray:copiedOperations]; |
| | | } |
| | | LOCK(self.runningOperationsLock); |
| | | NSArray<SDWebImageCombinedOperation *> *copiedOperations = [self.runningOperations copy]; |
| | | UNLOCK(self.runningOperationsLock); |
| | | [copiedOperations makeObjectsPerformSelector:@selector(cancel)]; // This will call `safelyRemoveOperationFromRunning:` and remove from the array |
| | | } |
| | | |
| | | - (BOOL)isRunning { |
| | | BOOL isRunning = NO; |
| | | @synchronized (self.runningOperations) { |
| | | isRunning = (self.runningOperations.count > 0); |
| | | } |
| | | LOCK(self.runningOperationsLock); |
| | | isRunning = (self.runningOperations.count > 0); |
| | | UNLOCK(self.runningOperationsLock); |
| | | return isRunning; |
| | | } |
| | | |
| | | - (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation { |
| | | @synchronized (self.runningOperations) { |
| | | if (operation) { |
| | | [self.runningOperations removeObject:operation]; |
| | | } |
| | | if (!operation) { |
| | | return; |
| | | } |
| | | LOCK(self.runningOperationsLock); |
| | | [self.runningOperations removeObject:operation]; |
| | | UNLOCK(self.runningOperationsLock); |
| | | } |
| | | |
| | | - (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation |
| | |
| | | |
| | | @implementation SDWebImageCombinedOperation |
| | | |
| | | - (void)setCancelBlock:(nullable SDWebImageNoParamsBlock)cancelBlock { |
| | | // check if the operation is already cancelled, then we just call the cancelBlock |
| | | if (self.isCancelled) { |
| | | if (cancelBlock) { |
| | | cancelBlock(); |
| | | } |
| | | _cancelBlock = nil; // don't forget to nil the cancelBlock, otherwise we will get crashes |
| | | } else { |
| | | _cancelBlock = [cancelBlock copy]; |
| | | } |
| | | } |
| | | |
| | | - (void)cancel { |
| | | self.cancelled = YES; |
| | | if (self.cacheOperation) { |
| | | [self.cacheOperation cancel]; |
| | | self.cacheOperation = nil; |
| | | } |
| | | if (self.cancelBlock) { |
| | | self.cancelBlock(); |
| | | |
| | | // TODO: this is a temporary fix to #809. |
| | | // Until we can figure the exact cause of the crash, going with the ivar instead of the setter |
| | | // self.cancelBlock = nil; |
| | | _cancelBlock = nil; |
| | | @synchronized(self) { |
| | | self.cancelled = YES; |
| | | if (self.cacheOperation) { |
| | | [self.cacheOperation cancel]; |
| | | self.cacheOperation = nil; |
| | | } |
| | | if (self.downloadToken) { |
| | | [self.manager.imageDownloader cancel:self.downloadToken]; |
| | | } |
| | | [self.manager safelyRemoveOperationFromRunning:self]; |
| | | } |
| | | } |
| | | |