| /* |
| * 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 "SDWebImageDecoder.h" |
| #import "UIImage+MultiFormat.h" |
| #import <ImageIO/ImageIO.h> |
| #import "SDWebImageManager.h" |
| |
| NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification"; |
| NSString *const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification"; |
| NSString *const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification"; |
| NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification"; |
| |
| @interface SDWebImageDownloaderOperation () <NSURLConnectionDataDelegate> |
| |
| @property (copy, nonatomic) SDWebImageDownloaderProgressBlock progressBlock; |
| @property (copy, nonatomic) SDWebImageDownloaderCompletedBlock completedBlock; |
| @property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock; |
| |
| @property (assign, nonatomic, getter = isExecuting) BOOL executing; |
| @property (assign, nonatomic, getter = isFinished) BOOL finished; |
| @property (strong, nonatomic) NSMutableData *imageData; |
| @property (strong, nonatomic) NSURLConnection *connection; |
| @property (strong, atomic) NSThread *thread; |
| |
| #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 |
| @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; |
| #endif |
| |
| @end |
| |
| @implementation SDWebImageDownloaderOperation { |
| size_t width, height; |
| UIImageOrientation orientation; |
| BOOL responseFromCached; |
| } |
| |
| @synthesize executing = _executing; |
| @synthesize finished = _finished; |
| |
| - (id)initWithRequest:(NSURLRequest *)request |
| options:(SDWebImageDownloaderOptions)options |
| progress:(SDWebImageDownloaderProgressBlock)progressBlock |
| completed:(SDWebImageDownloaderCompletedBlock)completedBlock |
| cancelled:(SDWebImageNoParamsBlock)cancelBlock { |
| if ((self = [super init])) { |
| _request = request; |
| _shouldDecompressImages = YES; |
| _shouldUseCredentialStorage = YES; |
| _options = options; |
| _progressBlock = [progressBlock copy]; |
| _completedBlock = [completedBlock copy]; |
| _cancelBlock = [cancelBlock copy]; |
| _executing = NO; |
| _finished = NO; |
| _expectedSize = 0; |
| responseFromCached = YES; // Initially wrong until `connection:willCacheResponse:` is called or not called |
| } |
| return self; |
| } |
| |
| - (void)start { |
| @synchronized (self) { |
| if (self.isCancelled) { |
| self.finished = YES; |
| [self reset]; |
| return; |
| } |
| |
| #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 |
| 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 |
| |
| self.executing = YES; |
| self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; |
| self.thread = [NSThread currentThread]; |
| } |
| |
| [self.connection start]; |
| |
| if (self.connection) { |
| if (self.progressBlock) { |
| self.progressBlock(0, NSURLResponseUnknownLength); |
| } |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self]; |
| }); |
| |
| if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) { |
| // Make sure to run the runloop in our background thread so it can process downloaded data |
| // Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5 |
| // not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466) |
| CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false); |
| } |
| else { |
| CFRunLoopRun(); |
| } |
| |
| if (!self.isFinished) { |
| [self.connection cancel]; |
| [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]]; |
| } |
| } |
| else { |
| if (self.completedBlock) { |
| self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES); |
| } |
| } |
| |
| #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 |
| 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) { |
| if (self.thread) { |
| [self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO]; |
| } |
| else { |
| [self cancelInternal]; |
| } |
| } |
| } |
| |
| - (void)cancelInternalAndStop { |
| if (self.isFinished) return; |
| [self cancelInternal]; |
| CFRunLoopStop(CFRunLoopGetCurrent()); |
| } |
| |
| - (void)cancelInternal { |
| if (self.isFinished) return; |
| [super cancel]; |
| if (self.cancelBlock) self.cancelBlock(); |
| |
| if (self.connection) { |
| [self.connection cancel]; |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; |
| }); |
| |
| // 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 { |
| self.cancelBlock = nil; |
| self.completedBlock = nil; |
| self.progressBlock = nil; |
| self.connection = nil; |
| self.imageData = nil; |
| self.thread = 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 NSURLConnection (delegate) |
| |
| - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { |
| |
| //'304 Not Modified' is an exceptional one |
| if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) { |
| NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0; |
| self.expectedSize = expected; |
| if (self.progressBlock) { |
| self.progressBlock(0, expected); |
| } |
| |
| self.imageData = [[NSMutableData alloc] initWithCapacity:expected]; |
| self.response = response; |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self]; |
| }); |
| } |
| 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.connection cancel]; |
| } |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; |
| }); |
| |
| if (self.completedBlock) { |
| self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES); |
| } |
| CFRunLoopStop(CFRunLoopGetCurrent()); |
| [self done]; |
| } |
| } |
| |
| - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { |
| [self.imageData appendData:data]; |
| |
| if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) { |
| // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/ |
| // Thanks to the author @Nyx0uf |
| |
| // Get the total bytes downloaded |
| const NSInteger totalSize = self.imageData.length; |
| |
| // Update the data source, we must pass ALL the data, not just the new bytes |
| CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL); |
| |
| 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 connectionDidFinishLoading.) So save it here and pass it on later. |
| orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)]; |
| } |
| |
| } |
| |
| if (width + height > 0 && totalSize < self.expectedSize) { |
| // Create the image |
| CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); |
| |
| #ifdef TARGET_OS_IPHONE |
| // 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) { |
| UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation]; |
| 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; |
| } |
| CGImageRelease(partialImageRef); |
| dispatch_main_sync_safe(^{ |
| if (self.completedBlock) { |
| self.completedBlock(image, nil, nil, NO); |
| } |
| }); |
| } |
| } |
| |
| CFRelease(imageSource); |
| } |
| |
| if (self.progressBlock) { |
| self.progressBlock(self.imageData.length, self.expectedSize); |
| } |
| } |
| |
| + (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; |
| } |
| } |
| |
| - (UIImage *)scaledImageForKey:(NSString *)key image:(UIImage *)image { |
| return SDScaledImageForKey(key, image); |
| } |
| |
| - (void)connectionDidFinishLoading:(NSURLConnection *)aConnection { |
| SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock; |
| @synchronized(self) { |
| CFRunLoopStop(CFRunLoopGetCurrent()); |
| self.thread = nil; |
| self.connection = nil; |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; |
| [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self]; |
| }); |
| } |
| |
| if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) { |
| responseFromCached = NO; |
| } |
| |
| if (completionBlock) { |
| if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) { |
| completionBlock(nil, nil, nil, YES); |
| } else if (self.imageData) { |
| UIImage *image = [UIImage sd_imageWithData:self.imageData]; |
| NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; |
| image = [self scaledImageForKey:key image:image]; |
| |
| // Do not force decoding animated GIFs |
| if (!image.images) { |
| if (self.shouldDecompressImages) { |
| image = [UIImage decodedImageWithImage:image]; |
| } |
| } |
| if (CGSizeEqualToSize(image.size, CGSizeZero)) { |
| completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES); |
| } |
| else { |
| completionBlock(image, self.imageData, nil, YES); |
| } |
| } else { |
| completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES); |
| } |
| } |
| self.completionBlock = nil; |
| [self done]; |
| } |
| |
| - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { |
| @synchronized(self) { |
| CFRunLoopStop(CFRunLoopGetCurrent()); |
| self.thread = nil; |
| self.connection = nil; |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; |
| }); |
| } |
| |
| if (self.completedBlock) { |
| self.completedBlock(nil, nil, error, YES); |
| } |
| self.completionBlock = nil; |
| [self done]; |
| } |
| |
| - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse { |
| responseFromCached = NO; // If this method is called, it means the response wasn't read from cache |
| if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) { |
| // Prevents caching of responses |
| return nil; |
| } |
| else { |
| return cachedResponse; |
| } |
| } |
| |
| - (BOOL)shouldContinueWhenAppEntersBackground { |
| return self.options & SDWebImageDownloaderContinueInBackground; |
| } |
| |
| - (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection __unused *)connection { |
| return self.shouldUseCredentialStorage; |
| } |
| |
| - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{ |
| if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { |
| if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates) && |
| [challenge.sender respondsToSelector:@selector(performDefaultHandlingForAuthenticationChallenge:)]) { |
| [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge]; |
| } else { |
| NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; |
| [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge]; |
| } |
| } else { |
| if ([challenge previousFailureCount] == 0) { |
| if (self.credential) { |
| [[challenge sender] useCredential:self.credential forAuthenticationChallenge:challenge]; |
| } else { |
| [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge]; |
| } |
| } else { |
| [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge]; |
| } |
| } |
| } |
| |
| @end |