New file |
| | |
| | | /* |
| | | * 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 "SDWebImageDownloaderOperation.h" |
| | | #import "SDWebImageManager.h" |
| | | #import "NSImage+WebCache.h" |
| | | #import "SDWebImageCodersManager.h" |
| | | |
| | | #define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); |
| | | #define UNLOCK(lock) dispatch_semaphore_signal(lock); |
| | | |
| | | // iOS 8 Foundation.framework extern these symbol but the define is in CFNetwork.framework. We just fix this without import CFNetwork.framework |
| | | #if (__IPHONE_OS_VERSION_MIN_REQUIRED && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0) |
| | | const float NSURLSessionTaskPriorityHigh = 0.75; |
| | | const float NSURLSessionTaskPriorityDefault = 0.5; |
| | | const float NSURLSessionTaskPriorityLow = 0.25; |
| | | #endif |
| | | |
| | | NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification"; |
| | | NSString *const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification"; |
| | | NSString *const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification"; |
| | | NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification"; |
| | | |
| | | static NSString *const kProgressCallbackKey = @"progress"; |
| | | static NSString *const kCompletedCallbackKey = @"completed"; |
| | | |
| | | typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary; |
| | | |
| | | @interface SDWebImageDownloaderOperation () |
| | | |
| | | @property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks; |
| | | |
| | | @property (assign, nonatomic, getter = isExecuting) BOOL executing; |
| | | @property (assign, nonatomic, getter = isFinished) BOOL finished; |
| | | @property (strong, nonatomic, nullable) NSMutableData *imageData; |
| | | @property (copy, nonatomic, nullable) NSData *cachedData; // for `SDWebImageDownloaderIgnoreCachedResponse` |
| | | |
| | | // This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run |
| | | // the task associated with this operation |
| | | @property (weak, nonatomic, nullable) NSURLSession *unownedSession; |
| | | // This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one |
| | | @property (strong, nonatomic, nullable) NSURLSession *ownedSession; |
| | | |
| | | @property (strong, nonatomic, readwrite, nullable) NSURLSessionTask *dataTask; |
| | | |
| | | @property (strong, nonatomic, nonnull) dispatch_semaphore_t callbacksLock; // a lock to keep the access to `callbackBlocks` thread-safe |
| | | |
| | | @property (strong, nonatomic, nonnull) dispatch_queue_t coderQueue; // the queue to do image decoding |
| | | #if SD_UIKIT |
| | | @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; |
| | | #endif |
| | | |
| | | @property (strong, nonatomic, nullable) id<SDWebImageProgressiveCoder> progressiveCoder; |
| | | |
| | | @end |
| | | |
| | | @implementation SDWebImageDownloaderOperation |
| | | |
| | | @synthesize executing = _executing; |
| | | @synthesize finished = _finished; |
| | | |
| | | - (nonnull instancetype)init { |
| | | return [self initWithRequest:nil inSession:nil options:0]; |
| | | } |
| | | |
| | | - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request |
| | | inSession:(nullable NSURLSession *)session |
| | | options:(SDWebImageDownloaderOptions)options { |
| | | if ((self = [super init])) { |
| | | _request = [request copy]; |
| | | _shouldDecompressImages = YES; |
| | | _options = options; |
| | | _callbackBlocks = [NSMutableArray new]; |
| | | _executing = NO; |
| | | _finished = NO; |
| | | _expectedSize = 0; |
| | | _unownedSession = session; |
| | | _callbacksLock = dispatch_semaphore_create(1); |
| | | _coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL); |
| | | } |
| | | return self; |
| | | } |
| | | |
| | | - (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]; |
| | | LOCK(self.callbacksLock); |
| | | [self.callbackBlocks addObject:callbacks]; |
| | | UNLOCK(self.callbacksLock); |
| | | return callbacks; |
| | | } |
| | | |
| | | - (nullable NSArray<id> *)callbacksForKey:(NSString *)key { |
| | | LOCK(self.callbacksLock); |
| | | NSMutableArray<id> *callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy]; |
| | | UNLOCK(self.callbacksLock); |
| | | // We need to remove [NSNull null] because there might not always be a progress block for each callback |
| | | [callbacks removeObjectIdenticalTo:[NSNull null]]; |
| | | return [callbacks copy]; // strip mutability here |
| | | } |
| | | |
| | | - (BOOL)cancel:(nullable id)token { |
| | | BOOL shouldCancel = NO; |
| | | LOCK(self.callbacksLock); |
| | | [self.callbackBlocks removeObjectIdenticalTo:token]; |
| | | if (self.callbackBlocks.count == 0) { |
| | | shouldCancel = YES; |
| | | } |
| | | UNLOCK(self.callbacksLock); |
| | | if (shouldCancel) { |
| | | [self cancel]; |
| | | } |
| | | return shouldCancel; |
| | | } |
| | | |
| | | - (void)start { |
| | | @synchronized (self) { |
| | | if (self.isCancelled) { |
| | | self.finished = YES; |
| | | [self reset]; |
| | | return; |
| | | } |
| | | |
| | | #if SD_UIKIT |
| | | Class UIApplicationClass = NSClassFromString(@"UIApplication"); |
| | | BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)]; |
| | | if (hasApplication && [self shouldContinueWhenAppEntersBackground]) { |
| | | __weak __typeof__ (self) wself = self; |
| | | UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)]; |
| | | self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{ |
| | | __strong __typeof (wself) sself = wself; |
| | | |
| | | if (sself) { |
| | | [sself cancel]; |
| | | |
| | | [app endBackgroundTask:sself.backgroundTaskId]; |
| | | sself.backgroundTaskId = UIBackgroundTaskInvalid; |
| | | } |
| | | }]; |
| | | } |
| | | #endif |
| | | NSURLSession *session = self.unownedSession; |
| | | if (!session) { |
| | | NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; |
| | | sessionConfig.timeoutIntervalForRequest = 15; |
| | | |
| | | /** |
| | | * Create the session for this task |
| | | * We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate |
| | | * method calls and completion handler calls. |
| | | */ |
| | | session = [NSURLSession sessionWithConfiguration:sessionConfig |
| | | delegate:self |
| | | delegateQueue:nil]; |
| | | self.ownedSession = session; |
| | | } |
| | | |
| | | if (self.options & SDWebImageDownloaderIgnoreCachedResponse) { |
| | | // Grab the cached data for later check |
| | | NSURLCache *URLCache = session.configuration.URLCache; |
| | | if (!URLCache) { |
| | | URLCache = [NSURLCache sharedURLCache]; |
| | | } |
| | | NSCachedURLResponse *cachedResponse; |
| | | // NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483 |
| | | @synchronized (URLCache) { |
| | | cachedResponse = [URLCache cachedResponseForRequest:self.request]; |
| | | } |
| | | if (cachedResponse) { |
| | | self.cachedData = cachedResponse.data; |
| | | } |
| | | } |
| | | |
| | | self.dataTask = [session dataTaskWithRequest:self.request]; |
| | | self.executing = YES; |
| | | } |
| | | |
| | | if (self.dataTask) { |
| | | #pragma clang diagnostic push |
| | | #pragma clang diagnostic ignored "-Wunguarded-availability" |
| | | if ([self.dataTask respondsToSelector:@selector(setPriority:)]) { |
| | | if (self.options & SDWebImageDownloaderHighPriority) { |
| | | self.dataTask.priority = NSURLSessionTaskPriorityHigh; |
| | | } else if (self.options & SDWebImageDownloaderLowPriority) { |
| | | self.dataTask.priority = NSURLSessionTaskPriorityLow; |
| | | } |
| | | } |
| | | #pragma clang diagnostic pop |
| | | [self.dataTask resume]; |
| | | for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { |
| | | progressBlock(0, NSURLResponseUnknownLength, self.request.URL); |
| | | } |
| | | __weak typeof(self) weakSelf = self; |
| | | dispatch_async(dispatch_get_main_queue(), ^{ |
| | | [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf]; |
| | | }); |
| | | } else { |
| | | [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]]; |
| | | [self done]; |
| | | return; |
| | | } |
| | | |
| | | #if SD_UIKIT |
| | | Class UIApplicationClass = NSClassFromString(@"UIApplication"); |
| | | if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) { |
| | | return; |
| | | } |
| | | if (self.backgroundTaskId != UIBackgroundTaskInvalid) { |
| | | UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)]; |
| | | [app endBackgroundTask:self.backgroundTaskId]; |
| | | self.backgroundTaskId = UIBackgroundTaskInvalid; |
| | | } |
| | | #endif |
| | | } |
| | | |
| | | - (void)cancel { |
| | | @synchronized (self) { |
| | | [self cancelInternal]; |
| | | } |
| | | } |
| | | |
| | | - (void)cancelInternal { |
| | | if (self.isFinished) return; |
| | | [super cancel]; |
| | | |
| | | if (self.dataTask) { |
| | | [self.dataTask cancel]; |
| | | __weak typeof(self) weakSelf = self; |
| | | dispatch_async(dispatch_get_main_queue(), ^{ |
| | | [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf]; |
| | | }); |
| | | |
| | | // As we cancelled the task, its callback won't be called and thus won't |
| | | // maintain the isFinished and isExecuting flags. |
| | | if (self.isExecuting) self.executing = NO; |
| | | if (!self.isFinished) self.finished = YES; |
| | | } |
| | | |
| | | [self reset]; |
| | | } |
| | | |
| | | - (void)done { |
| | | self.finished = YES; |
| | | self.executing = NO; |
| | | [self reset]; |
| | | } |
| | | |
| | | - (void)reset { |
| | | LOCK(self.callbacksLock); |
| | | [self.callbackBlocks removeAllObjects]; |
| | | UNLOCK(self.callbacksLock); |
| | | self.dataTask = nil; |
| | | |
| | | if (self.ownedSession) { |
| | | [self.ownedSession invalidateAndCancel]; |
| | | self.ownedSession = nil; |
| | | } |
| | | } |
| | | |
| | | - (void)setFinished:(BOOL)finished { |
| | | [self willChangeValueForKey:@"isFinished"]; |
| | | _finished = finished; |
| | | [self didChangeValueForKey:@"isFinished"]; |
| | | } |
| | | |
| | | - (void)setExecuting:(BOOL)executing { |
| | | [self willChangeValueForKey:@"isExecuting"]; |
| | | _executing = executing; |
| | | [self didChangeValueForKey:@"isExecuting"]; |
| | | } |
| | | |
| | | - (BOOL)isConcurrent { |
| | | return YES; |
| | | } |
| | | |
| | | #pragma mark NSURLSessionDataDelegate |
| | | |
| | | - (void)URLSession:(NSURLSession *)session |
| | | dataTask:(NSURLSessionDataTask *)dataTask |
| | | didReceiveResponse:(NSURLResponse *)response |
| | | completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { |
| | | NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow; |
| | | NSInteger expected = (NSInteger)response.expectedContentLength; |
| | | expected = expected > 0 ? expected : 0; |
| | | self.expectedSize = expected; |
| | | self.response = response; |
| | | NSInteger statusCode = [response respondsToSelector:@selector(statusCode)] ? ((NSHTTPURLResponse *)response).statusCode : 200; |
| | | BOOL valid = statusCode < 400; |
| | | //'304 Not Modified' is an exceptional one. It should be treated as cancelled if no cache data |
| | | //URLSession current behavior will return 200 status code when the server respond 304 and URLCache hit. But this is not a standard behavior and we just add a check |
| | | if (statusCode == 304 && !self.cachedData) { |
| | | valid = NO; |
| | | } |
| | | |
| | | if (valid) { |
| | | for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { |
| | | progressBlock(0, expected, self.request.URL); |
| | | } |
| | | } else { |
| | | // Status code invalid and marked as cancelled. Do not call `[self.dataTask cancel]` which may mass up URLSession life cycle |
| | | disposition = NSURLSessionResponseCancel; |
| | | } |
| | | |
| | | __weak typeof(self) weakSelf = self; |
| | | dispatch_async(dispatch_get_main_queue(), ^{ |
| | | [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:weakSelf]; |
| | | }); |
| | | |
| | | if (completionHandler) { |
| | | completionHandler(disposition); |
| | | } |
| | | } |
| | | |
| | | - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { |
| | | if (!self.imageData) { |
| | | self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize]; |
| | | } |
| | | [self.imageData appendData:data]; |
| | | |
| | | if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) { |
| | | // Get the image data |
| | | __block NSData *imageData = [self.imageData copy]; |
| | | // Get the total bytes downloaded |
| | | const NSInteger totalSize = imageData.length; |
| | | // Get the finish status |
| | | BOOL finished = (totalSize >= self.expectedSize); |
| | | |
| | | if (!self.progressiveCoder) { |
| | | // We need to create a new instance for progressive decoding to avoid conflicts |
| | | for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) { |
| | | if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] && |
| | | [((id<SDWebImageProgressiveCoder>)coder) canIncrementallyDecodeFromData:imageData]) { |
| | | self.progressiveCoder = [[[coder class] alloc] init]; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // progressive decode the image in coder queue |
| | | dispatch_async(self.coderQueue, ^{ |
| | | UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished]; |
| | | if (image) { |
| | | NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; |
| | | image = [self scaledImageForKey:key image:image]; |
| | | if (self.shouldDecompressImages) { |
| | | image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}]; |
| | | } |
| | | |
| | | // We do not keep the progressive decoding image even when `finished`=YES. Because they are for view rendering but not take full function from downloader options. And some coders implementation may not keep consistent between progressive decoding and normal decoding. |
| | | |
| | | [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO]; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { |
| | | progressBlock(self.imageData.length, self.expectedSize, self.request.URL); |
| | | } |
| | | } |
| | | |
| | | - (void)URLSession:(NSURLSession *)session |
| | | dataTask:(NSURLSessionDataTask *)dataTask |
| | | willCacheResponse:(NSCachedURLResponse *)proposedResponse |
| | | completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler { |
| | | |
| | | NSCachedURLResponse *cachedResponse = proposedResponse; |
| | | |
| | | if (!(self.options & SDWebImageDownloaderUseNSURLCache)) { |
| | | // Prevents caching of responses |
| | | cachedResponse = nil; |
| | | } |
| | | if (completionHandler) { |
| | | completionHandler(cachedResponse); |
| | | } |
| | | } |
| | | |
| | | #pragma mark NSURLSessionTaskDelegate |
| | | |
| | | - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { |
| | | @synchronized(self) { |
| | | self.dataTask = nil; |
| | | __weak typeof(self) weakSelf = self; |
| | | dispatch_async(dispatch_get_main_queue(), ^{ |
| | | [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf]; |
| | | if (!error) { |
| | | [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:weakSelf]; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // make sure to call `[self done]` to mark operation as finished |
| | | if (error) { |
| | | [self callCompletionBlocksWithError:error]; |
| | | [self done]; |
| | | } else { |
| | | if ([self callbacksForKey:kCompletedCallbackKey].count > 0) { |
| | | /** |
| | | * If you specified to use `NSURLCache`, then the response you get here is what you need. |
| | | */ |
| | | __block NSData *imageData = [self.imageData copy]; |
| | | if (imageData) { |
| | | /** if you specified to only use cached data via `SDWebImageDownloaderIgnoreCachedResponse`, |
| | | * then we should check if the cached data is equal to image data |
| | | */ |
| | | if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) { |
| | | // call completion block with nil |
| | | [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES]; |
| | | [self done]; |
| | | } else { |
| | | // decode the image in coder queue |
| | | dispatch_async(self.coderQueue, ^{ |
| | | UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData]; |
| | | NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; |
| | | image = [self scaledImageForKey:key image:image]; |
| | | |
| | | BOOL shouldDecode = YES; |
| | | // Do not force decoding animated GIFs and WebPs |
| | | if (image.images) { |
| | | shouldDecode = NO; |
| | | } else { |
| | | #ifdef SD_WEBP |
| | | SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData]; |
| | | if (imageFormat == SDImageFormatWebP) { |
| | | shouldDecode = NO; |
| | | } |
| | | #endif |
| | | } |
| | | |
| | | if (shouldDecode) { |
| | | if (self.shouldDecompressImages) { |
| | | BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages; |
| | | image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}]; |
| | | } |
| | | } |
| | | CGSize imageSize = image.size; |
| | | if (imageSize.width == 0 || imageSize.height == 0) { |
| | | [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]]; |
| | | } else { |
| | | [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES]; |
| | | } |
| | | [self done]; |
| | | }); |
| | | } |
| | | } else { |
| | | [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]]; |
| | | [self done]; |
| | | } |
| | | } else { |
| | | [self done]; |
| | | } |
| | | } |
| | | } |
| | | |
| | | - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { |
| | | |
| | | NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling; |
| | | __block NSURLCredential *credential = nil; |
| | | |
| | | if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { |
| | | if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) { |
| | | disposition = NSURLSessionAuthChallengePerformDefaultHandling; |
| | | } else { |
| | | credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; |
| | | disposition = NSURLSessionAuthChallengeUseCredential; |
| | | } |
| | | } else { |
| | | if (challenge.previousFailureCount == 0) { |
| | | if (self.credential) { |
| | | credential = self.credential; |
| | | disposition = NSURLSessionAuthChallengeUseCredential; |
| | | } else { |
| | | disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge; |
| | | } |
| | | } else { |
| | | disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge; |
| | | } |
| | | } |
| | | |
| | | if (completionHandler) { |
| | | completionHandler(disposition, credential); |
| | | } |
| | | } |
| | | |
| | | #pragma mark Helper methods |
| | | - (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image { |
| | | return SDScaledImageForKey(key, image); |
| | | } |
| | | |
| | | - (BOOL)shouldContinueWhenAppEntersBackground { |
| | | return self.options & SDWebImageDownloaderContinueInBackground; |
| | | } |
| | | |
| | | - (void)callCompletionBlocksWithError:(nullable NSError *)error { |
| | | [self callCompletionBlocksWithImage:nil imageData:nil error:error finished:YES]; |
| | | } |
| | | |
| | | - (void)callCompletionBlocksWithImage:(nullable UIImage *)image |
| | | imageData:(nullable NSData *)imageData |
| | | error:(nullable NSError *)error |
| | | finished:(BOOL)finished { |
| | | NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey]; |
| | | dispatch_main_async_safe(^{ |
| | | for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) { |
| | | completedBlock(image, imageData, error, finished); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | @end |