/* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageDownloaderOperation.h" #import "SDWebImageError.h" #import "SDInternalMacros.h" // 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 static NSString *const kProgressCallbackKey = @"progress"; static NSString *const kCompletedCallbackKey = @"completed"; typedef NSMutableDictionary SDCallbacksDictionary; @interface SDWebImageDownloaderOperation () @property (strong, nonatomic, nonnull) NSMutableArray *callbackBlocks; @property (assign, nonatomic, readwrite) SDWebImageDownloaderOptions options; @property (copy, nonatomic, readwrite, nullable) SDWebImageContext *context; @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` @property (assign, nonatomic) NSUInteger expectedSize; // may be 0 @property (assign, nonatomic) NSUInteger receivedSize; @property (strong, nonatomic, nullable, readwrite) NSURLResponse *response; @property (strong, nonatomic, nullable) NSError *responseError; @property (assign, nonatomic) double previousProgress; // previous progress percent // 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 @end @implementation SDWebImageDownloaderOperation @synthesize executing = _executing; @synthesize finished = _finished; - (nonnull instancetype)init { return [self initWithRequest:nil inSession:nil options:0]; } - (instancetype)initWithRequest:(NSURLRequest *)request inSession:(NSURLSession *)session options:(SDWebImageDownloaderOptions)options { return [self initWithRequest:request inSession:session options:options context:nil]; } - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request inSession:(nullable NSURLSession *)session options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context { if ((self = [super init])) { _request = [request copy]; _options = options; _context = [context copy]; _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); #if SD_UIKIT _backgroundTaskId = UIBackgroundTaskInvalid; #endif } 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]; SD_LOCK(self.callbacksLock); [self.callbackBlocks addObject:callbacks]; SD_UNLOCK(self.callbacksLock); return callbacks; } - (nullable NSArray *)callbacksForKey:(NSString *)key { SD_LOCK(self.callbacksLock); NSMutableArray *callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy]; SD_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; SD_LOCK(self.callbacksLock); [self.callbackBlocks removeObjectIdenticalTo:token]; if (self.callbackBlocks.count == 0) { shouldCancel = YES; } SD_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:^{ [wself cancel]; }]; } #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); } __block typeof(self) strongSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf]; }); } else { [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]]; [self done]; } } - (void)cancel { @synchronized (self) { [self cancelInternal]; } } - (void)cancelInternal { if (self.isFinished) return; [super cancel]; if (self.dataTask) { [self.dataTask cancel]; __block typeof(self) strongSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf]; }); // 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 { SD_LOCK(self.callbacksLock); [self.callbackBlocks removeAllObjects]; SD_UNLOCK(self.callbacksLock); @synchronized (self) { self.dataTask = nil; if (self.ownedSession) { [self.ownedSession invalidateAndCancel]; self.ownedSession = nil; } #if SD_UIKIT if (self.backgroundTaskId != UIBackgroundTaskInvalid) { // If backgroundTaskId != UIBackgroundTaskInvalid, sharedApplication is always exist UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)]; [app endBackgroundTask:self.backgroundTaskId]; self.backgroundTaskId = UIBackgroundTaskInvalid; } #endif } } - (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 >= 200 && statusCode < 400; if (!valid) { self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadStatusCode userInfo:@{SDWebImageErrorDownloadStatusCodeKey : @(statusCode)}]; } //'304 Not Modified' is an exceptional one //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; self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:nil]; } 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; } __block typeof(self) strongSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:strongSelf]; }); 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]; self.receivedSize = self.imageData.length; if (self.expectedSize == 0) { // Unknown expectedSize, immediately call progressBlock and return for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { progressBlock(self.receivedSize, self.expectedSize, self.request.URL); } return; } // Get the finish status BOOL finished = (self.receivedSize >= self.expectedSize); // Get the current progress double currentProgress = (double)self.receivedSize / (double)self.expectedSize; double previousProgress = self.previousProgress; double progressInterval = currentProgress - previousProgress; // Check if we need callback progress if (!finished && (progressInterval < self.minimumProgressInterval)) { return; } self.previousProgress = currentProgress; if (self.options & SDWebImageDownloaderProgressiveLoad) { // Get the image data NSData *imageData = [self.imageData copy]; // progressive decode the image in coder queue dispatch_async(self.coderQueue, ^{ @autoreleasepool { UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, finished, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context); if (image) { // 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.receivedSize, 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; __block typeof(self) strongSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf]; if (!error) { [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:strongSelf]; } }); } // make sure to call `[self done]` to mark operation as finished if (error) { // custom error instead of URLSession error if (self.responseError) { error = self.responseError; } [self callCompletionBlocksWithError:error]; [self done]; } else { if ([self callbacksForKey:kCompletedCallbackKey].count > 0) { NSData *imageData = [self.imageData copy]; self.imageData = nil; 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]) { self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:nil]; // call completion block with not modified error [self callCompletionBlocksWithError:self.responseError]; [self done]; } else { // decode the image in coder queue dispatch_async(self.coderQueue, ^{ @autoreleasepool { UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context); CGSize imageSize = image.size; if (imageSize.width == 0 || imageSize.height == 0) { [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData 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:SDWebImageErrorBadImageData 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 + (SDWebImageOptions)imageOptionsFromDownloaderOptions:(SDWebImageDownloaderOptions)downloadOptions { SDWebImageOptions options = 0; if (downloadOptions & SDWebImageDownloaderScaleDownLargeImages) options |= SDWebImageScaleDownLargeImages; if (downloadOptions & SDWebImageDownloaderDecodeFirstFrameOnly) options |= SDWebImageDecodeFirstFrameOnly; if (downloadOptions & SDWebImageDownloaderPreloadAllFrames) options |= SDWebImagePreloadAllFrames; if (downloadOptions & SDWebImageDownloaderAvoidDecodeImage) options |= SDWebImageAvoidDecodeImage; return options; } - (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 *completionBlocks = [self callbacksForKey:kCompletedCallbackKey]; dispatch_main_async_safe(^{ for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) { completedBlock(image, imageData, error, finished); } }); } @end