Files
PINRemoteImage/Pod/Classes/PINRemoteImageManager.m

1488 lines
62 KiB
Objective-C

//
// PINRemoteImageManager.m
// Pods
//
// Created by Garrett Moon on 8/17/14.
//
//
#import "PINRemoteImageManager.h"
#import <PINCache/PINCache.h>
#import <CommonCrypto/CommonDigest.h>
#import "PINAlternateRepresentationProvider.h"
#import "PINRemoteImage.h"
#import "PINRemoteLock.h"
#import "PINProgressiveImage.h"
#import "PINRemoteImageCallbacks.h"
#import "PINRemoteImageTask.h"
#import "PINRemoteImageProcessorTask.h"
#import "PINRemoteImageDownloadTask.h"
#import "PINDataTaskOperation.h"
#import "PINURLSessionManager.h"
#import "PINRemoteImageMemoryContainer.h"
#import "NSData+ImageDetectors.h"
#import "PINImage+DecodedImage.h"
#define PINRemoteImageManagerDefaultTimeout 60.0
//A limit of 200 characters is chosen because PINDiskCache
//may expand the length by encoding certain characters
#define PINRemoteImageManagerCacheKeyMaxLength 200
NSOperationQueuePriority operationPriorityWithImageManagerPriority(PINRemoteImageManagerPriority priority) {
switch (priority) {
case PINRemoteImageManagerPriorityVeryLow:
return NSOperationQueuePriorityVeryLow;
break;
case PINRemoteImageManagerPriorityLow:
return NSOperationQueuePriorityLow;
break;
case PINRemoteImageManagerPriorityMedium:
return NSOperationQueuePriorityNormal;
break;
case PINRemoteImageManagerPriorityHigh:
return NSOperationQueuePriorityHigh;
break;
case PINRemoteImageManagerPriorityVeryHigh:
return NSOperationQueuePriorityVeryHigh;
break;
}
}
float dataTaskPriorityWithImageManagerPriority(PINRemoteImageManagerPriority priority) {
switch (priority) {
case PINRemoteImageManagerPriorityVeryLow:
return 0.0;
break;
case PINRemoteImageManagerPriorityLow:
return 0.25;
break;
case PINRemoteImageManagerPriorityMedium:
return 0.5;
break;
case PINRemoteImageManagerPriorityHigh:
return 0.75;
break;
case PINRemoteImageManagerPriorityVeryHigh:
return 1.0;
break;
}
}
NSString * const PINRemoteImageManagerErrorDomain = @"PINRemoteImageManagerErrorDomain";
NSString * const PINRemoteImageCacheKey = @"cacheKey";
typedef void (^PINRemoteImageManagerDataCompletion)(NSData *data, NSError *error);
@interface NSOperationQueue (PINRemoteImageManager)
- (void)pin_addOperationWithQueuePriority:(PINRemoteImageManagerPriority)priority block:(void (^)(void))block;
@end
@interface PINTaskQOS : NSObject
- (instancetype)initWithBPS:(float)bytesPerSecond endDate:(NSDate *)endDate;
@property (nonatomic, strong) NSDate *endDate;
@property (nonatomic, assign) float bytesPerSecond;
@end
@interface PINRemoteImageManager () <PINURLSessionManagerDelegate>
{
dispatch_queue_t _callbackQueue;
PINRemoteLock *_lock;
NSOperationQueue *_concurrentOperationQueue;
NSOperationQueue *_urlSessionTaskQueue;
// Necesarry to have a strong reference to _defaultAlternateRepresentationProvider because _alternateRepProvider is __weak
PINAlternateRepresentationProvider *_defaultAlternateRepresentationProvider;
__weak PINAlternateRepresentationProvider *_alternateRepProvider;
}
@property (nonatomic, strong) PINCache *cache;
@property (nonatomic, strong) PINURLSessionManager *sessionManager;
@property (nonatomic, assign) NSTimeInterval timeout;
@property (nonatomic, strong) NSMutableDictionary <NSString *, __kindof PINRemoteImageTask *> *tasks;
@property (nonatomic, strong) NSHashTable <NSUUID *> *canceledTasks;
@property (nonatomic, strong) NSArray <NSNumber *> *progressThresholds;
@property (nonatomic, assign) BOOL shouldBlurProgressive;
@property (nonatomic, assign) CGSize maxProgressiveRenderSize;
@property (nonatomic, assign) NSTimeInterval estimatedRemainingTimeThreshold;
@property (nonatomic, strong) dispatch_queue_t callbackQueue;
@property (nonatomic, strong) NSOperationQueue *concurrentOperationQueue;
@property (nonatomic, strong) NSOperationQueue *urlSessionTaskQueue;
@property (nonatomic, strong) NSMutableArray <PINTaskQOS *> *taskQOS;
@property (nonatomic, assign) float highQualityBPSThreshold;
@property (nonatomic, assign) float lowQualityBPSThreshold;
@property (nonatomic, assign) BOOL shouldUpgradeLowQualityImages;
@property (nonatomic, copy) PINRemoteImageManagerAuthenticationChallenge authenticationChallengeHandler;
#if DEBUG
@property (nonatomic, assign) float currentBPS;
@property (nonatomic, assign) BOOL overrideBPS;
@property (nonatomic, assign) NSUInteger totalDownloads;
#endif
@end
#pragma mark PINRemoteImageManager
@implementation PINRemoteImageManager
static PINRemoteImageManager *sharedImageManager = nil;
static dispatch_once_t sharedDispatchToken;
+ (BOOL)supportsQOS
{
static dispatch_once_t onceToken;
static BOOL supportsQOS;
dispatch_once(&onceToken, ^{
supportsQOS = [NSOperation instancesRespondToSelector:@selector(setQualityOfService:)];
});
return supportsQOS;
}
+ (instancetype)sharedImageManager
{
dispatch_once(&sharedDispatchToken, ^{
sharedImageManager = [[[self class] alloc] init];
});
return sharedImageManager;
}
+ (void)setSharedImageManagerWithConfiguration:(NSURLSessionConfiguration *)configuration
{
NSAssert(sharedImageManager == nil, @"sharedImageManager singleton is already configured");
dispatch_once(&sharedDispatchToken, ^{
sharedImageManager = [[[self class] alloc] initWithSessionConfiguration:configuration];
});
}
- (instancetype)init
{
return [self initWithSessionConfiguration:nil];
}
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration
{
return [self initWithSessionConfiguration:configuration alternativeRepresentationProvider:nil];
}
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration alternativeRepresentationProvider:(id <PINRemoteImageManagerAlternateRepresentationProvider>)alternateRepProvider
{
if (self = [super init]) {
self.cache = [self defaultImageCache];
if (!configuration) {
configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
}
_callbackQueue = dispatch_queue_create("PINRemoteImageManagerCallbackQueue", DISPATCH_QUEUE_CONCURRENT);
_lock = [[PINRemoteLock alloc] initWithName:@"PINRemoteImageManager"];
_concurrentOperationQueue = [[NSOperationQueue alloc] init];
_concurrentOperationQueue.name = @"PINRemoteImageManager Concurrent Operation Queue";
_concurrentOperationQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
if ([[self class] supportsQOS]) {
_concurrentOperationQueue.qualityOfService = NSQualityOfServiceUtility;
}
_urlSessionTaskQueue = [[NSOperationQueue alloc] init];
_urlSessionTaskQueue.name = @"PINRemoteImageManager Concurrent URL Session Task Queue";
_urlSessionTaskQueue.maxConcurrentOperationCount = 10;
self.sessionManager = [[PINURLSessionManager alloc] initWithSessionConfiguration:configuration];
self.sessionManager.delegate = self;
self.estimatedRemainingTimeThreshold = 0.1;
self.timeout = PINRemoteImageManagerDefaultTimeout;
_highQualityBPSThreshold = 500000;
_lowQualityBPSThreshold = 50000; // approximately edge speeds
_shouldUpgradeLowQualityImages = NO;
_shouldBlurProgressive = YES;
_maxProgressiveRenderSize = CGSizeMake(1024, 1024);
self.tasks = [[NSMutableDictionary alloc] init];
self.canceledTasks = [[NSHashTable alloc] initWithOptions:NSHashTableWeakMemory capacity:5];
self.taskQOS = [[NSMutableArray alloc] initWithCapacity:5];
if (alternateRepProvider == nil) {
_defaultAlternateRepresentationProvider = [[PINAlternateRepresentationProvider alloc] init];
alternateRepProvider = _defaultAlternateRepresentationProvider;
}
_alternateRepProvider = alternateRepProvider;
}
return self;
}
- (PINCache *)defaultImageCache;
{
return [[PINCache alloc] initWithName:@"PINRemoteImageManagerCache"];
}
- (void)lockOnMainThread
{
#if !DEBUG
NSAssert(NO, @"lockOnMainThread should only be called for testing on debug builds!");
#endif
[_lock lock];
}
- (void)lock
{
NSAssert([NSThread isMainThread] == NO, @"lock should not be called from the main thread!");
[_lock lock];
}
- (void)unlock
{
[_lock unlock];
}
- (void)setAuthenticationChallenge:(PINRemoteImageManagerAuthenticationChallenge)challengeBlock {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
typeof(self) strongSelf = weakSelf;
[strongSelf lock];
strongSelf.authenticationChallengeHandler = challengeBlock;
[strongSelf unlock];
});
}
- (void)setMaxNumberOfConcurrentOperations:(NSInteger)maxNumberOfConcurrentOperations completion:(dispatch_block_t)completion
{
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
typeof(self) strongSelf = weakSelf;
[strongSelf lock];
strongSelf.concurrentOperationQueue.maxConcurrentOperationCount = maxNumberOfConcurrentOperations;
[strongSelf unlock];
if (completion) {
completion();
}
});
}
- (void)setMaxNumberOfConcurrentDownloads:(NSInteger)maxNumberOfConcurrentDownloads completion:(dispatch_block_t)completion
{
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
typeof(self) strongSelf = weakSelf;
[strongSelf lock];
strongSelf.urlSessionTaskQueue.maxConcurrentOperationCount = maxNumberOfConcurrentDownloads;
[strongSelf unlock];
if (completion) {
completion();
}
});
}
- (void)setEstimatedRemainingTimeThresholdForProgressiveDownloads:(NSTimeInterval)estimatedRemainingTimeThreshold completion:(dispatch_block_t)completion
{
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
typeof(self) strongSelf = weakSelf;
[strongSelf lock];
strongSelf.estimatedRemainingTimeThreshold = estimatedRemainingTimeThreshold;
[strongSelf unlock];
if (completion) {
completion();
}
});
}
- (void)setProgressThresholds:(NSArray *)progressThresholds completion:(dispatch_block_t)completion
{
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
typeof(self) strongSelf = weakSelf;
[strongSelf lock];
strongSelf.progressThresholds = progressThresholds;
[strongSelf unlock];
if (completion) {
completion();
}
});
}
- (void)setProgressiveRendersShouldBlur:(BOOL)shouldBlur completion:(nullable dispatch_block_t)completion
{
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
typeof(self) strongSelf = weakSelf;
[strongSelf lock];
strongSelf.shouldBlurProgressive = shouldBlur;
[strongSelf unlock];
if (completion) {
completion();
}
});
}
- (void)setProgressiveRendersMaxProgressiveRenderSize:(CGSize)maxProgressiveRenderSize completion:(nullable dispatch_block_t)completion
{
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
typeof(self) strongSelf = weakSelf;
[strongSelf lock];
strongSelf.maxProgressiveRenderSize = maxProgressiveRenderSize;
[strongSelf unlock];
if (completion) {
completion();
}
});
}
- (void)setHighQualityBPSThreshold:(float)highQualityBPSThreshold completion:(dispatch_block_t)completion
{
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
typeof(self) strongSelf = weakSelf;
[strongSelf lock];
strongSelf.highQualityBPSThreshold = highQualityBPSThreshold;
[strongSelf unlock];
if (completion) {
completion();
}
});
}
- (void)setLowQualityBPSThreshold:(float)lowQualityBPSThreshold completion:(dispatch_block_t)completion
{
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
typeof(self) strongSelf = weakSelf;
[strongSelf lock];
strongSelf.lowQualityBPSThreshold = lowQualityBPSThreshold;
[strongSelf unlock];
if (completion) {
completion();
}
});
}
- (void)setShouldUpgradeLowQualityImages:(BOOL)shouldUpgradeLowQualityImages completion:(dispatch_block_t)completion
{
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
typeof(self) strongSelf = weakSelf;
[strongSelf lock];
strongSelf.shouldUpgradeLowQualityImages = shouldUpgradeLowQualityImages;
[strongSelf unlock];
if (completion) {
completion();
}
});
}
- (NSUUID *)downloadImageWithURL:(NSURL *)url
completion:(PINRemoteImageManagerImageCompletion)completion
{
return [self downloadImageWithURL:url
options:PINRemoteImageManagerDownloadOptionsNone
completion:completion];
}
- (NSUUID *)downloadImageWithURL:(NSURL *)url
options:(PINRemoteImageManagerDownloadOptions)options
completion:(PINRemoteImageManagerImageCompletion)completion
{
return [self downloadImageWithURL:url
options:options
progressImage:nil
completion:completion];
}
- (NSUUID *)downloadImageWithURL:(NSURL *)url
options:(PINRemoteImageManagerDownloadOptions)options
progressImage:(PINRemoteImageManagerImageCompletion)progressImage
completion:(PINRemoteImageManagerImageCompletion)completion
{
return [self downloadImageWithURL:url
options:options
priority:PINRemoteImageManagerPriorityMedium
processorKey:nil
processor:nil
progressImage:progressImage
progressDownload:nil
completion:completion
inputUUID:nil];
}
- (NSUUID *)downloadImageWithURL:(NSURL *)url
options:(PINRemoteImageManagerDownloadOptions)options
progressDownload:(PINRemoteImageManagerProgressDownload)progressDownload
completion:(PINRemoteImageManagerImageCompletion)completion
{
return [self downloadImageWithURL:url
options:options
priority:PINRemoteImageManagerPriorityMedium
processorKey:nil
processor:nil
progressImage:nil
progressDownload:progressDownload
completion:completion
inputUUID:nil];
}
- (NSUUID *)downloadImageWithURL:(NSURL *)url
options:(PINRemoteImageManagerDownloadOptions)options
progressImage:(PINRemoteImageManagerImageCompletion)progressImage
progressDownload:(PINRemoteImageManagerProgressDownload)progressDownload
completion:(PINRemoteImageManagerImageCompletion)completion
{
return [self downloadImageWithURL:url
options:options
priority:PINRemoteImageManagerPriorityMedium
processorKey:nil
processor:nil
progressImage:progressImage
progressDownload:progressDownload
completion:completion
inputUUID:nil];
}
- (NSUUID *)downloadImageWithURL:(NSURL *)url
options:(PINRemoteImageManagerDownloadOptions)options
processorKey:(NSString *)processorKey
processor:(PINRemoteImageManagerImageProcessor)processor
completion:(PINRemoteImageManagerImageCompletion)completion
{
return [self downloadImageWithURL:url
options:options
priority:PINRemoteImageManagerPriorityMedium
processorKey:processorKey
processor:processor
progressImage:nil
progressDownload:nil
completion:completion
inputUUID:nil];
}
- (NSUUID *)downloadImageWithURL:(NSURL *)url
options:(PINRemoteImageManagerDownloadOptions)options
processorKey:(NSString *)processorKey
processor:(PINRemoteImageManagerImageProcessor)processor
progressDownload:(PINRemoteImageManagerProgressDownload)progressDownload
completion:(PINRemoteImageManagerImageCompletion)completion
{
return [self downloadImageWithURL:url
options:options
priority:PINRemoteImageManagerPriorityMedium
processorKey:processorKey
processor:processor
progressImage:nil
progressDownload:progressDownload
completion:completion
inputUUID:nil];
}
- (NSUUID *)downloadImageWithURL:(NSURL *)url
options:(PINRemoteImageManagerDownloadOptions)options
priority:(PINRemoteImageManagerPriority)priority
processorKey:(NSString *)processorKey
processor:(PINRemoteImageManagerImageProcessor)processor
progressImage:(PINRemoteImageManagerImageCompletion)progressImage
progressDownload:(PINRemoteImageManagerProgressDownload)progressDownload
completion:(PINRemoteImageManagerImageCompletion)completion
inputUUID:(NSUUID *)UUID
{
NSAssert((processor != nil && processorKey.length > 0) || (processor == nil && processorKey == nil), @"processor must not be nil and processorKey length must be greater than zero OR processor must be nil and processorKey must be nil");
Class taskClass;
if (processor && processorKey.length > 0) {
taskClass = [PINRemoteImageProcessorTask class];
} else {
taskClass = [PINRemoteImageDownloadTask class];
}
NSString *key = [self cacheKeyForURL:url processorKey:processorKey];
if (url == nil) {
[self earlyReturnWithOptions:options url:nil key:key object:nil completion:completion];
return nil;
}
NSAssert([url isKindOfClass:[NSURL class]], @"url must be of type NSURL, if it's an NSString, we'll try to correct");
if ([url isKindOfClass:[NSString class]]) {
url = [NSURL URLWithString:(NSString *)url];
}
if (UUID == nil) {
UUID = [NSUUID UUID];
}
//Check to see if the image is in memory cache and we're on the main thread.
//If so, special case this to avoid flashing the UI
id object = [self.cache.memoryCache objectForKey:key];
if (object) {
if ([self earlyReturnWithOptions:options url:url key:key object:object completion:completion]) {
return nil;
}
}
__weak typeof(self) weakSelf = self;
[_concurrentOperationQueue pin_addOperationWithQueuePriority:priority block:^
{
typeof(self) strongSelf = weakSelf;
[strongSelf lock];
//check canceled tasks first
if ([strongSelf.canceledTasks containsObject:UUID]) {
PINLog(@"skipping starting %@ because it was canceled.", UUID);
[strongSelf unlock];
return;
}
PINRemoteImageTask *task = [strongSelf.tasks objectForKey:key];
BOOL taskExisted = NO;
if (task == nil) {
task = [[taskClass alloc] init];
PINLog(@"Task does not exist creating with key: %@, URL: %@, UUID: %@, task: %p", key, url, UUID, task);
#if PINRemoteImageLogging
task.key = key;
#endif
} else {
taskExisted = YES;
PINLog(@"Task exists, attaching with key: %@, URL: %@, UUID: %@, task: %@", key, url, UUID, task);
}
[task addCallbacksWithCompletionBlock:completion progressImageBlock:progressImage progressDownloadBlock:progressDownload withUUID:UUID];
[strongSelf.tasks setObject:task forKey:key];
BlockAssert(taskClass == [task class], @"Task class should be the same!");
[strongSelf unlock];
if (taskExisted == NO) {
[strongSelf.concurrentOperationQueue pin_addOperationWithQueuePriority:priority block:^
{
typeof(self) strongSelf = weakSelf;
[strongSelf objectForKey:key options:options completion:^(BOOL found, BOOL valid, PINImage *image, id alternativeRepresentation) {
if (found) {
if (valid) {
typeof(self) strongSelf = weakSelf;
[strongSelf callCompletionsWithKey:key image:image alternativeRepresentation:alternativeRepresentation cached:YES error:nil finalized:YES];
} else {
//Remove completion and try again
typeof(self) strongSelf = weakSelf;
[strongSelf lock];
PINRemoteImageTask *task = [strongSelf.tasks objectForKey:key];
[task removeCallbackWithUUID:UUID];
if (task.callbackBlocks.count == 0) {
[strongSelf.tasks removeObjectForKey:key];
}
[strongSelf unlock];
//Skip early check
[strongSelf downloadImageWithURL:url
options:options | PINRemoteImageManagerDownloadOptionsSkipEarlyCheck
priority:priority
processorKey:processorKey
processor:processor
progressImage:(PINRemoteImageManagerImageCompletion)progressImage
progressDownload:nil
completion:completion
inputUUID:UUID];
}
} else {
if ([taskClass isSubclassOfClass:[PINRemoteImageProcessorTask class]]) {
//continue processing
[strongSelf downloadImageWithURL:url
options:options
priority:priority
key:key
processor:processor
UUID:UUID];
} else if ([taskClass isSubclassOfClass:[PINRemoteImageDownloadTask class]]) {
//continue downloading
[strongSelf downloadImageWithURL:url
options:options
priority:priority
key:key
progressImage:progressImage
UUID:UUID];
}
}
}];
}];
}
}];
return UUID;
}
- (void)downloadImageWithURL:(NSURL *)url
options:(PINRemoteImageManagerDownloadOptions)options
priority:(PINRemoteImageManagerPriority)priority
key:(NSString *)key
processor:(PINRemoteImageManagerImageProcessor)processor
UUID:(NSUUID *)UUID
{
PINRemoteImageProcessorTask *task = nil;
[self lock];
task = [self.tasks objectForKey:key];
//check processing task still exists and download hasn't been started for another task
if (task == nil || task.downloadTaskUUID != nil) {
[self unlock];
return;
}
__weak typeof(self) weakSelf = self;
NSUUID *downloadTaskUUID = [self downloadImageWithURL:url
options:options | PINRemoteImageManagerDownloadOptionsSkipEarlyCheck
completion:^(PINRemoteImageManagerResult *result)
{
typeof(self) strongSelf = weakSelf;
NSUInteger processCost = 0;
NSError *error = result.error;
PINRemoteImageProcessorTask *task = nil;
[strongSelf lock];
task = [strongSelf.tasks objectForKey:key];
[strongSelf unlock];
//check processing task still exists
if (task == nil) {
return;
}
if (result.image && error == nil) {
//If completionBlocks.count == 0, we've canceled before we were even able to start.
PINImage *image = processor(result, &processCost);
if (image == nil) {
error = [NSError errorWithDomain:PINRemoteImageManagerErrorDomain
code:PINRemoteImageManagerErrorFailedToProcessImage
userInfo:nil];
}
[strongSelf callCompletionsWithKey:key image:image alternativeRepresentation:nil cached:NO error:error finalized:NO];
if (error == nil) {
BOOL saveAsJPEG = (options & PINRemoteImageManagerSaveProcessedImageAsJPEG) != 0;
NSData *diskData = nil;
if (saveAsJPEG) {
diskData = PINImageJPEGRepresentation(image, 1.0);
} else {
diskData = PINImagePNGRepresentation(image);
}
[strongSelf materializeAndCacheObject:image cacheInDisk:diskData additionalCost:processCost key:key options:options outImage:nil outAltRep:nil];
}
[strongSelf callCompletionsWithKey:key image:image alternativeRepresentation:nil cached:NO error:error finalized:YES];
} else {
if (error == nil) {
error = [NSError errorWithDomain:PINRemoteImageManagerErrorDomain
code:PINRemoteImageManagerErrorFailedToFetchImageForProcessing
userInfo:nil];
}
[strongSelf callCompletionsWithKey:key image:nil alternativeRepresentation:nil cached:NO error:error finalized:YES];
}
}];
task.downloadTaskUUID = downloadTaskUUID;
[self unlock];
}
- (void)downloadImageWithURL:(NSURL *)url
options:(PINRemoteImageManagerDownloadOptions)options
priority:(PINRemoteImageManagerPriority)priority
key:(NSString *)key
progressImage:(PINRemoteImageManagerImageCompletion)progressImage
UUID:(NSUUID *)UUID
{
[self lock];
PINRemoteImageDownloadTask *task = [self.tasks objectForKey:key];
if (task.urlSessionTaskOperation == nil && task.callbackBlocks.count > 0) {
//If completionBlocks.count == 0, we've canceled before we were even able to start.
CFTimeInterval startTime = CACurrentMediaTime();
PINDataTaskOperation *urlSessionTaskOperation = [self sessionTaskWithURL:url key:key options:options priority:priority];
task.urlSessionTaskOperation = urlSessionTaskOperation;
task.sessionTaskStartTime = startTime;
}
[self unlock];
}
- (BOOL)earlyReturnWithOptions:(PINRemoteImageManagerDownloadOptions)options url:(NSURL *)url key:(NSString *)key object:(id)object completion:(PINRemoteImageManagerImageCompletion)completion
{
PINImage *image = nil;
id alternativeRepresentation = nil;
PINRemoteImageResultType resultType = PINRemoteImageResultTypeNone;
BOOL allowEarlyReturn = !(PINRemoteImageManagerDownloadOptionsSkipEarlyCheck & options);
if (url != nil && object != nil) {
resultType = PINRemoteImageResultTypeMemoryCache;
[self materializeAndCacheObject:object key:key options:options outImage:&image outAltRep:&alternativeRepresentation];
}
if (completion && ((image || alternativeRepresentation) || (url == nil))) {
//If we're on the main thread, special case to call completion immediately
NSError *error = nil;
if (!url) {
error = [NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorUnsupportedURL
userInfo:@{ NSLocalizedDescriptionKey : @"unsupported URL" }];
}
if (allowEarlyReturn && [NSThread isMainThread]) {
completion([PINRemoteImageManagerResult imageResultWithImage:image
alternativeRepresentation:alternativeRepresentation
requestLength:0
error:error
resultType:resultType
UUID:nil]);
} else {
dispatch_async(self.callbackQueue, ^{
completion([PINRemoteImageManagerResult imageResultWithImage:image
alternativeRepresentation:alternativeRepresentation
requestLength:0
error:error
resultType:resultType
UUID:nil]);
});
}
return YES;
}
return NO;
}
- (PINDataTaskOperation *)sessionTaskWithURL:(NSURL *)URL
key:(NSString *)key
options:(PINRemoteImageManagerDownloadOptions)options
priority:(PINRemoteImageManagerPriority)priority
{
__weak typeof(self) weakSelf = self;
return [self downloadDataWithURL:URL
key:key
priority:priority
completion:^(NSData *data, NSError *error)
{
[_concurrentOperationQueue pin_addOperationWithQueuePriority:priority block:^
{
typeof(self) strongSelf = weakSelf;
NSError *remoteImageError = error;
PINImage *image = nil;
id alternativeRepresentation = nil;
if (remoteImageError == nil) {
//stores the object in the caches
[strongSelf materializeAndCacheObject:data cacheInDisk:data additionalCost:0 key:key options:options outImage:&image outAltRep:&alternativeRepresentation];
}
if (error == nil && image == nil && alternativeRepresentation == nil) {
remoteImageError = [NSError errorWithDomain:PINRemoteImageManagerErrorDomain
code:PINRemoteImageManagerErrorFailedToDecodeImage
userInfo:nil];
}
[strongSelf callCompletionsWithKey:key image:image alternativeRepresentation:alternativeRepresentation cached:NO error:remoteImageError finalized:YES];
}];
}];
}
- (PINDataTaskOperation *)downloadDataWithURL:(NSURL *)url
key:(NSString *)key
priority:(PINRemoteImageManagerPriority)priority
completion:(PINRemoteImageManagerDataCompletion)completion
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:self.timeout];
[NSURLProtocol setProperty:key forKey:PINRemoteImageCacheKey inRequest:request];
__weak typeof(self) weakSelf = self;
PINDataTaskOperation *dataTaskOperation = [PINDataTaskOperation dataTaskOperationWithSessionManager:self.sessionManager
request:request
completionHandler:^(NSURLResponse *response, NSError *error)
{
typeof(self) strongSelf = weakSelf;
#if DEBUG
[strongSelf lock];
strongSelf.totalDownloads++;
[strongSelf unlock];
#endif
#if PINRemoteImageLogging
if (error && error.code != NSURLErrorCancelled) {
PINLog(@"Failed downloading image: %@ with error: %@", url, error);
} else if (error == nil && response.expectedContentLength == 0) {
PINLog(@"image is empty at URL: %@", url);
} else {
PINLog(@"Finished downloading image: %@", url);
}
#endif
if (error.code != NSURLErrorCancelled) {
[strongSelf lock];
PINRemoteImageDownloadTask *task = [strongSelf.tasks objectForKey:key];
NSData *data = task.progressImage.data;
[strongSelf unlock];
if (error == nil && data == nil) {
error = [NSError errorWithDomain:PINRemoteImageManagerErrorDomain
code:PINRemoteImageManagerErrorImageEmpty
userInfo:nil];
}
completion(data, error);
}
}];
if ([dataTaskOperation.dataTask respondsToSelector:@selector(setPriority:)]) {
dataTaskOperation.dataTask.priority = dataTaskPriorityWithImageManagerPriority(priority);
}
dataTaskOperation.queuePriority = operationPriorityWithImageManagerPriority(priority);
[self.urlSessionTaskQueue addOperation:dataTaskOperation];
return dataTaskOperation;
}
- (void)callCompletionsWithKey:(NSString *)key image:(PINImage *)image alternativeRepresentation:(id)alternativeRepresentation cached:(BOOL)cached error:(NSError *)error finalized:(BOOL)finalized
{
[self lock];
PINRemoteImageDownloadTask *task = [self.tasks objectForKey:key];
[task callCompletionsWithQueue:self.callbackQueue remove:!finalized withImage:image alternativeRepresentation:alternativeRepresentation cached:cached error:error];
if (finalized) {
[self.tasks removeObjectForKey:key];
}
[self unlock];
}
#pragma mark - Prefetching
- (NSArray<NSUUID *> *)prefetchImagesWithURLs:(NSArray <NSURL *> *)urls
{
return [self prefetchImagesWithURLs:urls options:PINRemoteImageManagerDownloadOptionsNone | PINRemoteImageManagerDownloadOptionsSkipEarlyCheck];
}
- (NSArray<NSUUID *> *)prefetchImagesWithURLs:(NSArray <NSURL *> *)urls options:(PINRemoteImageManagerDownloadOptions)options
{
NSMutableArray *tasks = [NSMutableArray arrayWithCapacity:urls.count];
for (NSURL *url in urls) {
NSUUID *task = [self prefetchImageWithURL:url options:options];
if (task != nil) {
[tasks addObject:task];
}
}
return tasks;
}
- (NSUUID *)prefetchImageWithURL:(NSURL *)url
{
return [self prefetchImageWithURL:url options:PINRemoteImageManagerDownloadOptionsNone | PINRemoteImageManagerDownloadOptionsSkipEarlyCheck];
}
- (NSUUID *)prefetchImageWithURL:(NSURL *)url options:(PINRemoteImageManagerDownloadOptions)options
{
return [self downloadImageWithURL:url
options:options
priority:PINRemoteImageManagerPriorityVeryLow
processorKey:nil
processor:nil
progressImage:nil
progressDownload:nil
completion:nil
inputUUID:nil];
}
#pragma mark - Cancelation & Priority
- (void)cancelTaskWithUUID:(NSUUID *)UUID
{
if (UUID == nil) {
return;
}
PINLog(@"Attempting to cancel UUID: %@", UUID);
__weak typeof(self) weakSelf = self;
[_concurrentOperationQueue pin_addOperationWithQueuePriority:PINRemoteImageManagerPriorityHigh block:^
{
typeof(self) strongSelf = weakSelf;
//find the task associated with the UUID. This might be spead up by storing a mapping of UUIDs to tasks
[strongSelf lock];
__block PINRemoteImageTask *taskToEvaluate = nil;
__block NSString *taskKey = nil;
[strongSelf.tasks enumerateKeysAndObjectsUsingBlock:^(NSString *key, PINRemoteImageTask *task, BOOL *stop) {
if (task.callbackBlocks[UUID]) {
taskToEvaluate = task;
taskKey = key;
*stop = YES;
}
}];
if (taskToEvaluate == nil) {
//maybe task hasn't been added to task list yet, add it to canceled tasks.
//there's no need to ever remove a UUID from canceledTasks because it is weak.
[strongSelf.canceledTasks addObject:UUID];
}
if ([taskToEvaluate cancelWithUUID:UUID manager:strongSelf]) {
[strongSelf.tasks removeObjectForKey:taskKey];
}
[strongSelf unlock];
}];
}
- (void)setPriority:(PINRemoteImageManagerPriority)priority ofTaskWithUUID:(NSUUID *)UUID
{
if (UUID == nil) {
return;
}
PINLog(@"Setting priority of UUID: %@ priority: %lu", UUID, (unsigned long)priority);
__weak typeof(self) weakSelf = self;
[_concurrentOperationQueue pin_addOperationWithQueuePriority:PINRemoteImageManagerPriorityHigh block:^{
typeof(self) strongSelf = weakSelf;
[strongSelf lock];
PINRemoteImageTask *taskToEvaluate = nil;
for (NSString *key in [strongSelf.tasks allKeys]) {
PINRemoteImageTask *task = [strongSelf.tasks objectForKey:key];
for (NSUUID *blockUUID in [task.callbackBlocks allKeys]) {
if ([blockUUID isEqual:UUID]) {
taskToEvaluate = task;
break;
}
}
}
[taskToEvaluate setPriority:priority];
[strongSelf unlock];
}];
}
- (void)setProgressImageCallback:(nullable PINRemoteImageManagerImageCompletion)progressImageCallback ofTaskWithUUID:(nonnull NSUUID *)UUID
{
if (UUID == nil) {
return;
}
PINLog(@"setting progress block of UUID: %@ progressBlock: %@", UUID, progressImageCallback);
__weak typeof(self) weakSelf = self;
[_concurrentOperationQueue pin_addOperationWithQueuePriority:PINRemoteImageManagerPriorityHigh block:^{
typeof(self) strongSelf = weakSelf;
[strongSelf lock];
for (NSString *key in [strongSelf.tasks allKeys]) {
PINRemoteImageTask *task = [strongSelf.tasks objectForKey:key];
for (NSUUID *blockUUID in [task.callbackBlocks allKeys]) {
if ([blockUUID isEqual:UUID]) {
if ([task isKindOfClass:[PINRemoteImageDownloadTask class]]) {
PINRemoteImageCallbacks *callbacks = task.callbackBlocks[blockUUID];
callbacks.progressImageBlock = progressImageCallback;
}
break;
}
}
}
[strongSelf unlock];
}];
}
#pragma mark - Caching
- (void)imageFromCacheWithCacheKey:(NSString *)cacheKey
completion:(PINRemoteImageManagerImageCompletion)completion
{
[self imageFromCacheWithCacheKey:cacheKey options:PINRemoteImageManagerDownloadOptionsNone completion:completion];
}
- (void)imageFromCacheWithCacheKey:(NSString *)cacheKey
options:(PINRemoteImageManagerDownloadOptions)options
completion:(PINRemoteImageManagerImageCompletion)completion
{
CFTimeInterval requestTime = CACurrentMediaTime();
if ((PINRemoteImageManagerDownloadOptionsSkipEarlyCheck & options) == NO && [NSThread isMainThread]) {
PINRemoteImageManagerResult *result = [self synchronousImageFromCacheWithCacheKey:cacheKey options:options];
if (result.image && result.error) {
completion((result));
return;
}
}
[self objectForKey:cacheKey options:options completion:^(BOOL found, BOOL valid, PINImage *image, id alternativeRepresentation) {
NSError *error = nil;
if (valid == NO) {
error = [NSError errorWithDomain:PINRemoteImageManagerErrorDomain
code:PINRemoteImageManagerErrorInvalidItemInCache
userInfo:nil];
}
dispatch_async(self.callbackQueue, ^{
completion([PINRemoteImageManagerResult imageResultWithImage:image
alternativeRepresentation:alternativeRepresentation
requestLength:CACurrentMediaTime() - requestTime
error:error
resultType:PINRemoteImageResultTypeCache
UUID:nil]);
});
}];
}
- (PINRemoteImageManagerResult *)synchronousImageFromCacheWithCacheKey:(NSString *)cacheKey options:(PINRemoteImageManagerDownloadOptions)options
{
CFTimeInterval requestTime = CACurrentMediaTime();
id object = [self.cache.memoryCache objectForKey:cacheKey];
PINImage *image;
id alternativeRepresentation;
NSError *error = nil;
if (object == nil) {
image = nil;
alternativeRepresentation = nil;
} else if ([self materializeAndCacheObject:object key:cacheKey options:options outImage:&image outAltRep:&alternativeRepresentation] == NO) {
error = [NSError errorWithDomain:PINRemoteImageManagerErrorDomain
code:PINRemoteImageManagerErrorInvalidItemInCache
userInfo:nil];
}
return [PINRemoteImageManagerResult imageResultWithImage:image
alternativeRepresentation:alternativeRepresentation
requestLength:CACurrentMediaTime() - requestTime
error:error
resultType:PINRemoteImageResultTypeMemoryCache
UUID:nil];
}
#pragma mark - Session Task Blocks
- (void)didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge forTask:(NSURLSessionTask *)task completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
[self lock];
if (self.authenticationChallengeHandler) {
self.authenticationChallengeHandler(task, challenge, ^(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential){
completionHandler(disposition, credential);
});
} else {
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
[self unlock];
}
- (void)didReceiveData:(NSData *)data forTask:(NSURLSessionDataTask *)dataTask
{
[self lock];
NSString *cacheKey = [NSURLProtocol propertyForKey:PINRemoteImageCacheKey inRequest:dataTask.originalRequest];
PINRemoteImageDownloadTask *task = [self.tasks objectForKey:cacheKey];
if (task.progressImage == nil) {
task.progressImage = [[PINProgressiveImage alloc] init];
task.progressImage.startTime = task.sessionTaskStartTime;
task.progressImage.estimatedRemainingTimeThreshold = self.estimatedRemainingTimeThreshold;
if (self.progressThresholds) {
task.progressImage.progressThresholds = self.progressThresholds;
}
}
PINProgressiveImage *progressiveImage = task.progressImage;
BOOL hasProgressBlocks = task.hasProgressBlocks;
BOOL shouldBlur = self.shouldBlurProgressive;
CGSize maxProgressiveRenderSize = self.maxProgressiveRenderSize;
[task callProgressDownloadWithQueue:self.callbackQueue completedBytes:dataTask.countOfBytesReceived totalBytes:dataTask.countOfBytesExpectedToReceive];
[self unlock];
[progressiveImage updateProgressiveImageWithData:data expectedNumberOfBytes:[dataTask countOfBytesExpectedToReceive]];
if (hasProgressBlocks && [NSOperation instancesRespondToSelector:@selector(qualityOfService)]) {
__weak typeof(self) weakSelf = self;
[_concurrentOperationQueue pin_addOperationWithQueuePriority:PINRemoteImageManagerPriorityLow block:^{
typeof(self) strongSelf = weakSelf;
CGFloat renderedImageQuality = 1.0;
PINImage *progressImage = [progressiveImage currentImageBlurred:shouldBlur maxProgressiveRenderSize:maxProgressiveRenderSize renderedImageQuality:&renderedImageQuality];
if (progressImage) {
[strongSelf lock];
PINRemoteImageDownloadTask *task = strongSelf.tasks[cacheKey];
[task callProgressImageWithQueue:strongSelf.callbackQueue withImage:progressImage renderedImageQuality:renderedImageQuality];
[strongSelf unlock];
}
}];
}
}
- (void)didCompleteTask:(NSURLSessionTask *)task withError:(NSError *)error
{
if (error == nil && [task isKindOfClass:[NSURLSessionDataTask class]]) {
NSURLSessionDataTask *dataTask = (NSURLSessionDataTask *)task;
[self lock];
NSString *cacheKey = [NSURLProtocol propertyForKey:PINRemoteImageCacheKey inRequest:dataTask.originalRequest];
PINRemoteImageDownloadTask *task = [self.tasks objectForKey:cacheKey];
task.sessionTaskEndTime = CACurrentMediaTime();
CFTimeInterval taskLength = task.sessionTaskEndTime - task.sessionTaskStartTime;
[self unlock];
float bytesPerSecond = dataTask.countOfBytesReceived / taskLength;
[self addTaskBPS:bytesPerSecond endDate:[NSDate date]];
}
}
#pragma mark - QOS
- (float)currentBytesPerSecond
{
[self lock];
#if DEBUG
if (self.overrideBPS) {
float currentBPS = self.currentBPS;
[self unlock];
return currentBPS;
}
#endif
const NSTimeInterval validThreshold = 60.0;
__block NSUInteger count = 0;
__block float bps = 0;
__block BOOL valid = NO;
NSDate *threshold = [NSDate dateWithTimeIntervalSinceNow:-validThreshold];
[self.taskQOS enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(PINTaskQOS *taskQOS, NSUInteger idx, BOOL *stop) {
if ([taskQOS.endDate compare:threshold] == NSOrderedAscending) {
*stop = YES;
return;
}
valid = YES;
count++;
bps += taskQOS.bytesPerSecond;
}];
[self unlock];
if (valid == NO) {
return -1;
}
return bps / (float)count;
}
- (void)addTaskBPS:(float)bytesPerSecond endDate:(NSDate *)endDate
{
//if bytesPerSecond is less than or equal to zero, ignore.
if (bytesPerSecond <= 0) {
return;
}
[self lock];
if (self.taskQOS.count >= 5) {
[self.taskQOS removeObjectAtIndex:0];
}
PINTaskQOS *taskQOS = [[PINTaskQOS alloc] initWithBPS:bytesPerSecond endDate:endDate];
[self.taskQOS addObject:taskQOS];
[self.taskQOS sortUsingComparator:^NSComparisonResult(PINTaskQOS *obj1, PINTaskQOS *obj2) {
return [obj1.endDate compare:obj2.endDate];
}];
[self unlock];
}
#if DEBUG
- (void)setCurrentBytesPerSecond:(float)currentBPS
{
[self lockOnMainThread];
_overrideBPS = YES;
_currentBPS = currentBPS;
[self unlock];
}
#endif
- (NSUUID *)downloadImageWithURLs:(NSArray <NSURL *> *)urls
options:(PINRemoteImageManagerDownloadOptions)options
progressImage:(PINRemoteImageManagerImageCompletion)progressImage
completion:(PINRemoteImageManagerImageCompletion)completion
{
NSUUID *UUID = [NSUUID UUID];
if (urls.count <= 1) {
NSURL *url = [urls firstObject];
[self downloadImageWithURL:url
options:options
priority:PINRemoteImageManagerPriorityMedium
processorKey:nil
processor:nil
progressImage:progressImage
progressDownload:nil
completion:completion
inputUUID:UUID];
return UUID;
}
__weak typeof(self) weakSelf = self;
[self.concurrentOperationQueue pin_addOperationWithQueuePriority:PINRemoteImageManagerPriorityMedium block:^{
__block NSInteger highestQualityDownloadedIdx = -1;
typeof(self) strongSelf = weakSelf;
//check for the highest quality image already in cache. It's possible that an image is in the process of being
//cached when this is being run. In which case two things could happen:
// - If network conditions dictate that a lower quality image should be downloaded than the one that is currently
// being cached, it will be downloaded in addition. This is not ideal behavior, worst case scenario and unlikely.
// - If network conditions dictate that the same quality image should be downloaded as the one being cached, no
// new image will be downloaded as either the caching will have finished by the time we actually request it or
// the task will still exist and our callback will be attached. In this case, no detrimental behavior will have
// occurred.
[urls enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(NSURL *url, NSUInteger idx, BOOL *stop) {
typeof(self) strongSelf = weakSelf;
BlockAssert([url isKindOfClass:[NSURL class]], @"url must be of type URL");
NSString *cacheKey = [strongSelf cacheKeyForURL:url processorKey:nil];
//we don't actually need the object, just need to know it exists so that we can request it later
id objectOrFileURL = [self.cache.memoryCache objectForKey:cacheKey];
if (objectOrFileURL == nil) {
objectOrFileURL = [strongSelf.cache.diskCache fileURLForKey:cacheKey];
}
if (objectOrFileURL) {
highestQualityDownloadedIdx = idx;
*stop = YES;
}
}];
float currentBytesPerSecond = [strongSelf currentBytesPerSecond];
[strongSelf lock];
float highQualityQPSThreshold = [strongSelf highQualityBPSThreshold];
float lowQualityQPSThreshold = [strongSelf lowQualityBPSThreshold];
BOOL shouldUpgradeLowQualityImages = [strongSelf shouldUpgradeLowQualityImages];
[strongSelf unlock];
NSUInteger desiredImageURLIdx;
if (currentBytesPerSecond == -1 || currentBytesPerSecond >= highQualityQPSThreshold) {
desiredImageURLIdx = urls.count - 1;
} else if (currentBytesPerSecond <= lowQualityQPSThreshold) {
desiredImageURLIdx = 0;
} else if (urls.count == 2) {
desiredImageURLIdx = roundf((currentBytesPerSecond - lowQualityQPSThreshold) / ((highQualityQPSThreshold - lowQualityQPSThreshold) / (float)(urls.count - 1)));
} else {
desiredImageURLIdx = ceilf((currentBytesPerSecond - lowQualityQPSThreshold) / ((highQualityQPSThreshold - lowQualityQPSThreshold) / (float)(urls.count - 2)));
}
NSUInteger downloadIdx;
//if the highest quality already downloaded is less than what currentBPS would dictate and shouldUpgrade is
//set, download the new higher quality image. If no image has been cached, download the image dictated by
//current bps
if ((highestQualityDownloadedIdx < desiredImageURLIdx && shouldUpgradeLowQualityImages) || highestQualityDownloadedIdx == -1) {
downloadIdx = desiredImageURLIdx;
} else {
downloadIdx = highestQualityDownloadedIdx;
}
NSURL *downloadURL = [urls objectAtIndex:downloadIdx];
[strongSelf downloadImageWithURL:downloadURL
options:options
priority:PINRemoteImageManagerPriorityMedium
processorKey:nil
processor:nil
progressImage:progressImage
progressDownload:nil
completion:^(PINRemoteImageManagerResult *result) {
typeof(self) strongSelf = weakSelf;
//clean out any lower quality images from the cache
for (NSInteger idx = downloadIdx - 1; idx >= 0; idx--) {
[[strongSelf cache] removeObjectForKey:[strongSelf cacheKeyForURL:[urls objectAtIndex:idx] processorKey:nil]];
}
if (completion) {
completion(result);
}
}
inputUUID:UUID];
}];
return UUID;
}
#pragma mark - Caching
- (BOOL)materializeAndCacheObject:(id)object
key:(NSString *)key
options:(PINRemoteImageManagerDownloadOptions)options
outImage:(PINImage **)outImage
outAltRep:(id *)outAlternateRepresentation
{
return [self materializeAndCacheObject:object cacheInDisk:nil additionalCost:0 key:key options:options outImage:outImage outAltRep:outAlternateRepresentation];
}
//takes the object from the cache and returns an image or animated image.
//if it's a non-alternative representation and skipDecode is not set it also decompresses the image.
- (BOOL)materializeAndCacheObject:(id)object
cacheInDisk:(NSData *)diskData
additionalCost:(NSUInteger)additionalCost
key:(NSString *)key
options:(PINRemoteImageManagerDownloadOptions)options
outImage:(PINImage **)outImage
outAltRep:(id *)outAlternateRepresentation
{
NSAssert(object != nil, @"Object should not be nil.");
if (object == nil) {
return NO;
}
BOOL alternateRepresentationsAllowed = (PINRemoteImageManagerDisallowAlternateRepresentations & options) == 0;
BOOL skipDecode = (options & PINRemoteImageManagerDownloadOptionsSkipDecode) != 0;
__block id alternateRepresentation = nil;
__block PINImage *image = nil;
__block NSData *data = nil;
__block BOOL updateMemoryCache = NO;
NSUInteger cacheCost = additionalCost;
PINRemoteImageMemoryContainer *container = nil;
if ([object isKindOfClass:[PINRemoteImageMemoryContainer class]]) {
container = (PINRemoteImageMemoryContainer *)object;
[container.lock lockWithBlock:^{
data = container.data;
}];
} else {
updateMemoryCache = YES;
// don't need to lock the container here because we just init it.
container = [[PINRemoteImageMemoryContainer alloc] init];
if ([object isKindOfClass:[PINImage class]]) {
data = diskData;
container.image = (PINImage *)object;
} else if ([object isKindOfClass:[NSData class]]) {
data = (NSData *)object;
} else {
//invalid item in cache
updateMemoryCache = NO;
data = nil;
container = nil;
}
container.data = data;
}
if (alternateRepresentationsAllowed) {
alternateRepresentation = [_alternateRepProvider alternateRepresentationWithData:data options:options];
}
if (alternateRepresentation == nil) {
//we need the image
[container.lock lockWithBlock:^{
image = container.image;
}];
if (image == nil) {
image = [PINImage pin_decodedImageWithData:container.data skipDecodeIfPossible:skipDecode];
if (skipDecode == NO) {
[container.lock lockWithBlock:^{
updateMemoryCache = YES;
container.image = image;
}];
}
}
}
if (updateMemoryCache) {
cacheCost += [data length];
cacheCost += (image.size.width + image.size.height) * 4; // 4 bytes per pixel
[self.cache.memoryCache setObject:container forKey:key withCost:cacheCost];
}
if (diskData) {
[self.cache.diskCache setObject:diskData forKey:key];
}
if (outImage) {
*outImage = image;
}
if (outAlternateRepresentation) {
*outAlternateRepresentation = alternateRepresentation;
}
if (image == nil && alternateRepresentation == nil) {
PINLog(@"Invalid item in cache");
[self.cache removeObjectForKey:key block:nil];
return NO;
}
return YES;
}
- (NSString *)cacheKeyForURL:(NSURL *)url processorKey:(NSString *)processorKey
{
NSString *cacheKey = [url absoluteString];
if (processorKey.length > 0) {
cacheKey = [cacheKey stringByAppendingString:[NSString stringWithFormat:@"-<%@>", processorKey]];
}
//PINDiskCache uses this key as the filename of the file written to disk
//Due to the current filesystem used in Darwin, this name must be limited to 255 chars.
//In case the generated key exceeds PINRemoteImageManagerCacheKeyMaxLength characters,
//we return the hash of it instead.
if (cacheKey.length > PINRemoteImageManagerCacheKeyMaxLength) {
__block CC_MD5_CTX ctx;
CC_MD5_Init(&ctx);
NSData *data = [cacheKey dataUsingEncoding:NSUTF8StringEncoding];
[data enumerateByteRangesUsingBlock:^(const void * _Nonnull bytes, NSRange byteRange, BOOL * _Nonnull stop) {
CC_MD5_Update(&ctx, bytes, (CC_LONG)byteRange.length);
}];
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5_Final(digest, &ctx);
NSMutableString *hexString = [NSMutableString stringWithCapacity:(CC_MD5_DIGEST_LENGTH * 2)];
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[hexString appendFormat:@"%02lx", (unsigned long)digest[i]];
}
cacheKey = [hexString copy];
}
return cacheKey;
}
- (void)objectForKey:(NSString *)key options:(PINRemoteImageManagerDownloadOptions)options completion:(void (^)(BOOL found, BOOL valid, PINImage *image, id alternativeRepresentation))completion
{
void (^materialize)(id object) = ^(id object) {
PINImage *image = nil;
id alternativeRepresentation = nil;
BOOL valid = [self materializeAndCacheObject:object
key:key
options:options
outImage:&image
outAltRep:&alternativeRepresentation];
completion(YES, valid, image, alternativeRepresentation);
};
PINRemoteImageMemoryContainer *container = [self.cache.memoryCache objectForKey:key];
if (container) {
materialize(container);
} else {
[self.cache.diskCache objectForKey:key block:^(PINDiskCache * _Nonnull cache, NSString * _Nonnull key, id<NSCoding> _Nullable object, NSURL * _Nullable fileURL) {
if (object) {
materialize(object);
} else {
completion(NO, NO, nil, nil);
}
}];
}
}
@end
@implementation NSOperationQueue (PINRemoteImageManager)
- (void)pin_addOperationWithQueuePriority:(PINRemoteImageManagerPriority)priority block:(void (^)(void))block
{
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:block];
operation.queuePriority = operationPriorityWithImageManagerPriority(priority);
if ([PINRemoteImageManager supportsQOS] == NO) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
operation.threadPriority = 0.2;
#pragma clang diagnostic pop
}
[self addOperation:operation];
}
@end
@implementation PINTaskQOS
- (instancetype)initWithBPS:(float)bytesPerSecond endDate:(NSDate *)endDate
{
if (self = [super init]) {
self.endDate = endDate;
self.bytesPerSecond = bytesPerSecond;
}
return self;
}
@end