422 lines
19 KiB
Objective-C
422 lines
19 KiB
Objective-C
// 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
|