123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- /*
- * 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 "SDWebImageManager.h"
- #import "NSImage+WebCache.h"
- #import "SDWebImageCodersManager.h"
- #import "UIImage+MultiFormat.h"
- #define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
- #define UNLOCK(lock) dispatch_semaphore_signal(lock);
- // 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
- 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; // for `SDWebImageDownloaderIgnoreCachedResponse`
- // 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
- @property (strong, nonatomic, nullable) id<SDWebImageProgressiveCoder> progressiveCoder;
- @end
- @implementation SDWebImageDownloaderOperation
- @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;
- _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];
- LOCK(self.callbacksLock);
- [self.callbackBlocks addObject:callbacks];
- UNLOCK(self.callbacksLock);
- return callbacks;
- }
- - (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
- LOCK(self.callbacksLock);
- NSMutableArray<id> *callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
- 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;
- LOCK(self.callbacksLock);
- [self.callbackBlocks removeObjectIdenticalTo:token];
- if (self.callbackBlocks.count == 0) {
- shouldCancel = YES;
- }
- 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:NSURLErrorDomain code:NSURLErrorUnknown 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 {
- LOCK(self.callbacksLock);
- [self.callbackBlocks removeAllObjects];
- 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 < 400;
- //'304 Not Modified' is an exceptional one. It should be treated as cancelled if no cache data
- //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;
- }
-
- 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];
- if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
- // Get the image data
- __block 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 (!self.progressiveCoder) {
- // We need to create a new instance for progressive decoding to avoid conflicts
- for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) {
- if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
- [((id<SDWebImageProgressiveCoder>)coder) canIncrementallyDecodeFromData:imageData]) {
- self.progressiveCoder = [[[coder class] alloc] init];
- break;
- }
- }
- }
-
- // progressive decode the image in coder queue
- dispatch_async(self.coderQueue, ^{
- @autoreleasepool {
- UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
- if (image) {
- NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
- image = [self scaledImageForKey:key image:image];
- if (self.shouldDecompressImages) {
- image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
- }
-
- // 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.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.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) {
- [self callCompletionBlocksWithError:error];
- [self done];
- } else {
- if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
- /**
- * If you specified to use `NSURLCache`, then the response you get here is what you need.
- */
- __block NSData *imageData = [self.imageData copy];
- self.imageData = nil;
- if (imageData) {
- if (self.options & SDWebImageDownloaderGifIncluded && imageData) {
- [self callCompletionBlocksWithImage:nil imageData:imageData error:nil finished:YES];
- [self done];
- return;
- }
- /** 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];
- [self done];
- } else {
- // decode the image in coder queue
- dispatch_async(self.coderQueue, ^{
- @autoreleasepool {
- UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
- NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
- image = [self scaledImageForKey:key image:image];
-
- // Do not force decoding animated images or GIF,
- // because there has imageCoder which can change `image` or `imageData` to static image, lose the animated feature totally.
- BOOL shouldDecode = !image.images && image.sd_imageFormat != SDImageFormatGIF;
- if (shouldDecode) {
- if (self.shouldDecompressImages) {
- BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
- image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
- }
- }
- CGSize imageSize = image.size;
- if (imageSize.width == 0 || imageSize.height == 0) {
- [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 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:0 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
- - (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
|