/* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageCache.h" #import "SDMemoryCache.h" #import "SDDiskCache.h" #import "NSImage+Compatibility.h" #import "SDImageCodersManager.h" #import "SDImageTransformer.h" #import "SDImageCoderHelper.h" #import "SDAnimatedImage.h" #import "UIImage+MemoryCacheCost.h" #import "UIImage+Metadata.h" @interface SDImageCache () #pragma mark - Properties @property (nonatomic, strong, nonnull) id memCache; @property (nonatomic, strong, nonnull) id diskCache; @property (nonatomic, copy, readwrite, nonnull) SDImageCacheConfig *config; @property (nonatomic, copy, readwrite, nonnull) NSString *diskCachePath; @property (nonatomic, strong, nullable) dispatch_queue_t ioQueue; @end @implementation SDImageCache #pragma mark - Singleton, init, dealloc + (nonnull instancetype)sharedImageCache { static dispatch_once_t once; static id instance; dispatch_once(&once, ^{ instance = [self new]; }); return instance; } - (instancetype)init { return [self initWithNamespace:@"default"]; } - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns { return [self initWithNamespace:ns diskCacheDirectory:nil]; } - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns diskCacheDirectory:(nullable NSString *)directory { return [self initWithNamespace:ns diskCacheDirectory:directory config:SDImageCacheConfig.defaultCacheConfig]; } - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns diskCacheDirectory:(nullable NSString *)directory config:(nullable SDImageCacheConfig *)config { if ((self = [super init])) { NSAssert(ns, @"Cache namespace should not be nil"); // Create IO serial queue _ioQueue = dispatch_queue_create("com.hackemist.SDImageCache", DISPATCH_QUEUE_SERIAL); if (!config) { config = SDImageCacheConfig.defaultCacheConfig; } _config = [config copy]; // Init the memory cache NSAssert([config.memoryCacheClass conformsToProtocol:@protocol(SDMemoryCache)], @"Custom memory cache class must conform to `SDMemoryCache` protocol"); _memCache = [[config.memoryCacheClass alloc] initWithConfig:_config]; // Init the disk cache if (directory != nil) { _diskCachePath = [directory stringByAppendingPathComponent:ns]; } else { NSString *path = [[[self userCacheDirectory] stringByAppendingPathComponent:@"com.hackemist.SDImageCache"] stringByAppendingPathComponent:ns]; _diskCachePath = path; } NSAssert([config.diskCacheClass conformsToProtocol:@protocol(SDDiskCache)], @"Custom disk cache class must conform to `SDDiskCache` protocol"); _diskCache = [[config.diskCacheClass alloc] initWithCachePath:_diskCachePath config:_config]; // Check and migrate disk cache directory if need [self migrateDiskCacheDirectory]; #if SD_UIKIT // Subscribe to app events [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; #endif #if SD_MAC [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:nil]; #endif } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - Cache paths - (nullable NSString *)cachePathForKey:(nullable NSString *)key { if (!key) { return nil; } return [self.diskCache cachePathForKey:key]; } - (nullable NSString *)userCacheDirectory { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); return paths.firstObject; } - (void)migrateDiskCacheDirectory { if ([self.diskCache isKindOfClass:[SDDiskCache class]]) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // ~/Library/Caches/com.hackemist.SDImageCache/default/ NSString *newDefaultPath = [[[self userCacheDirectory] stringByAppendingPathComponent:@"com.hackemist.SDImageCache"] stringByAppendingPathComponent:@"default"]; // ~/Library/Caches/default/com.hackemist.SDWebImageCache.default/ NSString *oldDefaultPath = [[[self userCacheDirectory] stringByAppendingPathComponent:@"default"] stringByAppendingPathComponent:@"com.hackemist.SDWebImageCache.default"]; dispatch_async(self.ioQueue, ^{ [((SDDiskCache *)self.diskCache) moveCacheDirectoryFromPath:oldDefaultPath toPath:newDefaultPath]; }); }); } } #pragma mark - Store Ops - (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key completion:(nullable SDWebImageNoParamsBlock)completionBlock { [self storeImage:image imageData:nil forKey:key toDisk:YES completion:completionBlock]; } - (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock { [self storeImage:image imageData:nil forKey:key toDisk:toDisk completion:completionBlock]; } - (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock { return [self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:toDisk completion:completionBlock]; } - (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key toMemory:(BOOL)toMemory toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock { if (!image || !key) { if (completionBlock) { completionBlock(); } return; } // if memory cache is enabled if (toMemory && self.config.shouldCacheImagesInMemory) { NSUInteger cost = image.sd_memoryCost; [self.memCache setObject:image forKey:key cost:cost]; } if (toDisk) { dispatch_async(self.ioQueue, ^{ @autoreleasepool { NSData *data = imageData; if (!data && image) { // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format SDImageFormat format; if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) { format = SDImageFormatPNG; } else { format = SDImageFormatJPEG; } data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil]; } [self _storeImageDataToDisk:data forKey:key]; } if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(); }); } }); } else { if (completionBlock) { completionBlock(); } } } - (void)storeImageToMemory:(UIImage *)image forKey:(NSString *)key { if (!image || !key) { return; } NSUInteger cost = image.sd_memoryCost; [self.memCache setObject:image forKey:key cost:cost]; } - (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key { if (!imageData || !key) { return; } dispatch_sync(self.ioQueue, ^{ [self _storeImageDataToDisk:imageData forKey:key]; }); } // Make sure to call form io queue by caller - (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key { if (!imageData || !key) { return; } [self.diskCache setData:imageData forKey:key]; } #pragma mark - Query and Retrieve Ops - (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDImageCacheCheckCompletionBlock)completionBlock { dispatch_async(self.ioQueue, ^{ BOOL exists = [self _diskImageDataExistsWithKey:key]; if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(exists); }); } }); } - (BOOL)diskImageDataExistsWithKey:(nullable NSString *)key { if (!key) { return NO; } __block BOOL exists = NO; dispatch_sync(self.ioQueue, ^{ exists = [self _diskImageDataExistsWithKey:key]; }); return exists; } // Make sure to call form io queue by caller - (BOOL)_diskImageDataExistsWithKey:(nullable NSString *)key { if (!key) { return NO; } return [self.diskCache containsDataForKey:key]; } - (nullable NSData *)diskImageDataForKey:(nullable NSString *)key { if (!key) { return nil; } __block NSData *imageData = nil; dispatch_sync(self.ioQueue, ^{ imageData = [self diskImageDataBySearchingAllPathsForKey:key]; }); return imageData; } - (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key { return [self.memCache objectForKey:key]; } - (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key { UIImage *diskImage = [self diskImageForKey:key]; if (diskImage && self.config.shouldCacheImagesInMemory) { NSUInteger cost = diskImage.sd_memoryCost; [self.memCache setObject:diskImage forKey:key cost:cost]; } return diskImage; } - (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key { // First check the in-memory cache... UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { return image; } // Second check the disk cache... image = [self imageFromDiskCacheForKey:key]; return image; } - (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key { if (!key) { return nil; } NSData *data = [self.diskCache dataForKey:key]; if (data) { return data; } // Addtional cache path for custom pre-load cache if (self.additionalCachePathBlock) { NSString *filePath = self.additionalCachePathBlock(key); if (filePath) { data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil]; } } return data; } - (nullable UIImage *)diskImageForKey:(nullable NSString *)key { NSData *data = [self diskImageDataForKey:key]; return [self diskImageForKey:key data:data]; } - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data { return [self diskImageForKey:key data:data options:0 context:nil]; } - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options context:(SDWebImageContext *)context { if (data) { UIImage *image = SDImageCacheDecodeImageData(data, key, [[self class] imageOptionsFromCacheOptions:options], context); return image; } else { return nil; } } - (nullable NSOperation *)queryCacheOperationForKey:(NSString *)key done:(SDImageCacheQueryCompletionBlock)doneBlock { return [self queryCacheOperationForKey:key options:0 done:doneBlock]; } - (nullable NSOperation *)queryCacheOperationForKey:(NSString *)key options:(SDImageCacheOptions)options done:(SDImageCacheQueryCompletionBlock)doneBlock { return [self queryCacheOperationForKey:key options:options context:nil done:doneBlock]; } - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock { if (!key) { if (doneBlock) { doneBlock(nil, nil, SDImageCacheTypeNone); } return nil; } id transformer = context[SDWebImageContextImageTransformer]; if (transformer) { // grab the transformed disk image if transformer provided NSString *transformerKey = [transformer transformerKey]; key = SDTransformedKeyForKey(key, transformerKey); } // First check the in-memory cache... UIImage *image = [self imageFromMemoryCacheForKey:key]; if ((options & SDImageCacheDecodeFirstFrameOnly) && image.sd_isAnimated) { #if SD_MAC image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp]; #else image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation]; #endif } BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData)); if (shouldQueryMemoryOnly) { if (doneBlock) { doneBlock(image, nil, SDImageCacheTypeMemory); } return nil; } // Second check the disk cache... NSOperation *operation = [NSOperation new]; // Check whether we need to synchronously query disk // 1. in-memory cache hit & memoryDataSync // 2. in-memory cache miss & diskDataSync BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) || (!image && options & SDImageCacheQueryDiskDataSync)); void(^queryDiskBlock)(void) = ^{ if (operation.isCancelled) { // do not call the completion if cancelled return; } @autoreleasepool { NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key]; UIImage *diskImage; SDImageCacheType cacheType = SDImageCacheTypeNone; if (image) { // the image is from in-memory cache, but need image data diskImage = image; cacheType = SDImageCacheTypeMemory; } else if (diskData) { cacheType = SDImageCacheTypeDisk; // decode image data only if in-memory cache missed diskImage = [self diskImageForKey:key data:diskData options:options context:context]; if (diskImage && self.config.shouldCacheImagesInMemory) { NSUInteger cost = diskImage.sd_memoryCost; [self.memCache setObject:diskImage forKey:key cost:cost]; } } if (doneBlock) { if (shouldQueryDiskSync) { doneBlock(diskImage, diskData, cacheType); } else { dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, diskData, cacheType); }); } } } }; // Query in ioQueue to keep IO-safe if (shouldQueryDiskSync) { dispatch_sync(self.ioQueue, queryDiskBlock); } else { dispatch_async(self.ioQueue, queryDiskBlock); } return operation; } #pragma mark - Remove Ops - (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion { [self removeImageForKey:key fromDisk:YES withCompletion:completion]; } - (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion { [self removeImageForKey:key fromMemory:YES fromDisk:fromDisk withCompletion:completion]; } - (void)removeImageForKey:(nullable NSString *)key fromMemory:(BOOL)fromMemory fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion { if (key == nil) { return; } if (fromMemory && self.config.shouldCacheImagesInMemory) { [self.memCache removeObjectForKey:key]; } if (fromDisk) { dispatch_async(self.ioQueue, ^{ [self.diskCache removeDataForKey:key]; if (completion) { dispatch_async(dispatch_get_main_queue(), ^{ completion(); }); } }); } else if (completion) { completion(); } } - (void)removeImageFromMemoryForKey:(NSString *)key { if (!key) { return; } [self.memCache removeObjectForKey:key]; } - (void)removeImageFromDiskForKey:(NSString *)key { if (!key) { return; } dispatch_sync(self.ioQueue, ^{ [self _removeImageFromDiskForKey:key]; }); } // Make sure to call form io queue by caller - (void)_removeImageFromDiskForKey:(NSString *)key { if (!key) { return; } [self.diskCache removeDataForKey:key]; } #pragma mark - Cache clean Ops - (void)clearMemory { [self.memCache removeAllObjects]; } - (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion { dispatch_async(self.ioQueue, ^{ [self.diskCache removeAllData]; if (completion) { dispatch_async(dispatch_get_main_queue(), ^{ completion(); }); } }); } - (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock { dispatch_async(self.ioQueue, ^{ [self.diskCache removeExpiredData]; if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(); }); } }); } #pragma mark - UIApplicationWillTerminateNotification #if SD_UIKIT || SD_MAC - (void)applicationWillTerminate:(NSNotification *)notification { [self deleteOldFilesWithCompletionBlock:nil]; } #endif #pragma mark - UIApplicationDidEnterBackgroundNotification #if SD_UIKIT - (void)applicationDidEnterBackground:(NSNotification *)notification { if (!self.config.shouldRemoveExpiredDataWhenEnterBackground) { return; } Class UIApplicationClass = NSClassFromString(@"UIApplication"); if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) { return; } UIApplication *application = [UIApplication performSelector:@selector(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 deleteOldFilesWithCompletionBlock:^{ [application endBackgroundTask:bgTask]; bgTask = UIBackgroundTaskInvalid; }]; } #endif #pragma mark - Cache Info - (NSUInteger)totalDiskSize { __block NSUInteger size = 0; dispatch_sync(self.ioQueue, ^{ size = [self.diskCache totalSize]; }); return size; } - (NSUInteger)totalDiskCount { __block NSUInteger count = 0; dispatch_sync(self.ioQueue, ^{ count = [self.diskCache totalCount]; }); return count; } - (void)calculateSizeWithCompletionBlock:(nullable SDImageCacheCalculateSizeBlock)completionBlock { dispatch_async(self.ioQueue, ^{ NSUInteger fileCount = [self.diskCache totalCount]; NSUInteger totalSize = [self.diskCache totalSize]; if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(fileCount, totalSize); }); } }); } #pragma mark - Helper + (SDWebImageOptions)imageOptionsFromCacheOptions:(SDImageCacheOptions)cacheOptions { SDWebImageOptions options = 0; if (cacheOptions & SDImageCacheScaleDownLargeImages) options |= SDWebImageScaleDownLargeImages; if (cacheOptions & SDImageCacheDecodeFirstFrameOnly) options |= SDWebImageDecodeFirstFrameOnly; if (cacheOptions & SDImageCachePreloadAllFrames) options |= SDWebImagePreloadAllFrames; if (cacheOptions & SDImageCacheAvoidDecodeImage) options |= SDWebImageAvoidDecodeImage; return options; } @end @implementation SDImageCache (SDImageCache) #pragma mark - SDImageCache - (id)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock { SDImageCacheOptions cacheOptions = 0; if (options & SDWebImageQueryMemoryData) cacheOptions |= SDImageCacheQueryMemoryData; if (options & SDWebImageQueryMemoryDataSync) cacheOptions |= SDImageCacheQueryMemoryDataSync; if (options & SDWebImageQueryDiskDataSync) cacheOptions |= SDImageCacheQueryDiskDataSync; if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages; if (options & SDWebImageAvoidDecodeImage) cacheOptions |= SDImageCacheAvoidDecodeImage; if (options & SDWebImageDecodeFirstFrameOnly) cacheOptions |= SDImageCacheDecodeFirstFrameOnly; if (options & SDWebImagePreloadAllFrames) cacheOptions |= SDImageCachePreloadAllFrames; return [self queryCacheOperationForKey:key options:cacheOptions context:context done:completionBlock]; } - (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock { switch (cacheType) { case SDImageCacheTypeNone: { [self storeImage:image imageData:imageData forKey:key toMemory:NO toDisk:NO completion:completionBlock]; } break; case SDImageCacheTypeMemory: { [self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:NO completion:completionBlock]; } break; case SDImageCacheTypeDisk: { [self storeImage:image imageData:imageData forKey:key toMemory:NO toDisk:YES completion:completionBlock]; } break; case SDImageCacheTypeAll: { [self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:YES completion:completionBlock]; } break; default: { if (completionBlock) { completionBlock(); } } break; } } - (void)removeImageForKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock { switch (cacheType) { case SDImageCacheTypeNone: { [self removeImageForKey:key fromMemory:NO fromDisk:NO withCompletion:completionBlock]; } break; case SDImageCacheTypeMemory: { [self removeImageForKey:key fromMemory:YES fromDisk:NO withCompletion:completionBlock]; } break; case SDImageCacheTypeDisk: { [self removeImageForKey:key fromMemory:NO fromDisk:YES withCompletion:completionBlock]; } break; case SDImageCacheTypeAll: { [self removeImageForKey:key fromMemory:YES fromDisk:YES withCompletion:completionBlock]; } break; default: { if (completionBlock) { completionBlock(); } } break; } } - (void)containsImageForKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDImageCacheContainsCompletionBlock)completionBlock { switch (cacheType) { case SDImageCacheTypeNone: { if (completionBlock) { completionBlock(SDImageCacheTypeNone); } } break; case SDImageCacheTypeMemory: { BOOL isInMemoryCache = ([self imageFromMemoryCacheForKey:key] != nil); if (completionBlock) { completionBlock(isInMemoryCache ? SDImageCacheTypeMemory : SDImageCacheTypeNone); } } break; case SDImageCacheTypeDisk: { [self diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) { if (completionBlock) { completionBlock(isInDiskCache ? SDImageCacheTypeDisk : SDImageCacheTypeNone); } }]; } break; case SDImageCacheTypeAll: { BOOL isInMemoryCache = ([self imageFromMemoryCacheForKey:key] != nil); if (isInMemoryCache) { if (completionBlock) { completionBlock(SDImageCacheTypeMemory); } return; } [self diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) { if (completionBlock) { completionBlock(isInDiskCache ? SDImageCacheTypeDisk : SDImageCacheTypeNone); } }]; } break; default: if (completionBlock) { completionBlock(SDImageCacheTypeNone); } break; } } - (void)clearWithCacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock { switch (cacheType) { case SDImageCacheTypeNone: { if (completionBlock) { completionBlock(); } } break; case SDImageCacheTypeMemory: { [self clearMemory]; if (completionBlock) { completionBlock(); } } break; case SDImageCacheTypeDisk: { [self clearDiskOnCompletion:completionBlock]; } break; case SDImageCacheTypeAll: { [self clearMemory]; [self clearDiskOnCompletion:completionBlock]; } break; default: { if (completionBlock) { completionBlock(); } } break; } } @end