// PINCache is a modified version of TMCache // Modifications by Garrett Moon // Copyright (c) 2015 Pinterest. All rights reserved. #import "PINDiskCache.h" #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_4_0 #import #endif #import #define PINDiskCacheError(error) if (error) { NSLog(@"%@ (%d) ERROR: %@", \ [[NSString stringWithUTF8String:__FILE__] lastPathComponent], \ __LINE__, [error localizedDescription]); } static NSString * const PINDiskCachePrefix = @"com.pinterest.PINDiskCache"; static NSString * const PINDiskCacheSharedName = @"PINDiskCacheShared"; @interface PINBackgroundTask : NSObject #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_4_0 && !TARGET_OS_WATCH @property (atomic, assign) UIBackgroundTaskIdentifier taskID; #endif + (instancetype)start; - (void)end; @end typedef NS_ENUM(NSUInteger, PINDiskCacheCondition) { PINDiskCacheConditionNotReady = 0, PINDiskCacheConditionReady = 1, }; @interface PINDiskCache () { NSConditionLock *_instanceLock; } @property (assign) NSUInteger byteCount; @property (strong, nonatomic) NSURL *cacheURL; #if OS_OBJECT_USE_OBJC @property (strong, nonatomic) dispatch_queue_t asyncQueue; #else @property (assign, nonatomic) dispatch_queue_t asyncQueue; #endif @property (strong, nonatomic) NSMutableDictionary *dates; @property (strong, nonatomic) NSMutableDictionary *sizes; @end @implementation PINDiskCache @synthesize willAddObjectBlock = _willAddObjectBlock; @synthesize willRemoveObjectBlock = _willRemoveObjectBlock; @synthesize willRemoveAllObjectsBlock = _willRemoveAllObjectsBlock; @synthesize didAddObjectBlock = _didAddObjectBlock; @synthesize didRemoveObjectBlock = _didRemoveObjectBlock; @synthesize didRemoveAllObjectsBlock = _didRemoveAllObjectsBlock; @synthesize byteLimit = _byteLimit; @synthesize ageLimit = _ageLimit; @synthesize ttlCache = _ttlCache; #if TARGET_OS_IPHONE @synthesize writingProtectionOption = _writingProtectionOption; #endif #pragma mark - Initialization - - (void)dealloc { #if !OS_OBJECT_USE_OBJC dispatch_release(_asyncQueue); _asyncQueue = nil; #endif } - (instancetype)init { @throw [NSException exceptionWithName:@"Must initialize with a name" reason:@"PINDiskCache must be initialized with a name. Call initWithName: instead." userInfo:nil]; return [self initWithName:@""]; } - (instancetype)initWithName:(NSString *)name { return [self initWithName:name rootPath:[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]]; } - (instancetype)initWithName:(NSString *)name rootPath:(NSString *)rootPath { if (!name) return nil; if (self = [super init]) { _name = [name copy]; _asyncQueue = dispatch_queue_create([[NSString stringWithFormat:@"%@ Asynchronous Queue", PINDiskCachePrefix] UTF8String], DISPATCH_QUEUE_CONCURRENT); _instanceLock = [[NSConditionLock alloc] initWithCondition:PINDiskCacheConditionNotReady]; _willAddObjectBlock = nil; _willRemoveObjectBlock = nil; _willRemoveAllObjectsBlock = nil; _didAddObjectBlock = nil; _didRemoveObjectBlock = nil; _didRemoveAllObjectsBlock = nil; _byteCount = 0; _byteLimit = 0; _ageLimit = 0.0; #if TARGET_OS_IPHONE _writingProtectionOption = NSDataWritingFileProtectionNone; #endif _dates = [[NSMutableDictionary alloc] init]; _sizes = [[NSMutableDictionary alloc] init]; NSString *pathComponent = [[NSString alloc] initWithFormat:@"%@.%@", PINDiskCachePrefix, _name]; _cacheURL = [NSURL fileURLWithPathComponents:@[ rootPath, pathComponent ]]; //we don't want to do anything without setting up the disk cache, but we also don't want to block init, it can take a while to initialize dispatch_async(_asyncQueue, ^{ //should always be able to aquire the lock unless the below code is running. [_instanceLock lockWhenCondition:PINDiskCacheConditionNotReady]; [self createCacheDirectory]; [self initializeDiskProperties]; [_instanceLock unlockWithCondition:PINDiskCacheConditionReady]; }); } return self; } - (NSString *)description { return [[NSString alloc] initWithFormat:@"%@.%@.%p", PINDiskCachePrefix, _name, (void *)self]; } + (instancetype)sharedCache { static id cache; static dispatch_once_t predicate; dispatch_once(&predicate, ^{ cache = [[self alloc] initWithName:PINDiskCacheSharedName]; }); return cache; } #pragma mark - Private Methods - - (NSURL *)encodedFileURLForKey:(NSString *)key { if (![key length]) return nil; return [_cacheURL URLByAppendingPathComponent:[self encodedString:key]]; } - (NSString *)keyForEncodedFileURL:(NSURL *)url { NSString *fileName = [url lastPathComponent]; if (!fileName) return nil; return [self decodedString:fileName]; } - (NSString *)encodedString:(NSString *)string { if (![string length]) { return @""; } if ([string respondsToSelector:@selector(stringByAddingPercentEncodingWithAllowedCharacters:)]) { return [string stringByAddingPercentEncodingWithAllowedCharacters:[[NSCharacterSet characterSetWithCharactersInString:@".:/%"] invertedSet]]; } else { CFStringRef static const charsToEscape = CFSTR(".:/%"); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" CFStringRef escapedString = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)string, NULL, charsToEscape, kCFStringEncodingUTF8); #pragma clang diagnostic pop return (__bridge_transfer NSString *)escapedString; } } - (NSString *)decodedString:(NSString *)string { if (![string length]) { return @""; } if ([string respondsToSelector:@selector(stringByRemovingPercentEncoding)]) { return [string stringByRemovingPercentEncoding]; } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" CFStringRef unescapedString = CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, (__bridge CFStringRef)string, CFSTR(""), kCFStringEncodingUTF8); #pragma clang diagnostic pop return (__bridge_transfer NSString *)unescapedString; } } #pragma mark - Private Trash Methods - + (dispatch_queue_t)sharedTrashQueue { static dispatch_queue_t trashQueue; static dispatch_once_t predicate; dispatch_once(&predicate, ^{ NSString *queueName = [[NSString alloc] initWithFormat:@"%@.trash", PINDiskCachePrefix]; trashQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(trashQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)); }); return trashQueue; } + (NSURL *)sharedTrashURL { static NSURL *sharedTrashURL; static dispatch_once_t predicate; dispatch_once(&predicate, ^{ sharedTrashURL = [[[NSURL alloc] initFileURLWithPath:NSTemporaryDirectory()] URLByAppendingPathComponent:PINDiskCachePrefix isDirectory:YES]; if (![[NSFileManager defaultManager] fileExistsAtPath:[sharedTrashURL path]]) { NSError *error = nil; [[NSFileManager defaultManager] createDirectoryAtURL:sharedTrashURL withIntermediateDirectories:YES attributes:nil error:&error]; PINDiskCacheError(error); } }); return sharedTrashURL; } +(BOOL)moveItemAtURLToTrash:(NSURL *)itemURL { if (![[NSFileManager defaultManager] fileExistsAtPath:[itemURL path]]) return NO; NSError *error = nil; NSString *uniqueString = [[NSProcessInfo processInfo] globallyUniqueString]; NSURL *uniqueTrashURL = [[PINDiskCache sharedTrashURL] URLByAppendingPathComponent:uniqueString]; BOOL moved = [[NSFileManager defaultManager] moveItemAtURL:itemURL toURL:uniqueTrashURL error:&error]; PINDiskCacheError(error); return moved; } + (void)emptyTrash { dispatch_async([self sharedTrashQueue], ^{ PINBackgroundTask *task = [PINBackgroundTask start]; NSError *searchTrashedItemsError = nil; NSArray *trashedItems = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:[self sharedTrashURL] includingPropertiesForKeys:nil options:0 error:&searchTrashedItemsError]; PINDiskCacheError(searchTrashedItemsError); for (NSURL *trashedItemURL in trashedItems) { NSError *removeTrashedItemError = nil; [[NSFileManager defaultManager] removeItemAtURL:trashedItemURL error:&removeTrashedItemError]; PINDiskCacheError(removeTrashedItemError); } [task end]; }); } #pragma mark - Private Queue Methods - - (BOOL)createCacheDirectory { if ([[NSFileManager defaultManager] fileExistsAtPath:[_cacheURL path]]) return NO; NSError *error = nil; BOOL success = [[NSFileManager defaultManager] createDirectoryAtURL:_cacheURL withIntermediateDirectories:YES attributes:nil error:&error]; PINDiskCacheError(error); return success; } - (void)initializeDiskProperties { NSUInteger byteCount = 0; NSArray *keys = @[ NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey ]; NSError *error = nil; NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:_cacheURL includingPropertiesForKeys:keys options:NSDirectoryEnumerationSkipsHiddenFiles error:&error]; PINDiskCacheError(error); for (NSURL *fileURL in files) { NSString *key = [self keyForEncodedFileURL:fileURL]; error = nil; NSDictionary *dictionary = [fileURL resourceValuesForKeys:keys error:&error]; PINDiskCacheError(error); NSDate *date = [dictionary objectForKey:NSURLContentModificationDateKey]; if (date && key) [_dates setObject:date forKey:key]; NSNumber *fileSize = [dictionary objectForKey:NSURLTotalFileAllocatedSizeKey]; if (fileSize) { [_sizes setObject:fileSize forKey:key]; byteCount += [fileSize unsignedIntegerValue]; } } if (byteCount > 0) self.byteCount = byteCount; // atomic } - (BOOL)setFileModificationDate:(NSDate *)date forURL:(NSURL *)fileURL { if (!date || !fileURL) { return NO; } NSError *error = nil; BOOL success = [[NSFileManager defaultManager] setAttributes:@{ NSFileModificationDate: date } ofItemAtPath:[fileURL path] error:&error]; PINDiskCacheError(error); if (success) { NSString *key = [self keyForEncodedFileURL:fileURL]; if (key) { [_dates setObject:date forKey:key]; } } return success; } - (BOOL)removeFileAndExecuteBlocksForKey:(NSString *)key { NSURL *fileURL = [self encodedFileURLForKey:key]; if (!fileURL || ![[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]]) return NO; if (_willRemoveObjectBlock) _willRemoveObjectBlock(self, key, nil, fileURL); BOOL trashed = [PINDiskCache moveItemAtURLToTrash:fileURL]; if (!trashed) return NO; [PINDiskCache emptyTrash]; NSNumber *byteSize = [_sizes objectForKey:key]; if (byteSize) self.byteCount = _byteCount - [byteSize unsignedIntegerValue]; // atomic [_sizes removeObjectForKey:key]; [_dates removeObjectForKey:key]; if (_didRemoveObjectBlock) _didRemoveObjectBlock(self, key, nil, fileURL); return YES; } - (void)trimDiskToSize:(NSUInteger)trimByteCount { if (_byteCount <= trimByteCount) return; NSArray *keysSortedBySize = [_sizes keysSortedByValueUsingSelector:@selector(compare:)]; for (NSString *key in [keysSortedBySize reverseObjectEnumerator]) { // largest objects first [self removeFileAndExecuteBlocksForKey:key]; if (_byteCount <= trimByteCount) break; } } - (void)trimDiskToSizeByDate:(NSUInteger)trimByteCount { if (_byteCount <= trimByteCount) return; NSArray *keysSortedByDate = [_dates keysSortedByValueUsingSelector:@selector(compare:)]; for (NSString *key in keysSortedByDate) { // oldest objects first [self removeFileAndExecuteBlocksForKey:key]; if (_byteCount <= trimByteCount) break; } } - (void)trimDiskToDate:(NSDate *)trimDate { NSArray *keysSortedByDate = [_dates keysSortedByValueUsingSelector:@selector(compare:)]; for (NSString *key in keysSortedByDate) { // oldest files first NSDate *accessDate = [_dates objectForKey:key]; if (!accessDate) continue; if ([accessDate compare:trimDate] == NSOrderedAscending) { // older than trim date [self removeFileAndExecuteBlocksForKey:key]; } else { break; } } } - (void)trimToAgeLimitRecursively { [self lock]; NSTimeInterval ageLimit = _ageLimit; [self unlock]; if (ageLimit == 0.0) return; [self lock]; NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:-ageLimit]; [self trimDiskToDate:date]; [self unlock]; __weak PINDiskCache *weakSelf = self; dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_ageLimit * NSEC_PER_SEC)); dispatch_after(time, _asyncQueue, ^(void) { PINDiskCache *strongSelf = weakSelf; [strongSelf trimToAgeLimitRecursively]; }); } #pragma mark - Public Asynchronous Methods - - (void)lockFileAccessWhileExecutingBlock:(void(^)(PINDiskCache *diskCache))block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; if (block) { [strongSelf lock]; block(strongSelf); [strongSelf unlock]; } }); } - (void)containsObjectForKey:(NSString *)key block:(PINDiskCacheContainsBlock)block { if (!key || !block) return; __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; block([strongSelf containsObjectForKey:key]); }); } - (void)objectForKey:(NSString *)key block:(PINDiskCacheObjectBlock)block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; NSURL *fileURL = nil; id object = [strongSelf objectForKey:key fileURL:&fileURL]; if (block) { [strongSelf lock]; block(strongSelf, key, object, fileURL); [strongSelf unlock]; } }); } - (void)fileURLForKey:(NSString *)key block:(PINDiskCacheObjectBlock)block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; NSURL *fileURL = [strongSelf fileURLForKey:key]; if (block) { [strongSelf lock]; block(strongSelf, key, nil, fileURL); [strongSelf unlock]; } }); } - (void)setObject:(id )object forKey:(NSString *)key block:(PINDiskCacheObjectBlock)block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; NSURL *fileURL = nil; [strongSelf setObject:object forKey:key fileURL:&fileURL]; if (block) { [strongSelf lock]; block(strongSelf, key, object, fileURL); [strongSelf unlock]; } }); } - (void)removeObjectForKey:(NSString *)key block:(PINDiskCacheObjectBlock)block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; NSURL *fileURL = nil; [strongSelf removeObjectForKey:key fileURL:&fileURL]; if (block) { [strongSelf lock]; block(strongSelf, key, nil, fileURL); [strongSelf unlock]; } }); } - (void)trimToSize:(NSUInteger)trimByteCount block:(PINDiskCacheBlock)block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; [strongSelf trimToSize:trimByteCount]; if (block) { [strongSelf lock]; block(strongSelf); [strongSelf unlock]; } }); } - (void)trimToDate:(NSDate *)trimDate block:(PINDiskCacheBlock)block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; [strongSelf trimToDate:trimDate]; if (block) { [strongSelf lock]; block(strongSelf); [strongSelf unlock]; } }); } - (void)trimToSizeByDate:(NSUInteger)trimByteCount block:(PINDiskCacheBlock)block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; [strongSelf trimToSizeByDate:trimByteCount]; if (block) { [strongSelf lock]; block(strongSelf); [strongSelf unlock]; } }); } - (void)removeAllObjects:(PINDiskCacheBlock)block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; [strongSelf removeAllObjects]; if (block) { [strongSelf lock]; block(strongSelf); [strongSelf unlock]; } }); } - (void)enumerateObjectsWithBlock:(PINDiskCacheObjectBlock)block completionBlock:(PINDiskCacheBlock)completionBlock { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; [strongSelf enumerateObjectsWithBlock:block]; if (completionBlock) { [self lock]; completionBlock(strongSelf); [self unlock]; } }); } #pragma mark - Public Synchronous Methods - - (void)synchronouslyLockFileAccessWhileExecutingBlock:(void(^)(PINDiskCache *diskCache))block { if (block) { [self lock]; block(self); [self unlock]; } } - (BOOL)containsObjectForKey:(NSString *)key { return ([self fileURLForKey:key updateFileModificationDate:NO] != nil); } - (__nullable id)objectForKey:(NSString *)key { return [self objectForKey:key fileURL:nil]; } - (id)objectForKeyedSubscript:(NSString *)key { return [self objectForKey:key]; } - (__nullable id )objectForKey:(NSString *)key fileURL:(NSURL **)outFileURL { NSDate *now = [[NSDate alloc] init]; if (!key) return nil; id object = nil; NSURL *fileURL = nil; [self lock]; fileURL = [self encodedFileURLForKey:key]; object = nil; if ([[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]] && // If the cache should behave like a TTL cache, then only fetch the object if there's a valid ageLimit and the object is still alive (!self->_ttlCache || self->_ageLimit <= 0 || fabs([[_dates objectForKey:key] timeIntervalSinceDate:now]) < self->_ageLimit)) { @try { object = [NSKeyedUnarchiver unarchiveObjectWithFile:[fileURL path]]; } @catch (NSException *exception) { NSError *error = nil; [[NSFileManager defaultManager] removeItemAtPath:[fileURL path] error:&error]; PINDiskCacheError(error); } if (!self->_ttlCache) { [self setFileModificationDate:now forURL:fileURL]; } } [self unlock]; if (outFileURL) { *outFileURL = fileURL; } return object; } /// Helper function to call fileURLForKey:updateFileModificationDate: - (NSURL *)fileURLForKey:(NSString *)key { // Don't update the file modification time, if self is a ttlCache return [self fileURLForKey:key updateFileModificationDate:!self->_ttlCache]; } - (NSURL *)fileURLForKey:(NSString *)key updateFileModificationDate:(BOOL)updateFileModificationDate { if (!key) { return nil; } NSDate *now = [[NSDate alloc] init]; NSURL *fileURL = nil; [self lock]; fileURL = [self encodedFileURLForKey:key]; if ([[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]]) { if (updateFileModificationDate) { [self setFileModificationDate:now forURL:fileURL]; } } else { fileURL = nil; } [self unlock]; return fileURL; } - (void)setObject:(id )object forKey:(NSString *)key { [self setObject:object forKey:key fileURL:nil]; } - (void)setObject:(id)object forKeyedSubscript:(NSString *)key { [self setObject:object forKey:key]; } - (void)setObject:(id )object forKey:(NSString *)key fileURL:(NSURL **)outFileURL { NSDate *now = [[NSDate alloc] init]; if (!key || !object) return; PINBackgroundTask *task = [PINBackgroundTask start]; #if TARGET_OS_IPHONE NSDataWritingOptions writeOptions = NSDataWritingAtomic | self.writingProtectionOption; #else NSDataWritingOptions writeOptions = NSDataWritingAtomic; #endif NSURL *fileURL = nil; [self lock]; fileURL = [self encodedFileURLForKey:key]; if (self->_willAddObjectBlock) self->_willAddObjectBlock(self, key, object, fileURL); NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object]; NSError *writeError = nil; BOOL written = [data writeToURL:fileURL options:writeOptions error:&writeError]; PINDiskCacheError(writeError); if (written) { [self setFileModificationDate:now forURL:fileURL]; NSError *error = nil; NSDictionary *values = [fileURL resourceValuesForKeys:@[ NSURLTotalFileAllocatedSizeKey ] error:&error]; PINDiskCacheError(error); NSNumber *diskFileSize = [values objectForKey:NSURLTotalFileAllocatedSizeKey]; if (diskFileSize) { NSNumber *prevDiskFileSize = [self->_sizes objectForKey:key]; if (prevDiskFileSize) { self.byteCount = self->_byteCount - [prevDiskFileSize unsignedIntegerValue]; } [self->_sizes setObject:diskFileSize forKey:key]; self.byteCount = self->_byteCount + [diskFileSize unsignedIntegerValue]; // atomic } if (self->_byteLimit > 0 && self->_byteCount > self->_byteLimit) [self trimToSizeByDate:self->_byteLimit block:nil]; } else { fileURL = nil; } if (self->_didAddObjectBlock) self->_didAddObjectBlock(self, key, object, written ? fileURL : nil); [self unlock]; if (outFileURL) { *outFileURL = fileURL; } [task end]; } - (void)removeObjectForKey:(NSString *)key { [self removeObjectForKey:key fileURL:nil]; } - (void)removeObjectForKey:(NSString *)key fileURL:(NSURL **)outFileURL { if (!key) return; PINBackgroundTask *task = [PINBackgroundTask start]; NSURL *fileURL = nil; [self lock]; fileURL = [self encodedFileURLForKey:key]; [self removeFileAndExecuteBlocksForKey:key]; [self unlock]; [task end]; if (outFileURL) { *outFileURL = fileURL; } } - (void)trimToSize:(NSUInteger)trimByteCount { if (trimByteCount == 0) { [self removeAllObjects]; return; } PINBackgroundTask *task = [PINBackgroundTask start]; [self lock]; [self trimDiskToSize:trimByteCount]; [self unlock]; [task end]; } - (void)trimToDate:(NSDate *)trimDate { if (!trimDate) return; if ([trimDate isEqualToDate:[NSDate distantPast]]) { [self removeAllObjects]; return; } PINBackgroundTask *task = [PINBackgroundTask start]; [self lock]; [self trimDiskToDate:trimDate]; [self unlock]; [task end]; } - (void)trimToSizeByDate:(NSUInteger)trimByteCount { if (trimByteCount == 0) { [self removeAllObjects]; return; } PINBackgroundTask *task = [PINBackgroundTask start]; [self lock]; [self trimDiskToSizeByDate:trimByteCount]; [self unlock]; [task end]; } - (void)removeAllObjects { PINBackgroundTask *task = [PINBackgroundTask start]; [self lock]; if (self->_willRemoveAllObjectsBlock) self->_willRemoveAllObjectsBlock(self); [PINDiskCache moveItemAtURLToTrash:self->_cacheURL]; [PINDiskCache emptyTrash]; [self createCacheDirectory]; [self->_dates removeAllObjects]; [self->_sizes removeAllObjects]; self.byteCount = 0; // atomic if (self->_didRemoveAllObjectsBlock) self->_didRemoveAllObjectsBlock(self); [self unlock]; [task end]; } - (void)enumerateObjectsWithBlock:(PINDiskCacheObjectBlock)block { if (!block) return; PINBackgroundTask *task = [PINBackgroundTask start]; [self lock]; NSDate *now = [NSDate date]; NSArray *keysSortedByDate = [self->_dates keysSortedByValueUsingSelector:@selector(compare:)]; for (NSString *key in keysSortedByDate) { NSURL *fileURL = [self encodedFileURLForKey:key]; // If the cache should behave like a TTL cache, then only fetch the object if there's a valid ageLimit and the object is still alive if (!self->_ttlCache || self->_ageLimit <= 0 || fabs([[_dates objectForKey:key] timeIntervalSinceDate:now]) < self->_ageLimit) { block(self, key, nil, fileURL); } } [self unlock]; [task end]; } #pragma mark - Public Thread Safe Accessors - - (PINDiskCacheObjectBlock)willAddObjectBlock { PINDiskCacheObjectBlock block = nil; [self lock]; block = _willAddObjectBlock; [self unlock]; return block; } - (void)setWillAddObjectBlock:(PINDiskCacheObjectBlock)block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; if (!strongSelf) return; [strongSelf lock]; strongSelf->_willAddObjectBlock = [block copy]; [strongSelf unlock]; }); } - (PINDiskCacheObjectBlock)willRemoveObjectBlock { PINDiskCacheObjectBlock block = nil; [self lock]; block = _willRemoveObjectBlock; [self unlock]; return block; } - (void)setWillRemoveObjectBlock:(PINDiskCacheObjectBlock)block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; if (!strongSelf) return; [strongSelf lock]; strongSelf->_willRemoveObjectBlock = [block copy]; [strongSelf unlock]; }); } - (PINDiskCacheBlock)willRemoveAllObjectsBlock { PINDiskCacheBlock block = nil; [self lock]; block = _willRemoveAllObjectsBlock; [self unlock]; return block; } - (void)setWillRemoveAllObjectsBlock:(PINDiskCacheBlock)block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; if (!strongSelf) return; [strongSelf lock]; strongSelf->_willRemoveAllObjectsBlock = [block copy]; [strongSelf unlock]; }); } - (PINDiskCacheObjectBlock)didAddObjectBlock { PINDiskCacheObjectBlock block = nil; [self lock]; block = _didAddObjectBlock; [self unlock]; return block; } - (void)setDidAddObjectBlock:(PINDiskCacheObjectBlock)block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; if (!strongSelf) return; [strongSelf lock]; strongSelf->_didAddObjectBlock = [block copy]; [strongSelf unlock]; }); } - (PINDiskCacheObjectBlock)didRemoveObjectBlock { PINDiskCacheObjectBlock block = nil; [self lock]; block = _didRemoveObjectBlock; [self unlock]; return block; } - (void)setDidRemoveObjectBlock:(PINDiskCacheObjectBlock)block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; if (!strongSelf) return; [strongSelf lock]; strongSelf->_didRemoveObjectBlock = [block copy]; [strongSelf unlock]; }); } - (PINDiskCacheBlock)didRemoveAllObjectsBlock { PINDiskCacheBlock block = nil; [self lock]; block = _didRemoveAllObjectsBlock; [self unlock]; return block; } - (void)setDidRemoveAllObjectsBlock:(PINDiskCacheBlock)block { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; if (!strongSelf) return; [strongSelf lock]; strongSelf->_didRemoveAllObjectsBlock = [block copy]; [strongSelf unlock]; }); } - (NSUInteger)byteLimit { NSUInteger byteLimit; [self lock]; byteLimit = _byteLimit; [self unlock]; return byteLimit; } - (void)setByteLimit:(NSUInteger)byteLimit { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; if (!strongSelf) return; [strongSelf lock]; strongSelf->_byteLimit = byteLimit; if (byteLimit > 0) [strongSelf trimDiskToSizeByDate:byteLimit]; [strongSelf unlock]; }); } - (NSTimeInterval)ageLimit { NSTimeInterval ageLimit; [self lock]; ageLimit = _ageLimit; [self unlock]; return ageLimit; } - (void)setAgeLimit:(NSTimeInterval)ageLimit { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; if (!strongSelf) return; [strongSelf lock]; strongSelf->_ageLimit = ageLimit; [strongSelf unlock]; [strongSelf trimToAgeLimitRecursively]; }); } - (BOOL)isTTLCache { BOOL isTTLCache; [self lock]; isTTLCache = _ttlCache; [self unlock]; return isTTLCache; } - (void)setTtlCache:(BOOL)ttlCache { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; if (!strongSelf) return; [strongSelf lock]; strongSelf->_ttlCache = ttlCache; [strongSelf unlock]; }); } #if TARGET_OS_IPHONE - (NSDataWritingOptions)writingProtectionOption { NSDataWritingOptions option; [self lock]; option = _writingProtectionOption; [self unlock]; return option; } - (void)setWritingProtectionOption:(NSDataWritingOptions)writingProtectionOption { __weak PINDiskCache *weakSelf = self; dispatch_async(_asyncQueue, ^{ PINDiskCache *strongSelf = weakSelf; if (!strongSelf) return; NSDataWritingOptions option = NSDataWritingFileProtectionMask & writingProtectionOption; [strongSelf lock]; strongSelf->_writingProtectionOption = option; [strongSelf unlock]; }); } #endif - (void)lock { [_instanceLock lockWhenCondition:PINDiskCacheConditionReady]; } - (void)unlock { [_instanceLock unlockWithCondition:PINDiskCacheConditionReady]; } @end @implementation PINBackgroundTask + (BOOL)isAppExtension { static BOOL isExtension; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSDictionary *extensionDictionary = [[NSBundle mainBundle] infoDictionary][@"NSExtension"]; isExtension = [extensionDictionary isKindOfClass:[NSDictionary class]]; }); return isExtension; } - (instancetype)init { if (self = [super init]) { #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_4_0 && !TARGET_OS_WATCH _taskID = UIBackgroundTaskInvalid; #endif } return self; } + (instancetype)start { PINBackgroundTask *task = nil; #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_4_0 && !TARGET_OS_WATCH if ([self.class isAppExtension]) { return task; } task = [[self alloc] init]; UIApplication *sharedApplication = [UIApplication performSelector:@selector(sharedApplication)]; task.taskID = [sharedApplication beginBackgroundTaskWithExpirationHandler:^{ UIBackgroundTaskIdentifier taskID = task.taskID; task.taskID = UIBackgroundTaskInvalid; [sharedApplication endBackgroundTask:taskID]; }]; #endif return task; } - (void)end { #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_4_0 && !TARGET_OS_WATCH if ([self.class isAppExtension]) { return; } UIBackgroundTaskIdentifier taskID = self.taskID; self.taskID = UIBackgroundTaskInvalid; UIApplication *sharedApplication = [UIApplication performSelector:@selector(sharedApplication)]; [sharedApplication endBackgroundTask:taskID]; #endif } @end