/*
|
* 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 ()
|
|
@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;
|
|
// 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) NSURLSession *unownedSession;
|
// This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
|
@property (strong, nonatomic) NSURLSession *ownedSession;
|
|
@property (strong, nonatomic, readwrite) NSURLSessionTask *dataTask;
|
|
@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 {
|
|
return [self initWithRequest:request
|
inSession:nil
|
options:options
|
progress:progressBlock
|
completed:completedBlock
|
cancelled:cancelBlock];
|
}
|
|
- (id)initWithRequest:(NSURLRequest *)request
|
inSession:(NSURLSession *)session
|
options:(SDWebImageDownloaderOptions)options
|
progress:(SDWebImageDownloaderProgressBlock)progressBlock
|
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
|
cancelled:(SDWebImageNoParamsBlock)cancelBlock {
|
if ((self = [super init])) {
|
_request = request;
|
_shouldDecompressImages = YES;
|
_options = options;
|
_progressBlock = [progressBlock copy];
|
_completedBlock = [completedBlock copy];
|
_cancelBlock = [cancelBlock copy];
|
_executing = NO;
|
_finished = NO;
|
_expectedSize = 0;
|
_unownedSession = session;
|
responseFromCached = YES; // Initially wrong until `- URLSession:dataTask:willCacheResponse:completionHandler: 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
|
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.thread = [NSThread currentThread];
|
}
|
|
[self.dataTask resume];
|
|
if (self.dataTask) {
|
if (self.progressBlock) {
|
self.progressBlock(0, NSURLResponseUnknownLength);
|
}
|
dispatch_async(dispatch_get_main_queue(), ^{
|
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
|
});
|
}
|
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];
|
}
|
|
- (void)cancelInternal {
|
if (self.isFinished) return;
|
[super cancel];
|
if (self.cancelBlock) self.cancelBlock();
|
|
if (self.dataTask) {
|
[self.dataTask 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.dataTask = nil;
|
self.imageData = nil;
|
self.thread = 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 = 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.dataTask 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);
|
}
|
[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 && 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 didCompleteWithError.) 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);
|
}
|
}
|
|
- (void)URLSession:(NSURLSession *)session
|
dataTask:(NSURLSessionDataTask *)dataTask
|
willCacheResponse:(NSCachedURLResponse *)proposedResponse
|
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
|
|
responseFromCached = NO; // If this method is called, it means the response wasn't read from cache
|
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.thread = nil;
|
self.dataTask = nil;
|
dispatch_async(dispatch_get_main_queue(), ^{
|
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
|
if (!error) {
|
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
|
}
|
});
|
}
|
|
if (error) {
|
if (self.completedBlock) {
|
self.completedBlock(nil, nil, error, YES);
|
}
|
} else {
|
SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
|
|
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)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
|
|
+ (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);
|
}
|
|
- (BOOL)shouldContinueWhenAppEntersBackground {
|
return self.options & SDWebImageDownloaderContinueInBackground;
|
}
|
|
@end
|