123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 |
- // AFImageDownloader.m
- // Copyright (c) 2011–2016 Alamofire Software Foundation ( http://alamofire.org/ )
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- #import <TargetConditionals.h>
- #if TARGET_OS_IOS || TARGET_OS_TV
- #import "AFImageDownloader.h"
- #import "AFHTTPSessionManager.h"
- @interface AFImageDownloaderResponseHandler : NSObject
- @property (nonatomic, strong) NSUUID *uuid;
- @property (nonatomic, copy) void (^successBlock)(NSURLRequest *, NSHTTPURLResponse *, UIImage *);
- @property (nonatomic, copy) void (^failureBlock)(NSURLRequest *, NSHTTPURLResponse *, NSError *);
- @end
- @implementation AFImageDownloaderResponseHandler
- - (instancetype)initWithUUID:(NSUUID *)uuid
- success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))success
- failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure {
- if (self = [self init]) {
- self.uuid = uuid;
- self.successBlock = success;
- self.failureBlock = failure;
- }
- return self;
- }
- - (NSString *)description {
- return [NSString stringWithFormat: @"<AFImageDownloaderResponseHandler>UUID: %@", [self.uuid UUIDString]];
- }
- @end
- @interface AFImageDownloaderMergedTask : NSObject
- @property (nonatomic, strong) NSString *URLIdentifier;
- @property (nonatomic, strong) NSUUID *identifier;
- @property (nonatomic, strong) NSURLSessionDataTask *task;
- @property (nonatomic, strong) NSMutableArray <AFImageDownloaderResponseHandler*> *responseHandlers;
- @end
- @implementation AFImageDownloaderMergedTask
- - (instancetype)initWithURLIdentifier:(NSString *)URLIdentifier identifier:(NSUUID *)identifier task:(NSURLSessionDataTask *)task {
- if (self = [self init]) {
- self.URLIdentifier = URLIdentifier;
- self.task = task;
- self.identifier = identifier;
- self.responseHandlers = [[NSMutableArray alloc] init];
- }
- return self;
- }
- - (void)addResponseHandler:(AFImageDownloaderResponseHandler *)handler {
- [self.responseHandlers addObject:handler];
- }
- - (void)removeResponseHandler:(AFImageDownloaderResponseHandler *)handler {
- [self.responseHandlers removeObject:handler];
- }
- @end
- @implementation AFImageDownloadReceipt
- - (instancetype)initWithReceiptID:(NSUUID *)receiptID task:(NSURLSessionDataTask *)task {
- if (self = [self init]) {
- self.receiptID = receiptID;
- self.task = task;
- }
- return self;
- }
- @end
- @interface AFImageDownloader ()
- @property (nonatomic, strong) dispatch_queue_t synchronizationQueue;
- @property (nonatomic, strong) dispatch_queue_t responseQueue;
- @property (nonatomic, assign) NSInteger maximumActiveDownloads;
- @property (nonatomic, assign) NSInteger activeRequestCount;
- @property (nonatomic, strong) NSMutableArray *queuedMergedTasks;
- @property (nonatomic, strong) NSMutableDictionary *mergedTasks;
- @end
- @implementation AFImageDownloader
- + (NSURLCache *)defaultURLCache {
- NSUInteger memoryCapacity = 20 * 1024 * 1024; // 20MB
- NSUInteger diskCapacity = 150 * 1024 * 1024; // 150MB
- NSURL *cacheURL = [[[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory
- inDomain:NSUserDomainMask
- appropriateForURL:nil
- create:YES
- error:nil]
- URLByAppendingPathComponent:@"com.alamofire.imagedownloader"];
-
- #if TARGET_OS_MACCATALYST
- return [[NSURLCache alloc] initWithMemoryCapacity:memoryCapacity
- diskCapacity:diskCapacity
- directoryURL:cacheURL];
- #else
- return [[NSURLCache alloc] initWithMemoryCapacity:memoryCapacity
- diskCapacity:diskCapacity
- diskPath:[cacheURL path]];
- #endif
- }
- + (NSURLSessionConfiguration *)defaultURLSessionConfiguration {
- NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
- //TODO set the default HTTP headers
- configuration.HTTPShouldSetCookies = YES;
- configuration.HTTPShouldUsePipelining = NO;
- configuration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
- configuration.allowsCellularAccess = YES;
- configuration.timeoutIntervalForRequest = 60.0;
- configuration.URLCache = [AFImageDownloader defaultURLCache];
- return configuration;
- }
- - (instancetype)init {
- NSURLSessionConfiguration *defaultConfiguration = [self.class defaultURLSessionConfiguration];
- return [self initWithSessionConfiguration:defaultConfiguration];
- }
- - (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
- AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration];
- sessionManager.responseSerializer = [AFImageResponseSerializer serializer];
- return [self initWithSessionManager:sessionManager
- downloadPrioritization:AFImageDownloadPrioritizationFIFO
- maximumActiveDownloads:4
- imageCache:[[AFAutoPurgingImageCache alloc] init]];
- }
- - (instancetype)initWithSessionManager:(AFHTTPSessionManager *)sessionManager
- downloadPrioritization:(AFImageDownloadPrioritization)downloadPrioritization
- maximumActiveDownloads:(NSInteger)maximumActiveDownloads
- imageCache:(id <AFImageRequestCache>)imageCache {
- if (self = [super init]) {
- self.sessionManager = sessionManager;
- self.downloadPrioritization = downloadPrioritization;
- self.maximumActiveDownloads = maximumActiveDownloads;
- self.imageCache = imageCache;
- self.queuedMergedTasks = [[NSMutableArray alloc] init];
- self.mergedTasks = [[NSMutableDictionary alloc] init];
- self.activeRequestCount = 0;
- NSString *name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.synchronizationqueue-%@", [[NSUUID UUID] UUIDString]];
- self.synchronizationQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL);
- name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.responsequeue-%@", [[NSUUID UUID] UUIDString]];
- self.responseQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);
- }
- return self;
- }
- + (instancetype)defaultInstance {
- static AFImageDownloader *sharedInstance = nil;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- sharedInstance = [[self alloc] init];
- });
- return sharedInstance;
- }
- - (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
- success:(void (^)(NSURLRequest * _Nonnull, NSHTTPURLResponse * _Nullable, UIImage * _Nonnull))success
- failure:(void (^)(NSURLRequest * _Nonnull, NSHTTPURLResponse * _Nullable, NSError * _Nonnull))failure {
- return [self downloadImageForURLRequest:request withReceiptID:[NSUUID UUID] success:success failure:failure];
- }
- - (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request
- withReceiptID:(nonnull NSUUID *)receiptID
- success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))success
- failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure {
- __block NSURLSessionDataTask *task = nil;
- dispatch_sync(self.synchronizationQueue, ^{
- NSString *URLIdentifier = request.URL.absoluteString;
- if (URLIdentifier == nil) {
- if (failure) {
- NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil];
- dispatch_async(dispatch_get_main_queue(), ^{
- failure(request, nil, error);
- });
- }
- return;
- }
- // 1) Append the success and failure blocks to a pre-existing request if it already exists
- AFImageDownloaderMergedTask *existingMergedTask = self.mergedTasks[URLIdentifier];
- if (existingMergedTask != nil) {
- AFImageDownloaderResponseHandler *handler = [[AFImageDownloaderResponseHandler alloc] initWithUUID:receiptID success:success failure:failure];
- [existingMergedTask addResponseHandler:handler];
- task = existingMergedTask.task;
- return;
- }
- // 2) Attempt to load the image from the image cache if the cache policy allows it
- switch (request.cachePolicy) {
- case NSURLRequestUseProtocolCachePolicy:
- case NSURLRequestReturnCacheDataElseLoad:
- case NSURLRequestReturnCacheDataDontLoad: {
- UIImage *cachedImage = [self.imageCache imageforRequest:request withAdditionalIdentifier:nil];
- if (cachedImage != nil) {
- if (success) {
- dispatch_async(dispatch_get_main_queue(), ^{
- success(request, nil, cachedImage);
- });
- }
- return;
- }
- break;
- }
- default:
- break;
- }
- // 3) Create the request and set up authentication, validation and response serialization
- NSUUID *mergedTaskIdentifier = [NSUUID UUID];
- NSURLSessionDataTask *createdTask;
- __weak __typeof__(self) weakSelf = self;
- createdTask = [self.sessionManager
- dataTaskWithRequest:request
- uploadProgress:nil
- downloadProgress:nil
- completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
- dispatch_async(self.responseQueue, ^{
- __strong __typeof__(weakSelf) strongSelf = weakSelf;
- AFImageDownloaderMergedTask *mergedTask = [strongSelf safelyGetMergedTask:URLIdentifier];
- if ([mergedTask.identifier isEqual:mergedTaskIdentifier]) {
- mergedTask = [strongSelf safelyRemoveMergedTaskWithURLIdentifier:URLIdentifier];
- if (error) {
- for (AFImageDownloaderResponseHandler *handler in mergedTask.responseHandlers) {
- if (handler.failureBlock) {
- dispatch_async(dispatch_get_main_queue(), ^{
- handler.failureBlock(request, (NSHTTPURLResponse *)response, error);
- });
- }
- }
- } else {
- if ([strongSelf.imageCache shouldCacheImage:responseObject forRequest:request withAdditionalIdentifier:nil]) {
- [strongSelf.imageCache addImage:responseObject forRequest:request withAdditionalIdentifier:nil];
- }
- for (AFImageDownloaderResponseHandler *handler in mergedTask.responseHandlers) {
- if (handler.successBlock) {
- dispatch_async(dispatch_get_main_queue(), ^{
- handler.successBlock(request, (NSHTTPURLResponse *)response, responseObject);
- });
- }
- }
-
- }
- }
- [strongSelf safelyDecrementActiveTaskCount];
- [strongSelf safelyStartNextTaskIfNecessary];
- });
- }];
- // 4) Store the response handler for use when the request completes
- AFImageDownloaderResponseHandler *handler = [[AFImageDownloaderResponseHandler alloc] initWithUUID:receiptID
- success:success
- failure:failure];
- AFImageDownloaderMergedTask *mergedTask = [[AFImageDownloaderMergedTask alloc]
- initWithURLIdentifier:URLIdentifier
- identifier:mergedTaskIdentifier
- task:createdTask];
- [mergedTask addResponseHandler:handler];
- self.mergedTasks[URLIdentifier] = mergedTask;
- // 5) Either start the request or enqueue it depending on the current active request count
- if ([self isActiveRequestCountBelowMaximumLimit]) {
- [self startMergedTask:mergedTask];
- } else {
- [self enqueueMergedTask:mergedTask];
- }
- task = mergedTask.task;
- });
- if (task) {
- return [[AFImageDownloadReceipt alloc] initWithReceiptID:receiptID task:task];
- } else {
- return nil;
- }
- }
- - (void)cancelTaskForImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt {
- dispatch_sync(self.synchronizationQueue, ^{
- NSString *URLIdentifier = imageDownloadReceipt.task.originalRequest.URL.absoluteString;
- AFImageDownloaderMergedTask *mergedTask = self.mergedTasks[URLIdentifier];
- NSUInteger index = [mergedTask.responseHandlers indexOfObjectPassingTest:^BOOL(AFImageDownloaderResponseHandler * _Nonnull handler, __unused NSUInteger idx, __unused BOOL * _Nonnull stop) {
- return handler.uuid == imageDownloadReceipt.receiptID;
- }];
- if (index != NSNotFound) {
- AFImageDownloaderResponseHandler *handler = mergedTask.responseHandlers[index];
- [mergedTask removeResponseHandler:handler];
- NSString *failureReason = [NSString stringWithFormat:@"ImageDownloader cancelled URL request: %@",imageDownloadReceipt.task.originalRequest.URL.absoluteString];
- NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey:failureReason};
- NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:userInfo];
- if (handler.failureBlock) {
- dispatch_async(dispatch_get_main_queue(), ^{
- handler.failureBlock(imageDownloadReceipt.task.originalRequest, nil, error);
- });
- }
- }
- if (mergedTask.responseHandlers.count == 0) {
- [mergedTask.task cancel];
- [self removeMergedTaskWithURLIdentifier:URLIdentifier];
- }
- });
- }
- - (AFImageDownloaderMergedTask *)safelyRemoveMergedTaskWithURLIdentifier:(NSString *)URLIdentifier {
- __block AFImageDownloaderMergedTask *mergedTask = nil;
- dispatch_sync(self.synchronizationQueue, ^{
- mergedTask = [self removeMergedTaskWithURLIdentifier:URLIdentifier];
- });
- return mergedTask;
- }
- //This method should only be called from safely within the synchronizationQueue
- - (AFImageDownloaderMergedTask *)removeMergedTaskWithURLIdentifier:(NSString *)URLIdentifier {
- AFImageDownloaderMergedTask *mergedTask = self.mergedTasks[URLIdentifier];
- [self.mergedTasks removeObjectForKey:URLIdentifier];
- return mergedTask;
- }
- - (void)safelyDecrementActiveTaskCount {
- dispatch_sync(self.synchronizationQueue, ^{
- if (self.activeRequestCount > 0) {
- self.activeRequestCount -= 1;
- }
- });
- }
- - (void)safelyStartNextTaskIfNecessary {
- dispatch_sync(self.synchronizationQueue, ^{
- if ([self isActiveRequestCountBelowMaximumLimit]) {
- while (self.queuedMergedTasks.count > 0) {
- AFImageDownloaderMergedTask *mergedTask = [self dequeueMergedTask];
- if (mergedTask.task.state == NSURLSessionTaskStateSuspended) {
- [self startMergedTask:mergedTask];
- break;
- }
- }
- }
- });
- }
- - (void)startMergedTask:(AFImageDownloaderMergedTask *)mergedTask {
- [mergedTask.task resume];
- ++self.activeRequestCount;
- }
- - (void)enqueueMergedTask:(AFImageDownloaderMergedTask *)mergedTask {
- switch (self.downloadPrioritization) {
- case AFImageDownloadPrioritizationFIFO:
- [self.queuedMergedTasks addObject:mergedTask];
- break;
- case AFImageDownloadPrioritizationLIFO:
- [self.queuedMergedTasks insertObject:mergedTask atIndex:0];
- break;
- }
- }
- - (AFImageDownloaderMergedTask *)dequeueMergedTask {
- AFImageDownloaderMergedTask *mergedTask = nil;
- mergedTask = [self.queuedMergedTasks firstObject];
- [self.queuedMergedTasks removeObject:mergedTask];
- return mergedTask;
- }
- - (BOOL)isActiveRequestCountBelowMaximumLimit {
- return self.activeRequestCount < self.maximumActiveDownloads;
- }
- - (AFImageDownloaderMergedTask *)safelyGetMergedTask:(NSString *)URLIdentifier {
- __block AFImageDownloaderMergedTask *mergedTask;
- dispatch_sync(self.synchronizationQueue, ^(){
- mergedTask = self.mergedTasks[URLIdentifier];
- });
- return mergedTask;
- }
- @end
- #endif
|