/* * 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 "SDWebImageDecoder.h" #import "UIImage+MultiFormat.h" #import #import "SDWebImageManager.h" #import "NSImage+WebCache.h" 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 SDCallbacksDictionary; @interface SDWebImageDownloaderOperation () @property (strong, nonatomic, nonnull) NSMutableArray *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; // 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 (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t barrierQueue; #if SD_UIKIT @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; #endif @end @implementation SDWebImageDownloaderOperation { size_t _width, _height; #if SD_UIKIT || SD_WATCH UIImageOrientation _orientation; #endif CGImageSourceRef _imageSource; } @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; _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT); } return self; } - (void)dealloc { SDDispatchQueueRelease(_barrierQueue); if (_imageSource) { CFRelease(_imageSource); _imageSource = NULL; } } - (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]; dispatch_barrier_async(self.barrierQueue, ^{ [self.callbackBlocks addObject:callbacks]; }); return callbacks; } - (nullable NSArray *)callbacksForKey:(NSString *)key { __block NSMutableArray *callbacks = nil; dispatch_sync(self.barrierQueue, ^{ // We need to remove [NSNull null] because there might not always be a progress block for each callback callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy]; [callbacks removeObjectIdenticalTo:[NSNull null]]; }); return [callbacks copy]; // strip mutability here } - (BOOL)cancel:(nullable id)token { __block BOOL shouldCancel = NO; dispatch_barrier_sync(self.barrierQueue, ^{ [self.callbackBlocks removeObjectIdenticalTo:token]; if (self.callbackBlocks.count == 0) { shouldCancel = YES; } }); 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 if (self.options & SDWebImageDownloaderIgnoreCachedResponse) { // Grab the cached data for later check NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]; if (cachedResponse) { self.cachedData = cachedResponse.data; } } NSURLSession *session = self.unownedSession; if (!self.unownedSession) { 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. */ self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil]; session = self.ownedSession; } self.dataTask = [session dataTaskWithRequest:self.request]; self.executing = YES; } [self.dataTask resume]; if (self.dataTask) { 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:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]]; } #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 connection, 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 { __weak typeof(self) weakSelf = self; dispatch_barrier_async(self.barrierQueue, ^{ [weakSelf.callbackBlocks removeAllObjects]; }); self.dataTask = nil; NSOperationQueue *delegateQueue; if (self.unownedSession) { delegateQueue = self.unownedSession.delegateQueue; } else { delegateQueue = self.ownedSession.delegateQueue; } if (delegateQueue) { NSAssert(delegateQueue.maxConcurrentOperationCount == 1, @"NSURLSession delegate queue should be a serial queue"); [delegateQueue addOperationWithBlock:^{ weakSelf.imageData = 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 { //'304 Not Modified' is an exceptional one if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode < 400 && ((NSHTTPURLResponse *)response).statusCode != 304)) { NSInteger expected = (NSInteger)response.expectedContentLength; expected = expected > 0 ? expected : 0; self.expectedSize = expected; for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { progressBlock(0, expected, self.request.URL); } self.imageData = [[NSMutableData alloc] initWithCapacity:expected]; self.response = response; __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:weakSelf]; }); } else { NSUInteger code = ((NSHTTPURLResponse *)response).statusCode; //This is the case when server returns '304 Not Modified'. It means that remote image is not changed. //In case of 304 we need just cancel the operation and return cached image from the cache. if (code == 304) { [self cancelInternal]; } else { [self.dataTask cancel]; } __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf]; }); [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:((NSHTTPURLResponse *)response).statusCode userInfo:nil]]; [self done]; } if (completionHandler) { completionHandler(NSURLSessionResponseAllow); } } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { [self.imageData appendData:data]; if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) { // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/ // Thanks to the author @Nyx0uf // Get the image data 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 (!_imageSource) { _imageSource = CGImageSourceCreateIncremental(NULL); } // Update the data source, we must pass ALL the data, not just the new bytes CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)imageData, finished); if (_width + _height == 0) { CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL); if (properties) { NSInteger orientationValue = -1; CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight); if (val) CFNumberGetValue(val, kCFNumberLongType, &_height); val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth); if (val) CFNumberGetValue(val, kCFNumberLongType, &_width); val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue); CFRelease(properties); // When we draw to Core Graphics, we lose orientation information, // which means the image below born of initWithCGIImage will be // oriented incorrectly sometimes. (Unlike the image born of initWithData // in didCompleteWithError.) So save it here and pass it on later. #if SD_UIKIT || SD_WATCH _orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)]; #endif } } if (_width + _height > 0 && !finished) { // Create the image CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL); #if SD_UIKIT || SD_WATCH // Workaround for iOS anamorphic image if (partialImageRef) { const size_t partialHeight = CGImageGetHeight(partialImageRef); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef bmContext = CGBitmapContextCreate(NULL, _width, _height, 8, _width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); CGColorSpaceRelease(colorSpace); if (bmContext) { CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = _width, .size.height = partialHeight}, partialImageRef); CGImageRelease(partialImageRef); partialImageRef = CGBitmapContextCreateImage(bmContext); CGContextRelease(bmContext); } else { CGImageRelease(partialImageRef); partialImageRef = nil; } } #endif if (partialImageRef) { #if SD_UIKIT || SD_WATCH UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:_orientation]; #elif SD_MAC UIImage *image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize]; #endif CGImageRelease(partialImageRef); NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; UIImage *scaledImage = [self scaledImageForKey:key image:image]; if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:scaledImage]; } else { image = scaledImage; } [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO]; } } if (finished) { if (_imageSource) { CFRelease(_imageSource); _imageSource = NULL; } } } 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.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) { // 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]; } }); } if (error) { [self callCompletionBlocksWithError:error]; } else { if ([self callbacksForKey:kCompletedCallbackKey].count > 0) { /** * If you specified to use `NSURLCache`, then the response you get here is what you need. */ NSData *imageData = [self.imageData copy]; if (imageData) { UIImage *image = [UIImage sd_imageWithData: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]; } else { 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) { if (self.options & SDWebImageDownloaderScaleDownLargeImages) { #if SD_UIKIT || SD_WATCH image = [UIImage decodedAndScaledDownImageWithImage:image]; imageData = UIImagePNGRepresentation(image); #endif } else { image = [UIImage decodedImageWithImage:image]; } } } if (CGSizeEqualToSize(image.size, CGSizeZero)) { [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]]; } else { [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES]; } } } else { [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]]; } } } [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 #if SD_UIKIT || SD_WATCH + (UIImageOrientation)orientationFromPropertyValue:(NSInteger)value { switch (value) { case 1: return UIImageOrientationUp; case 3: return UIImageOrientationDown; case 8: return UIImageOrientationLeft; case 6: return UIImageOrientationRight; case 2: return UIImageOrientationUpMirrored; case 4: return UIImageOrientationDownMirrored; case 5: return UIImageOrientationLeftMirrored; case 7: return UIImageOrientationRightMirrored; default: return UIImageOrientationUp; } } #endif - (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 *completionBlocks = [self callbacksForKey:kCompletedCallbackKey]; dispatch_main_async_safe(^{ for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) { completedBlock(image, imageData, error, finished); } }); } @end