0% found this document useful (0 votes)
59 views8 pages

SDWeb Image Manager

The document describes a class for managing image downloads and caching. It handles operations for downloading, caching, and retrieving images. It uses locks and semaphores to synchronize access to operations and caches.

Uploaded by

istv4n
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
59 views8 pages

SDWeb Image Manager

The document describes a class for managing image downloads and caching. It handles operations for downloading, caching, and retrieving images. It uses locks and semaphores to synchronize access to operations and caches.

Uploaded by

istv4n
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 8

/*

* This file is part of the SDWebImage package.


* (c) Olivier Poitrey <[email protected]>
*
* 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;
}

// `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), ^{
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), ^{
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

You might also like