| /* |
| * This file is part of the SDWebImage package. |
| * (c) Olivier Poitrey <rs@dailymotion.com> |
| * |
| * For the full copyright and license information, please view the LICENSE |
| * file that was distributed with this source code. |
| */ |
| |
| #import "SDImageCache.h" |
| #import "SDWebImageDecoder.h" |
| #import "UIImage+MultiFormat.h" |
| #import <CommonCrypto/CommonDigest.h> |
| |
| static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week |
| // PNG signature bytes and data (below) |
| static unsigned char kPNGSignatureBytes[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; |
| static NSData *kPNGSignatureData = nil; |
| |
| BOOL ImageDataHasPNGPreffix(NSData *data); |
| |
| BOOL ImageDataHasPNGPreffix(NSData *data) { |
| NSUInteger pngSignatureLength = [kPNGSignatureData length]; |
| if ([data length] >= pngSignatureLength) { |
| if ([[data subdataWithRange:NSMakeRange(0, pngSignatureLength)] isEqualToData:kPNGSignatureData]) { |
| return YES; |
| } |
| } |
| |
| return NO; |
| } |
| |
| @interface SDImageCache () |
| |
| @property (strong, nonatomic) NSCache *memCache; |
| @property (strong, nonatomic) NSString *diskCachePath; |
| @property (strong, nonatomic) NSMutableArray *customPaths; |
| @property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue; |
| |
| @end |
| |
| |
| @implementation SDImageCache { |
| NSFileManager *_fileManager; |
| } |
| |
| + (SDImageCache *)sharedImageCache { |
| static dispatch_once_t once; |
| static id instance; |
| dispatch_once(&once, ^{ |
| instance = [self new]; |
| kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8]; |
| }); |
| return instance; |
| } |
| |
| - (id)init { |
| return [self initWithNamespace:@"default"]; |
| } |
| |
| - (id)initWithNamespace:(NSString *)ns { |
| if ((self = [super init])) { |
| NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns]; |
| |
| // Create IO serial queue |
| _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL); |
| |
| // Init default values |
| _maxCacheAge = kDefaultCacheMaxCacheAge; |
| |
| // Init the memory cache |
| _memCache = [[NSCache alloc] init]; |
| _memCache.name = fullNamespace; |
| |
| // Init the disk cache |
| NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); |
| _diskCachePath = [paths[0] stringByAppendingPathComponent:fullNamespace]; |
| |
| dispatch_sync(_ioQueue, ^{ |
| _fileManager = [NSFileManager new]; |
| }); |
| |
| #if TARGET_OS_IPHONE |
| // Subscribe to app events |
| [[NSNotificationCenter defaultCenter] addObserver:self |
| selector:@selector(clearMemory) |
| name:UIApplicationDidReceiveMemoryWarningNotification |
| object:nil]; |
| |
| [[NSNotificationCenter defaultCenter] addObserver:self |
| selector:@selector(cleanDisk) |
| name:UIApplicationWillTerminateNotification |
| object:nil]; |
| |
| [[NSNotificationCenter defaultCenter] addObserver:self |
| selector:@selector(backgroundCleanDisk) |
| name:UIApplicationDidEnterBackgroundNotification |
| object:nil]; |
| #endif |
| } |
| |
| return self; |
| } |
| |
| - (void)dealloc { |
| [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| SDDispatchQueueRelease(_ioQueue); |
| } |
| |
| - (void)addReadOnlyCachePath:(NSString *)path { |
| if (!self.customPaths) { |
| self.customPaths = [NSMutableArray new]; |
| } |
| |
| if (![self.customPaths containsObject:path]) { |
| [self.customPaths addObject:path]; |
| } |
| } |
| |
| - (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path { |
| NSString *filename = [self cachedFileNameForKey:key]; |
| return [path stringByAppendingPathComponent:filename]; |
| } |
| |
| - (NSString *)defaultCachePathForKey:(NSString *)key { |
| return [self cachePathForKey:key inPath:self.diskCachePath]; |
| } |
| |
| #pragma mark SDImageCache (private) |
| |
| - (NSString *)cachedFileNameForKey:(NSString *)key { |
| const char *str = [key UTF8String]; |
| if (str == NULL) { |
| str = ""; |
| } |
| unsigned char r[CC_MD5_DIGEST_LENGTH]; |
| CC_MD5(str, (CC_LONG)strlen(str), r); |
| NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", |
| r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15]]; |
| |
| return filename; |
| } |
| |
| #pragma mark ImageCache |
| |
| - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk { |
| if (!image || !key) { |
| return; |
| } |
| |
| [self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale]; |
| |
| if (toDisk) { |
| dispatch_async(self.ioQueue, ^{ |
| NSData *data = imageData; |
| |
| if (image && (recalculate || !data)) { |
| #if TARGET_OS_IPHONE |
| // We need to determine if the image is a PNG or a JPEG |
| // PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html) |
| // The first eight bytes of a PNG file always contain the following (decimal) values: |
| // 137 80 78 71 13 10 26 10 |
| |
| // We assume the image is PNG, in case the imageData is nil (i.e. if trying to save a UIImage directly), |
| // we will consider it PNG to avoid loosing the transparency |
| BOOL imageIsPng = YES; |
| |
| // But if we have an image data, we will look at the preffix |
| if ([imageData length] >= [kPNGSignatureData length]) { |
| imageIsPng = ImageDataHasPNGPreffix(imageData); |
| } |
| |
| if (imageIsPng) { |
| data = UIImagePNGRepresentation(image); |
| } |
| else { |
| data = UIImageJPEGRepresentation(image, (CGFloat)1.0); |
| } |
| #else |
| data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil]; |
| #endif |
| } |
| |
| if (data) { |
| if (![_fileManager fileExistsAtPath:_diskCachePath]) { |
| [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL]; |
| } |
| |
| [_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil]; |
| } |
| }); |
| } |
| } |
| |
| - (void)storeImage:(UIImage *)image forKey:(NSString *)key { |
| [self storeImage:image recalculateFromImage:YES imageData:nil forKey:key toDisk:YES]; |
| } |
| |
| - (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk { |
| [self storeImage:image recalculateFromImage:YES imageData:nil forKey:key toDisk:toDisk]; |
| } |
| |
| - (BOOL)diskImageExistsWithKey:(NSString *)key { |
| BOOL exists = NO; |
| |
| // this is an exception to access the filemanager on another queue than ioQueue, but we are using the shared instance |
| // from apple docs on NSFileManager: The methods of the shared NSFileManager object can be called from multiple threads safely. |
| exists = [[NSFileManager defaultManager] fileExistsAtPath:[self defaultCachePathForKey:key]]; |
| |
| return exists; |
| } |
| |
| - (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock { |
| dispatch_async(_ioQueue, ^{ |
| BOOL exists = [_fileManager fileExistsAtPath:[self defaultCachePathForKey:key]]; |
| if (completionBlock) { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| completionBlock(exists); |
| }); |
| } |
| }); |
| } |
| |
| - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key { |
| return [self.memCache objectForKey:key]; |
| } |
| |
| - (UIImage *)imageFromDiskCacheForKey:(NSString *)key { |
| // First check the in-memory cache... |
| UIImage *image = [self imageFromMemoryCacheForKey:key]; |
| if (image) { |
| return image; |
| } |
| |
| // Second check the disk cache... |
| UIImage *diskImage = [self diskImageForKey:key]; |
| if (diskImage) { |
| CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale; |
| [self.memCache setObject:diskImage forKey:key cost:cost]; |
| } |
| |
| return diskImage; |
| } |
| |
| - (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key { |
| NSString *defaultPath = [self defaultCachePathForKey:key]; |
| NSData *data = [NSData dataWithContentsOfFile:defaultPath]; |
| if (data) { |
| return data; |
| } |
| |
| for (NSString *path in self.customPaths) { |
| NSString *filePath = [self cachePathForKey:key inPath:path]; |
| NSData *imageData = [NSData dataWithContentsOfFile:filePath]; |
| if (imageData) { |
| return imageData; |
| } |
| } |
| |
| return nil; |
| } |
| |
| - (UIImage *)diskImageForKey:(NSString *)key { |
| NSData *data = [self diskImageDataBySearchingAllPathsForKey:key]; |
| if (data) { |
| UIImage *image = [UIImage sd_imageWithData:data]; |
| image = [self scaledImageForKey:key image:image]; |
| image = [UIImage decodedImageWithImage:image]; |
| return image; |
| } |
| else { |
| return nil; |
| } |
| } |
| |
| - (UIImage *)scaledImageForKey:(NSString *)key image:(UIImage *)image { |
| return SDScaledImageForKey(key, image); |
| } |
| |
| - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock { |
| if (!doneBlock) { |
| return nil; |
| } |
| |
| if (!key) { |
| doneBlock(nil, SDImageCacheTypeNone); |
| return nil; |
| } |
| |
| // First check the in-memory cache... |
| UIImage *image = [self imageFromMemoryCacheForKey:key]; |
| if (image) { |
| doneBlock(image, SDImageCacheTypeMemory); |
| return nil; |
| } |
| |
| NSOperation *operation = [NSOperation new]; |
| dispatch_async(self.ioQueue, ^{ |
| if (operation.isCancelled) { |
| return; |
| } |
| |
| @autoreleasepool { |
| UIImage *diskImage = [self diskImageForKey:key]; |
| if (diskImage) { |
| CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale; |
| [self.memCache setObject:diskImage forKey:key cost:cost]; |
| } |
| |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| doneBlock(diskImage, SDImageCacheTypeDisk); |
| }); |
| } |
| }); |
| |
| return operation; |
| } |
| |
| - (void)removeImageForKey:(NSString *)key { |
| [self removeImageForKey:key withCompletion:nil]; |
| } |
| |
| - (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion { |
| [self removeImageForKey:key fromDisk:YES withCompletion:completion]; |
| } |
| |
| - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk { |
| [self removeImageForKey:key fromDisk:fromDisk withCompletion:nil]; |
| } |
| |
| - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion { |
| |
| if (key == nil) { |
| return; |
| } |
| |
| [self.memCache removeObjectForKey:key]; |
| |
| if (fromDisk) { |
| dispatch_async(self.ioQueue, ^{ |
| [_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil]; |
| |
| if (completion) { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| completion(); |
| }); |
| } |
| }); |
| } else if (completion){ |
| completion(); |
| } |
| |
| } |
| |
| - (void)setMaxMemoryCost:(NSUInteger)maxMemoryCost { |
| self.memCache.totalCostLimit = maxMemoryCost; |
| } |
| |
| - (NSUInteger)maxMemoryCost { |
| return self.memCache.totalCostLimit; |
| } |
| |
| - (void)clearMemory { |
| [self.memCache removeAllObjects]; |
| } |
| |
| - (void)clearDisk { |
| [self clearDiskOnCompletion:nil]; |
| } |
| |
| - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion |
| { |
| dispatch_async(self.ioQueue, ^{ |
| [_fileManager removeItemAtPath:self.diskCachePath error:nil]; |
| [_fileManager createDirectoryAtPath:self.diskCachePath |
| withIntermediateDirectories:YES |
| attributes:nil |
| error:NULL]; |
| |
| if (completion) { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| completion(); |
| }); |
| } |
| }); |
| } |
| |
| - (void)cleanDisk { |
| [self cleanDiskWithCompletionBlock:nil]; |
| } |
| |
| - (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock { |
| dispatch_async(self.ioQueue, ^{ |
| NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; |
| NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey]; |
| |
| // This enumerator prefetches useful properties for our cache files. |
| NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL |
| includingPropertiesForKeys:resourceKeys |
| options:NSDirectoryEnumerationSkipsHiddenFiles |
| errorHandler:NULL]; |
| |
| NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge]; |
| NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary]; |
| NSUInteger currentCacheSize = 0; |
| |
| // Enumerate all of the files in the cache directory. This loop has two purposes: |
| // |
| // 1. Removing files that are older than the expiration date. |
| // 2. Storing file attributes for the size-based cleanup pass. |
| NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init]; |
| for (NSURL *fileURL in fileEnumerator) { |
| NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL]; |
| |
| // Skip directories. |
| if ([resourceValues[NSURLIsDirectoryKey] boolValue]) { |
| continue; |
| } |
| |
| // Remove files that are older than the expiration date; |
| NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey]; |
| if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) { |
| [urlsToDelete addObject:fileURL]; |
| continue; |
| } |
| |
| // Store a reference to this file and account for its total size. |
| NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; |
| currentCacheSize += [totalAllocatedSize unsignedIntegerValue]; |
| [cacheFiles setObject:resourceValues forKey:fileURL]; |
| } |
| |
| for (NSURL *fileURL in urlsToDelete) { |
| [_fileManager removeItemAtURL:fileURL error:nil]; |
| } |
| |
| // If our remaining disk cache exceeds a configured maximum size, perform a second |
| // size-based cleanup pass. We delete the oldest files first. |
| if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) { |
| // Target half of our maximum cache size for this cleanup pass. |
| const NSUInteger desiredCacheSize = self.maxCacheSize / 2; |
| |
| // Sort the remaining cache files by their last modification time (oldest first). |
| NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent |
| usingComparator:^NSComparisonResult(id obj1, id obj2) { |
| return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]]; |
| }]; |
| |
| // Delete files until we fall below our desired cache size. |
| for (NSURL *fileURL in sortedFiles) { |
| if ([_fileManager removeItemAtURL:fileURL error:nil]) { |
| NSDictionary *resourceValues = cacheFiles[fileURL]; |
| NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; |
| currentCacheSize -= [totalAllocatedSize unsignedIntegerValue]; |
| |
| if (currentCacheSize < desiredCacheSize) { |
| break; |
| } |
| } |
| } |
| } |
| if (completionBlock) { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| completionBlock(); |
| }); |
| } |
| }); |
| } |
| |
| - (void)backgroundCleanDisk { |
| UIApplication *application = [UIApplication sharedApplication]; |
| __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{ |
| // Clean up any unfinished task business by marking where you |
| // stopped or ending the task outright. |
| [application endBackgroundTask:bgTask]; |
| bgTask = UIBackgroundTaskInvalid; |
| }]; |
| |
| // Start the long-running task and return immediately. |
| [self cleanDiskWithCompletionBlock:^{ |
| [application endBackgroundTask:bgTask]; |
| bgTask = UIBackgroundTaskInvalid; |
| }]; |
| } |
| |
| - (NSUInteger)getSize { |
| __block NSUInteger size = 0; |
| dispatch_sync(self.ioQueue, ^{ |
| NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath]; |
| for (NSString *fileName in fileEnumerator) { |
| NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName]; |
| NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil]; |
| size += [attrs fileSize]; |
| } |
| }); |
| return size; |
| } |
| |
| - (NSUInteger)getDiskCount { |
| __block NSUInteger count = 0; |
| dispatch_sync(self.ioQueue, ^{ |
| NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath]; |
| count = [[fileEnumerator allObjects] count]; |
| }); |
| return count; |
| } |
| |
| - (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock { |
| NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; |
| |
| dispatch_async(self.ioQueue, ^{ |
| NSUInteger fileCount = 0; |
| NSUInteger totalSize = 0; |
| |
| NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL |
| includingPropertiesForKeys:@[NSFileSize] |
| options:NSDirectoryEnumerationSkipsHiddenFiles |
| errorHandler:NULL]; |
| |
| for (NSURL *fileURL in fileEnumerator) { |
| NSNumber *fileSize; |
| [fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL]; |
| totalSize += [fileSize unsignedIntegerValue]; |
| fileCount += 1; |
| } |
| |
| if (completionBlock) { |
| dispatch_async(dispatch_get_main_queue(), ^{ |
| completionBlock(fileCount, totalSize); |
| }); |
| } |
| }); |
| } |
| |
| @end |