You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
421 lines
19 KiB
421 lines
19 KiB
// 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
|
|
|