123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597 |
- /*
- * 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"
- #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<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;
- // 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<id> *)callbacksForKey:(NSString *)key {
- __block NSMutableArray<id> *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<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
- dispatch_main_async_safe(^{
- for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
- completedBlock(image, imageData, error, finished);
- }
- });
- }
- @end
|