123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- /*
- * This file is part of the SDWebImage package.
- * (c) Olivier Poitrey <rs@dailymotion.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- #import "SDWebImageManager.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 (strong, nonatomic, nullable) SDWebImageDownloadToken *downloadToken;
- @property (strong, nonatomic, nullable) NSOperation *cacheOperation;
- @property (weak, nonatomic, nullable) SDWebImageManager *manager;
- @end
- @interface SDWebImageManager ()
- @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) NSMutableSet<SDWebImageCombinedOperation *> *runningOperations;
- @property (strong, nonatomic, nonnull) dispatch_semaphore_t runningOperationsLock; // a lock to keep the access to `runningOperations` thread-safe
- @end
- @implementation SDWebImageManager
- + (nonnull instancetype)sharedManager {
- static dispatch_once_t once;
- static id instance;
- dispatch_once(&once, ^{
- instance = [self new];
- });
- return instance;
- }
- - (nonnull instancetype)init {
- SDImageCache *cache = [SDImageCache sharedImageCache];
- SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
- return [self initWithCache:cache downloader:downloader];
- }
- - (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
- if ((self = [super init])) {
- _imageCache = cache;
- _imageDownloader = downloader;
- _failedURLs = [NSMutableSet new];
- _failedURLsLock = dispatch_semaphore_create(1);
- _runningOperations = [NSMutableSet new];
- _runningOperationsLock = dispatch_semaphore_create(1);
- }
- return self;
- }
- - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
- if (!url) {
- return @"";
- }
- if (self.cacheKeyFilter) {
- return self.cacheKeyFilter(url);
- } else {
- return url.absoluteString;
- }
- }
- - (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
- return SDScaledImageForKey(key, image);
- }
- - (void)cachedImageExistsForURL:(nullable NSURL *)url
- completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {
- NSString *key = [self cacheKeyForURL:url];
-
- BOOL isInMemoryCache = ([self.imageCache imageFromMemoryCacheForKey:key] != nil);
-
- if (isInMemoryCache) {
- // making sure we call the completion block on the main queue
- dispatch_async(dispatch_get_main_queue(), ^{
- if (completionBlock) {
- completionBlock(YES);
- }
- });
- return;
- }
-
- [self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
- // the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
- if (completionBlock) {
- completionBlock(isInDiskCache);
- }
- }];
- }
- - (void)diskImageExistsForURL:(nullable NSURL *)url
- completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {
- NSString *key = [self cacheKeyForURL:url];
-
- [self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
- // the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
- if (completionBlock) {
- completionBlock(isInDiskCache);
- }
- }];
- }
- - (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
- options:(SDWebImageOptions)options
- progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
- completed:(nullable SDInternalCompletionBlock)completedBlock {
- // Invoking this method without a completedBlock is pointless
- NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
- // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
- // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
- if ([url isKindOfClass:NSString.class]) {
- url = [NSURL URLWithString:(NSString *)url];
- }
- // Prevents app crashing on argument type error like sending NSNull instead of NSURL
- if (![url isKindOfClass:NSURL.class]) {
- url = nil;
- }
- SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
- operation.manager = self;
- BOOL isFailedUrl = NO;
- if (url) {
- LOCK(self.failedURLsLock);
- isFailedUrl = [self.failedURLs containsObject:url];
- UNLOCK(self.failedURLsLock);
- }
- if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
- [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
- return operation;
- }
- LOCK(self.runningOperationsLock);
- [self.runningOperations addObject:operation];
- UNLOCK(self.runningOperationsLock);
- NSString *key = [self cacheKeyForURL:url];
-
- 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;
- }
-
- // 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: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
- SDWebImageDownloaderOptions downloaderOptions = 0;
- if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
- if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
- if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
- if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
- if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
- if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
- if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
- if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
-
- if (cachedImage && options & SDWebImageRefreshCached) {
- // force progressive off if image already cached but forced refreshing
- downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
- // ignore image read from NSURLCache if image if cached but force refreshing
- downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
- }
- if (options & SDWebImageGifImageIncluded) downloaderOptions |= SDWebImageDownloaderGifIncluded;
-
- // `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: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)) {
- 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
- } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
- @autoreleasepool {
- UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
-
- 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
- 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:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
- }
- });
- } else {
- if (downloadedImage && finished) {
- if (self.cacheSerializer) {
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
- @autoreleasepool {
- 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:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
- }
- }
- if (finished) {
- [self safelyRemoveOperationFromRunning:strongSubOperation];
- }
- }];
- } else if (cachedImage) {
- [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
- [self safelyRemoveOperationFromRunning:strongOperation];
- } else {
- // Image not in cache and download disallowed by delegate
- [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
- [self safelyRemoveOperationFromRunning:strongOperation];
- }
- }];
- return operation;
- }
- - (void)saveImageToCache:(nullable UIImage *)image forURL:(nullable NSURL *)url {
- if (image && url) {
- NSString *key = [self cacheKeyForURL:url];
- [self.imageCache storeImage:image forKey:key toDisk:YES completion:nil];
- }
- }
- - (void)cancelAll {
- LOCK(self.runningOperationsLock);
- NSSet<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;
- LOCK(self.runningOperationsLock);
- isRunning = (self.runningOperations.count > 0);
- UNLOCK(self.runningOperationsLock);
- return isRunning;
- }
- - (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
- if (!operation) {
- return;
- }
- LOCK(self.runningOperationsLock);
- [self.runningOperations removeObject:operation];
- UNLOCK(self.runningOperationsLock);
- }
- - (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
- completion:(nullable SDInternalCompletionBlock)completionBlock
- error:(nullable NSError *)error
- url:(nullable NSURL *)url {
- [self callCompletionBlockForOperation:operation completion:completionBlock image:nil data:nil error:error cacheType:SDImageCacheTypeNone finished:YES url:url];
- }
- - (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
- completion:(nullable SDInternalCompletionBlock)completionBlock
- image:(nullable UIImage *)image
- data:(nullable NSData *)data
- error:(nullable NSError *)error
- cacheType:(SDImageCacheType)cacheType
- finished:(BOOL)finished
- url:(nullable NSURL *)url {
- dispatch_main_async_safe(^{
- if (operation && !operation.isCancelled && completionBlock) {
- completionBlock(image, data, error, cacheType, finished, url);
- }
- });
- }
- @end
- @implementation SDWebImageCombinedOperation
- - (void)cancel {
- @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];
- }
- }
- @end
|