123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- /*
- * 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 "SDImageCache.h"
- #import "SDWebImageDownloader.h"
- #import "UIImage+Metadata.h"
- #import "SDWebImageError.h"
- #import "SDInternalMacros.h"
- static id<SDImageCache> _defaultImageCache;
- static id<SDImageLoader> _defaultImageLoader;
- @interface SDWebImageCombinedOperation ()
- @property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
- @property (strong, nonatomic, readwrite, nullable) id<SDWebImageOperation> loaderOperation;
- @property (strong, nonatomic, readwrite, nullable) id<SDWebImageOperation> cacheOperation;
- @property (weak, nonatomic, nullable) SDWebImageManager *manager;
- @end
- @interface SDWebImageManager ()
- @property (strong, nonatomic, readwrite, nonnull) SDImageCache *imageCache;
- @property (strong, nonatomic, readwrite, nonnull) id<SDImageLoader> imageLoader;
- @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
- + (id<SDImageCache>)defaultImageCache {
- return _defaultImageCache;
- }
- + (void)setDefaultImageCache:(id<SDImageCache>)defaultImageCache {
- if (defaultImageCache && ![defaultImageCache conformsToProtocol:@protocol(SDImageCache)]) {
- return;
- }
- _defaultImageCache = defaultImageCache;
- }
- + (id<SDImageLoader>)defaultImageLoader {
- return _defaultImageLoader;
- }
- + (void)setDefaultImageLoader:(id<SDImageLoader>)defaultImageLoader {
- if (defaultImageLoader && ![defaultImageLoader conformsToProtocol:@protocol(SDImageLoader)]) {
- return;
- }
- _defaultImageLoader = defaultImageLoader;
- }
- + (nonnull instancetype)sharedManager {
- static dispatch_once_t once;
- static id instance;
- dispatch_once(&once, ^{
- instance = [self new];
- });
- return instance;
- }
- - (nonnull instancetype)init {
- id<SDImageCache> cache = [[self class] defaultImageCache];
- if (!cache) {
- cache = [SDImageCache sharedImageCache];
- }
- id<SDImageLoader> loader = [[self class] defaultImageLoader];
- if (!loader) {
- loader = [SDWebImageDownloader sharedDownloader];
- }
- return [self initWithCache:cache loader:loader];
- }
- - (nonnull instancetype)initWithCache:(nonnull id<SDImageCache>)cache loader:(nonnull id<SDImageLoader>)loader {
- if ((self = [super init])) {
- _imageCache = cache;
- _imageLoader = loader;
- _failedURLs = [NSMutableSet new];
- _failedURLsLock = dispatch_semaphore_create(1);
- _runningOperations = [NSMutableSet new];
- _runningOperationsLock = dispatch_semaphore_create(1);
- }
- return self;
- }
- - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
- return [self cacheKeyForURL:url cacheKeyFilter:self.cacheKeyFilter];
- }
- - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url cacheKeyFilter:(id<SDWebImageCacheKeyFilter>)cacheKeyFilter {
- if (!url) {
- return @"";
- }
- if (cacheKeyFilter) {
- return [cacheKeyFilter cacheKeyForURL:url];
- } else {
- return url.absoluteString;
- }
- }
- - (SDWebImageCombinedOperation *)loadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDInternalCompletionBlock)completedBlock {
- return [self loadImageWithURL:url options:options context:nil progress:progressBlock completed:completedBlock];
- }
- - (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
- options:(SDWebImageOptions)options
- context:(nullable SDWebImageContext *)context
- progress:(nullable SDImageLoaderProgressBlock)progressBlock
- completed:(nonnull 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) {
- SD_LOCK(self.failedURLsLock);
- isFailedUrl = [self.failedURLs containsObject:url];
- SD_UNLOCK(self.failedURLsLock);
- }
- if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
- [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}] url:url];
- return operation;
- }
- SD_LOCK(self.runningOperationsLock);
- [self.runningOperations addObject:operation];
- SD_UNLOCK(self.runningOperationsLock);
-
- // Preprocess the context arg to provide the default value from manager
- context = [self processedContextWithContext:context];
-
- // Start the entry to load image from cache
- [self callCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
- return operation;
- }
- - (void)cancelAll {
- SD_LOCK(self.runningOperationsLock);
- NSSet<SDWebImageCombinedOperation *> *copiedOperations = [self.runningOperations copy];
- SD_UNLOCK(self.runningOperationsLock);
- [copiedOperations makeObjectsPerformSelector:@selector(cancel)]; // This will call `safelyRemoveOperationFromRunning:` and remove from the array
- }
- - (BOOL)isRunning {
- BOOL isRunning = NO;
- SD_LOCK(self.runningOperationsLock);
- isRunning = (self.runningOperations.count > 0);
- SD_UNLOCK(self.runningOperationsLock);
- return isRunning;
- }
- #pragma mark - Private
- // Query cache process
- - (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
- url:(nonnull NSURL *)url
- options:(SDWebImageOptions)options
- context:(nullable SDWebImageContext *)context
- progress:(nullable SDImageLoaderProgressBlock)progressBlock
- completed:(nullable SDInternalCompletionBlock)completedBlock {
- // Check whether we should query cache
- BOOL shouldQueryCache = (options & SDWebImageFromLoaderOnly) == 0;
- if (shouldQueryCache) {
- id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
- NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
- @weakify(operation);
- operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
- @strongify(operation);
- if (!operation || operation.isCancelled) {
- [self safelyRemoveOperationFromRunning:operation];
- return;
- }
- // Continue download process
- [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
- }];
- } else {
- // Continue download process
- [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
- }
- }
- // Download process
- - (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
- url:(nonnull NSURL *)url
- options:(SDWebImageOptions)options
- context:(SDWebImageContext *)context
- cachedImage:(nullable UIImage *)cachedImage
- cachedData:(nullable NSData *)cachedData
- cacheType:(SDImageCacheType)cacheType
- progress:(nullable SDImageLoaderProgressBlock)progressBlock
- completed:(nullable SDInternalCompletionBlock)completedBlock {
- // Check whether we should download image from network
- BOOL shouldDownload = (options & SDWebImageFromCacheOnly) == 0;
- shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
- shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
- shouldDownload &= [self.imageLoader canRequestImageForURL: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:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
- // Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.
- SDWebImageMutableContext *mutableContext;
- if (context) {
- mutableContext = [context mutableCopy];
- } else {
- mutableContext = [NSMutableDictionary dictionary];
- }
- mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
- context = [mutableContext copy];
- }
-
- // `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle
- @weakify(operation);
- operation.loaderOperation = [self.imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
- @strongify(operation);
- if (!operation || operation.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 (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
- // Image refresh hit the NSURLCache cache, do not call the completion block
- } else if (error) {
- [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
- BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error];
-
- if (shouldBlockFailedURL) {
- SD_LOCK(self.failedURLsLock);
- [self.failedURLs addObject:url];
- SD_UNLOCK(self.failedURLsLock);
- }
- } else {
- if ((options & SDWebImageRetryFailed)) {
- SD_LOCK(self.failedURLsLock);
- [self.failedURLs removeObject:url];
- SD_UNLOCK(self.failedURLsLock);
- }
-
- [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
- }
-
- if (finished) {
- [self safelyRemoveOperationFromRunning:operation];
- }
- }];
- } else if (cachedImage) {
- [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
- [self safelyRemoveOperationFromRunning:operation];
- } else {
- // Image not in cache and download disallowed by delegate
- [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
- [self safelyRemoveOperationFromRunning:operation];
- }
- }
- // Store cache process
- - (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
- url:(nonnull NSURL *)url
- options:(SDWebImageOptions)options
- context:(SDWebImageContext *)context
- downloadedImage:(nullable UIImage *)downloadedImage
- downloadedData:(nullable NSData *)downloadedData
- finished:(BOOL)finished
- progress:(nullable SDImageLoaderProgressBlock)progressBlock
- completed:(nullable SDInternalCompletionBlock)completedBlock {
- SDImageCacheType storeCacheType = SDImageCacheTypeAll;
- if (context[SDWebImageContextStoreCacheType]) {
- storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue];
- }
- id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
- NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
- id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
- id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
- if (downloadedImage && (!downloadedImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer) {
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
- @autoreleasepool {
- UIImage *transformedImage = [transformer transformedImageWithImage:downloadedImage forKey:key];
- if (transformedImage && finished) {
- NSString *transformerKey = [transformer transformerKey];
- NSString *cacheKey = SDTransformedKeyForKey(key, transformerKey);
- BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
- NSData *cacheData;
- // pass nil if the image was transformed, so we can recalculate the data from the image
- if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
- cacheData = [cacheSerializer cacheDataWithImage:transformedImage originalData:(imageWasTransformed ? nil : downloadedData) imageURL:url];
- } else {
- cacheData = (imageWasTransformed ? nil : downloadedData);
- }
- [self.imageCache storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType completion:nil];
- }
-
- [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
- }
- });
- } else {
- if (downloadedImage && finished) {
- if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
- @autoreleasepool {
- NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url];
- [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key cacheType:storeCacheType completion:nil];
- }
- });
- } else {
- [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:storeCacheType completion:nil];
- }
- }
- [self callCompletionBlockForOperation:operation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
- }
- }
- #pragma mark - Helper
- - (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
- if (!operation) {
- return;
- }
- SD_LOCK(self.runningOperationsLock);
- [self.runningOperations removeObject:operation];
- SD_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);
- }
- });
- }
- - (BOOL)shouldBlockFailedURLWithURL:(nonnull NSURL *)url
- error:(nonnull NSError *)error {
- // Check whether we should block failed url
- BOOL shouldBlockFailedURL;
- if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
- shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
- } else {
- shouldBlockFailedURL = [self.imageLoader shouldBlockFailedURLWithURL:url error:error];
- }
-
- return shouldBlockFailedURL;
- }
- - (SDWebImageContext *)processedContextWithContext:(SDWebImageContext *)context {
- SDWebImageMutableContext *mutableContext = [SDWebImageMutableContext dictionary];
-
- // Image Transformer from manager
- if (!context[SDWebImageContextImageTransformer]) {
- id<SDImageTransformer> transformer = self.transformer;
- [mutableContext setValue:transformer forKey:SDWebImageContextImageTransformer];
- }
- // Cache key filter from manager
- if (!context[SDWebImageContextCacheKeyFilter]) {
- id<SDWebImageCacheKeyFilter> cacheKeyFilter = self.cacheKeyFilter;
- [mutableContext setValue:cacheKeyFilter forKey:SDWebImageContextCacheKeyFilter];
- }
- // Cache serializer from manager
- if (!context[SDWebImageContextCacheSerializer]) {
- id<SDWebImageCacheSerializer> cacheSerializer = self.cacheSerializer;
- [mutableContext setValue:cacheSerializer forKey:SDWebImageContextCacheSerializer];
- }
-
- if (mutableContext.count == 0) {
- return context;
- } else {
- [mutableContext addEntriesFromDictionary:context];
- return [mutableContext copy];
- }
- }
- @end
- @implementation SDWebImageCombinedOperation
- - (void)cancel {
- @synchronized(self) {
- if (self.isCancelled) {
- return;
- }
- self.cancelled = YES;
- if (self.cacheOperation) {
- [self.cacheOperation cancel];
- self.cacheOperation = nil;
- }
- if (self.loaderOperation) {
- [self.loaderOperation cancel];
- self.loaderOperation = nil;
- }
- [self.manager safelyRemoveOperationFromRunning:self];
- }
- }
- @end
|