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.
392 lines
17 KiB
392 lines
17 KiB
1 year ago
|
// 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 {
|
||
|
return [[NSURLCache alloc] initWithMemoryCapacity:20 * 1024 * 1024
|
||
|
diskCapacity:150 * 1024 * 1024
|
||
|
diskPath:@"com.alamofire.imagedownloader"];
|
||
|
}
|
||
|
|
||
|
+ (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];
|
||
|
AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:defaultConfiguration];
|
||
|
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.downloadPrioritizaton = 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
|
||
|
completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
|
||
|
dispatch_async(self.responseQueue, ^{
|
||
|
__strong __typeof__(weakSelf) strongSelf = weakSelf;
|
||
|
AFImageDownloaderMergedTask *mergedTask = self.mergedTasks[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 {
|
||
|
[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.state == NSURLSessionTaskStateSuspended) {
|
||
|
[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.downloadPrioritizaton) {
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
@end
|
||
|
|
||
|
#endif
|