SDImageCache.m 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814
  1. /*
  2. * This file is part of the SDWebImage package.
  3. * (c) Olivier Poitrey <rs@dailymotion.com>
  4. *
  5. * For the full copyright and license information, please view the LICENSE
  6. * file that was distributed with this source code.
  7. */
  8. #import "SDImageCache.h"
  9. #import <CommonCrypto/CommonDigest.h>
  10. #import "NSImage+WebCache.h"
  11. #import "UIImage+MemoryCacheCost.h"
  12. #import "SDWebImageCodersManager.h"
  13. #define SD_MAX_FILE_EXTENSION_LENGTH (NAME_MAX - CC_MD5_DIGEST_LENGTH * 2 - 1)
  14. #define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
  15. #define UNLOCK(lock) dispatch_semaphore_signal(lock);
  16. // A memory cache which auto purge the cache on memory warning and support weak cache.
  17. @interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType>
  18. @end
  19. // Private
  20. @interface SDMemoryCache <KeyType, ObjectType> ()
  21. @property (nonatomic, strong, nonnull) SDImageCacheConfig *config;
  22. @property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; // strong-weak cache
  23. @property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock; // a lock to keep the access to `weakCache` thread-safe
  24. - (instancetype)init NS_UNAVAILABLE;
  25. - (instancetype)initWithConfig:(nonnull SDImageCacheConfig *)config;
  26. @end
  27. @implementation SDMemoryCache
  28. // Current this seems no use on macOS (macOS use virtual memory and do not clear cache when memory warning). So we only override on iOS/tvOS platform.
  29. // But in the future there may be more options and features for this subclass.
  30. #if SD_UIKIT
  31. - (void)dealloc {
  32. [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
  33. self.delegate = nil;
  34. }
  35. - (instancetype)initWithConfig:(SDImageCacheConfig *)config {
  36. self = [super init];
  37. if (self) {
  38. // Use a strong-weak maptable storing the secondary cache. Follow the doc that NSCache does not copy keys
  39. // This is useful when the memory warning, the cache was purged. However, the image instance can be retained by other instance such as imageViews and alive.
  40. // At this case, we can sync weak cache back and do not need to load from disk cache
  41. self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
  42. self.weakCacheLock = dispatch_semaphore_create(1);
  43. self.config = config;
  44. [[NSNotificationCenter defaultCenter] addObserver:self
  45. selector:@selector(didReceiveMemoryWarning:)
  46. name:UIApplicationDidReceiveMemoryWarningNotification
  47. object:nil];
  48. }
  49. return self;
  50. }
  51. - (void)didReceiveMemoryWarning:(NSNotification *)notification {
  52. // Only remove cache, but keep weak cache
  53. [super removeAllObjects];
  54. }
  55. // `setObject:forKey:` just call this with 0 cost. Override this is enough
  56. - (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
  57. [super setObject:obj forKey:key cost:g];
  58. if (!self.config.shouldUseWeakMemoryCache) {
  59. return;
  60. }
  61. if (key && obj) {
  62. // Store weak cache
  63. LOCK(self.weakCacheLock);
  64. // Do the real copy of the key and only let NSMapTable manage the key's lifetime
  65. // Fixes issue #2507 https://github.com/SDWebImage/SDWebImage/issues/2507
  66. [self.weakCache setObject:obj forKey:[[key mutableCopy] copy]];
  67. UNLOCK(self.weakCacheLock);
  68. }
  69. }
  70. - (id)objectForKey:(id)key {
  71. id obj = [super objectForKey:key];
  72. if (!self.config.shouldUseWeakMemoryCache) {
  73. return obj;
  74. }
  75. if (key && !obj) {
  76. // Check weak cache
  77. LOCK(self.weakCacheLock);
  78. obj = [self.weakCache objectForKey:key];
  79. UNLOCK(self.weakCacheLock);
  80. if (obj) {
  81. // Sync cache
  82. NSUInteger cost = 0;
  83. if ([obj isKindOfClass:[UIImage class]]) {
  84. cost = [(UIImage *)obj sd_memoryCost];
  85. }
  86. [super setObject:obj forKey:key cost:cost];
  87. }
  88. }
  89. return obj;
  90. }
  91. - (void)removeObjectForKey:(id)key {
  92. [super removeObjectForKey:key];
  93. if (!self.config.shouldUseWeakMemoryCache) {
  94. return;
  95. }
  96. if (key) {
  97. // Remove weak cache
  98. LOCK(self.weakCacheLock);
  99. [self.weakCache removeObjectForKey:key];
  100. UNLOCK(self.weakCacheLock);
  101. }
  102. }
  103. - (void)removeAllObjects {
  104. [super removeAllObjects];
  105. if (!self.config.shouldUseWeakMemoryCache) {
  106. return;
  107. }
  108. // Manually remove should also remove weak cache
  109. LOCK(self.weakCacheLock);
  110. [self.weakCache removeAllObjects];
  111. UNLOCK(self.weakCacheLock);
  112. }
  113. #else
  114. - (instancetype)initWithConfig:(SDImageCacheConfig *)config {
  115. self = [super init];
  116. return self;
  117. }
  118. #endif
  119. @end
  120. @interface SDImageCache ()
  121. #pragma mark - Properties
  122. @property (strong, nonatomic, nonnull) SDMemoryCache *memCache;
  123. @property (strong, nonatomic, nonnull) NSString *diskCachePath;
  124. @property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
  125. @property (strong, nonatomic, nullable) dispatch_queue_t ioQueue;
  126. @property (strong, nonatomic, nonnull) NSFileManager *fileManager;
  127. @end
  128. @implementation SDImageCache
  129. #pragma mark - Singleton, init, dealloc
  130. + (nonnull instancetype)sharedImageCache {
  131. static dispatch_once_t once;
  132. static id instance;
  133. dispatch_once(&once, ^{
  134. instance = [self new];
  135. });
  136. return instance;
  137. }
  138. - (instancetype)init {
  139. return [self initWithNamespace:@"default"];
  140. }
  141. - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns {
  142. NSString *path = [self makeDiskCachePath:ns];
  143. return [self initWithNamespace:ns diskCacheDirectory:path];
  144. }
  145. - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
  146. diskCacheDirectory:(nonnull NSString *)directory {
  147. if ((self = [super init])) {
  148. NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
  149. // Create IO serial queue
  150. _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
  151. _config = [[SDImageCacheConfig alloc] init];
  152. // Init the memory cache
  153. _memCache = [[SDMemoryCache alloc] initWithConfig:_config];
  154. _memCache.name = fullNamespace;
  155. // Init the disk cache
  156. if (directory != nil) {
  157. _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
  158. } else {
  159. NSString *path = [self makeDiskCachePath:ns];
  160. _diskCachePath = path;
  161. }
  162. dispatch_sync(_ioQueue, ^{
  163. self.fileManager = [NSFileManager new];
  164. });
  165. #if SD_UIKIT
  166. // Subscribe to app events
  167. [[NSNotificationCenter defaultCenter] addObserver:self
  168. selector:@selector(deleteOldFiles)
  169. name:UIApplicationWillTerminateNotification
  170. object:nil];
  171. [[NSNotificationCenter defaultCenter] addObserver:self
  172. selector:@selector(backgroundDeleteOldFiles)
  173. name:UIApplicationDidEnterBackgroundNotification
  174. object:nil];
  175. #endif
  176. }
  177. return self;
  178. }
  179. - (void)dealloc {
  180. [[NSNotificationCenter defaultCenter] removeObserver:self];
  181. }
  182. #pragma mark - Cache paths
  183. - (void)addReadOnlyCachePath:(nonnull NSString *)path {
  184. if (!self.customPaths) {
  185. self.customPaths = [NSMutableArray new];
  186. }
  187. if (![self.customPaths containsObject:path]) {
  188. [self.customPaths addObject:path];
  189. }
  190. }
  191. - (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
  192. NSString *filename = [self cachedFileNameForKey:key];
  193. return [path stringByAppendingPathComponent:filename];
  194. }
  195. - (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key {
  196. return [self cachePathForKey:key inPath:self.diskCachePath];
  197. }
  198. - (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
  199. const char *str = key.UTF8String;
  200. if (str == NULL) {
  201. str = "";
  202. }
  203. unsigned char r[CC_MD5_DIGEST_LENGTH];
  204. CC_MD5(str, (CC_LONG)strlen(str), r);
  205. NSURL *keyURL = [NSURL URLWithString:key];
  206. NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
  207. // File system has file name length limit, we need to check if ext is too long, we don't add it to the filename
  208. if (ext.length > SD_MAX_FILE_EXTENSION_LENGTH) {
  209. ext = nil;
  210. }
  211. NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
  212. r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
  213. r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
  214. return filename;
  215. }
  216. - (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace {
  217. NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
  218. return [paths[0] stringByAppendingPathComponent:fullNamespace];
  219. }
  220. #pragma mark - Store Ops
  221. - (void)storeImage:(nullable UIImage *)image
  222. forKey:(nullable NSString *)key
  223. completion:(nullable SDWebImageNoParamsBlock)completionBlock {
  224. [self storeImage:image imageData:nil forKey:key toDisk:YES completion:completionBlock];
  225. }
  226. - (void)storeImage:(nullable UIImage *)image
  227. forKey:(nullable NSString *)key
  228. toDisk:(BOOL)toDisk
  229. completion:(nullable SDWebImageNoParamsBlock)completionBlock {
  230. [self storeImage:image imageData:nil forKey:key toDisk:toDisk completion:completionBlock];
  231. }
  232. - (void)storeImage:(nullable UIImage *)image
  233. imageData:(nullable NSData *)imageData
  234. forKey:(nullable NSString *)key
  235. toDisk:(BOOL)toDisk
  236. completion:(nullable SDWebImageNoParamsBlock)completionBlock {
  237. if (!image || !key) {
  238. if (completionBlock) {
  239. completionBlock();
  240. }
  241. return;
  242. }
  243. // if memory cache is enabled
  244. if (self.config.shouldCacheImagesInMemory) {
  245. NSUInteger cost = image.sd_memoryCost;
  246. [self.memCache setObject:image forKey:key cost:cost];
  247. }
  248. if (toDisk) {
  249. dispatch_async(self.ioQueue, ^{
  250. @autoreleasepool {
  251. NSData *data = imageData;
  252. if (!data && image) {
  253. // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
  254. SDImageFormat format;
  255. if (SDCGImageRefContainsAlpha(image.CGImage)) {
  256. format = SDImageFormatPNG;
  257. } else {
  258. format = SDImageFormatJPEG;
  259. }
  260. data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
  261. }
  262. [self _storeImageDataToDisk:data forKey:key];
  263. }
  264. if (completionBlock) {
  265. dispatch_async(dispatch_get_main_queue(), ^{
  266. completionBlock();
  267. });
  268. }
  269. });
  270. } else {
  271. if (completionBlock) {
  272. completionBlock();
  273. }
  274. }
  275. }
  276. - (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
  277. if (!imageData || !key) {
  278. return;
  279. }
  280. dispatch_sync(self.ioQueue, ^{
  281. [self _storeImageDataToDisk:imageData forKey:key];
  282. });
  283. }
  284. // Make sure to call form io queue by caller
  285. - (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
  286. if (!imageData || !key) {
  287. return;
  288. }
  289. if (![self.fileManager fileExistsAtPath:_diskCachePath]) {
  290. [self.fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
  291. }
  292. // get cache Path for image key
  293. NSString *cachePathForKey = [self defaultCachePathForKey:key];
  294. // transform to NSUrl
  295. NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
  296. [imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
  297. // disable iCloud backup
  298. if (self.config.shouldDisableiCloud) {
  299. [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
  300. }
  301. }
  302. #pragma mark - Query and Retrieve Ops
  303. - (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {
  304. dispatch_async(self.ioQueue, ^{
  305. BOOL exists = [self _diskImageDataExistsWithKey:key];
  306. if (completionBlock) {
  307. dispatch_async(dispatch_get_main_queue(), ^{
  308. completionBlock(exists);
  309. });
  310. }
  311. });
  312. }
  313. - (BOOL)diskImageDataExistsWithKey:(nullable NSString *)key {
  314. if (!key) {
  315. return NO;
  316. }
  317. __block BOOL exists = NO;
  318. dispatch_sync(self.ioQueue, ^{
  319. exists = [self _diskImageDataExistsWithKey:key];
  320. });
  321. return exists;
  322. }
  323. // Make sure to call form io queue by caller
  324. - (BOOL)_diskImageDataExistsWithKey:(nullable NSString *)key {
  325. if (!key) {
  326. return NO;
  327. }
  328. BOOL exists = [self.fileManager fileExistsAtPath:[self defaultCachePathForKey:key]];
  329. // fallback because of https://github.com/SDWebImage/SDWebImage/pull/976 that added the extension to the disk file name
  330. // checking the key with and without the extension
  331. if (!exists) {
  332. exists = [self.fileManager fileExistsAtPath:[self defaultCachePathForKey:key].stringByDeletingPathExtension];
  333. }
  334. return exists;
  335. }
  336. - (nullable NSData *)diskImageDataForKey:(nullable NSString *)key {
  337. if (!key) {
  338. return nil;
  339. }
  340. __block NSData *imageData = nil;
  341. dispatch_sync(self.ioQueue, ^{
  342. imageData = [self diskImageDataBySearchingAllPathsForKey:key];
  343. });
  344. return imageData;
  345. }
  346. - (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
  347. return [self.memCache objectForKey:key];
  348. }
  349. - (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key {
  350. UIImage *diskImage = [self diskImageForKey:key];
  351. if (diskImage && self.config.shouldCacheImagesInMemory) {
  352. NSUInteger cost = diskImage.sd_memoryCost;
  353. [self.memCache setObject:diskImage forKey:key cost:cost];
  354. }
  355. return diskImage;
  356. }
  357. - (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key {
  358. // First check the in-memory cache...
  359. UIImage *image = [self imageFromMemoryCacheForKey:key];
  360. if (image) {
  361. return image;
  362. }
  363. // Second check the disk cache...
  364. image = [self imageFromDiskCacheForKey:key];
  365. return image;
  366. }
  367. - (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
  368. NSString *defaultPath = [self defaultCachePathForKey:key];
  369. NSData *data = [NSData dataWithContentsOfFile:defaultPath options:self.config.diskCacheReadingOptions error:nil];
  370. if (data) {
  371. return data;
  372. }
  373. // fallback because of https://github.com/SDWebImage/SDWebImage/pull/976 that added the extension to the disk file name
  374. // checking the key with and without the extension
  375. data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
  376. if (data) {
  377. return data;
  378. }
  379. NSArray<NSString *> *customPaths = [self.customPaths copy];
  380. for (NSString *path in customPaths) {
  381. NSString *filePath = [self cachePathForKey:key inPath:path];
  382. NSData *imageData = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
  383. if (imageData) {
  384. return imageData;
  385. }
  386. // fallback because of https://github.com/SDWebImage/SDWebImage/pull/976 that added the extension to the disk file name
  387. // checking the key with and without the extension
  388. imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
  389. if (imageData) {
  390. return imageData;
  391. }
  392. }
  393. return nil;
  394. }
  395. - (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
  396. NSData *data = [self diskImageDataForKey:key];
  397. return [self diskImageForKey:key data:data];
  398. }
  399. - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data {
  400. return [self diskImageForKey:key data:data options:0];
  401. }
  402. - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options {
  403. if (data) {
  404. UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
  405. image = [self scaledImageForKey:key image:image];
  406. if (self.config.shouldDecompressImages) {
  407. BOOL shouldScaleDown = options & SDImageCacheScaleDownLargeImages;
  408. image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
  409. }
  410. return image;
  411. } else {
  412. return nil;
  413. }
  414. }
  415. - (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
  416. return SDScaledImageForKey(key, image);
  417. }
  418. - (NSOperation *)queryCacheOperationForKey:(NSString *)key done:(SDCacheQueryCompletedBlock)doneBlock {
  419. return [self queryCacheOperationForKey:key options:0 done:doneBlock];
  420. }
  421. - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
  422. if (!key) {
  423. if (doneBlock) {
  424. doneBlock(nil, nil, SDImageCacheTypeNone);
  425. }
  426. return nil;
  427. }
  428. // First check the in-memory cache...
  429. UIImage *image = [self imageFromMemoryCacheForKey:key];
  430. BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
  431. if (shouldQueryMemoryOnly) {
  432. if (doneBlock) {
  433. doneBlock(image, nil, SDImageCacheTypeMemory);
  434. }
  435. return nil;
  436. }
  437. NSOperation *operation = [NSOperation new];
  438. void(^queryDiskBlock)(void) = ^{
  439. if (operation.isCancelled) {
  440. // do not call the completion if cancelled
  441. return;
  442. }
  443. @autoreleasepool {
  444. NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
  445. UIImage *diskImage;
  446. SDImageCacheType cacheType = SDImageCacheTypeNone;
  447. if (image) {
  448. // the image is from in-memory cache
  449. diskImage = image;
  450. cacheType = SDImageCacheTypeMemory;
  451. } else if (diskData) {
  452. cacheType = SDImageCacheTypeDisk;
  453. // decode image data only if in-memory cache missed
  454. diskImage = [self diskImageForKey:key data:diskData options:options];
  455. if (diskImage && self.config.shouldCacheImagesInMemory) {
  456. NSUInteger cost = diskImage.sd_memoryCost;
  457. [self.memCache setObject:diskImage forKey:key cost:cost];
  458. }
  459. }
  460. if (doneBlock) {
  461. if (options & SDImageCacheQueryDiskSync) {
  462. doneBlock(diskImage, diskData, cacheType);
  463. } else {
  464. dispatch_async(dispatch_get_main_queue(), ^{
  465. doneBlock(diskImage, diskData, cacheType);
  466. });
  467. }
  468. }
  469. }
  470. };
  471. if (options & SDImageCacheQueryDiskSync) {
  472. queryDiskBlock();
  473. } else {
  474. dispatch_async(self.ioQueue, queryDiskBlock);
  475. }
  476. return operation;
  477. }
  478. #pragma mark - Remove Ops
  479. - (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion {
  480. [self removeImageForKey:key fromDisk:YES withCompletion:completion];
  481. }
  482. - (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion {
  483. if (key == nil) {
  484. return;
  485. }
  486. if (self.config.shouldCacheImagesInMemory) {
  487. [self.memCache removeObjectForKey:key];
  488. }
  489. if (fromDisk) {
  490. dispatch_async(self.ioQueue, ^{
  491. [self.fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
  492. if (completion) {
  493. dispatch_async(dispatch_get_main_queue(), ^{
  494. completion();
  495. });
  496. }
  497. });
  498. } else if (completion){
  499. completion();
  500. }
  501. }
  502. # pragma mark - Mem Cache settings
  503. - (void)setMaxMemoryCost:(NSUInteger)maxMemoryCost {
  504. self.memCache.totalCostLimit = maxMemoryCost;
  505. }
  506. - (NSUInteger)maxMemoryCost {
  507. return self.memCache.totalCostLimit;
  508. }
  509. - (NSUInteger)maxMemoryCountLimit {
  510. return self.memCache.countLimit;
  511. }
  512. - (void)setMaxMemoryCountLimit:(NSUInteger)maxCountLimit {
  513. self.memCache.countLimit = maxCountLimit;
  514. }
  515. #pragma mark - Cache clean Ops
  516. - (void)clearMemory {
  517. [self.memCache removeAllObjects];
  518. }
  519. - (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion {
  520. dispatch_async(self.ioQueue, ^{
  521. [self.fileManager removeItemAtPath:self.diskCachePath error:nil];
  522. [self.fileManager createDirectoryAtPath:self.diskCachePath
  523. withIntermediateDirectories:YES
  524. attributes:nil
  525. error:NULL];
  526. if (completion) {
  527. dispatch_async(dispatch_get_main_queue(), ^{
  528. completion();
  529. });
  530. }
  531. });
  532. }
  533. - (void)deleteOldFiles {
  534. [self deleteOldFilesWithCompletionBlock:nil];
  535. }
  536. - (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
  537. dispatch_async(self.ioQueue, ^{
  538. NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
  539. // Compute content date key to be used for tests
  540. NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
  541. switch (self.config.diskCacheExpireType) {
  542. case SDImageCacheConfigExpireTypeAccessDate:
  543. cacheContentDateKey = NSURLContentAccessDateKey;
  544. break;
  545. case SDImageCacheConfigExpireTypeModificationDate:
  546. cacheContentDateKey = NSURLContentModificationDateKey;
  547. break;
  548. default:
  549. break;
  550. }
  551. NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
  552. // This enumerator prefetches useful properties for our cache files.
  553. NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
  554. includingPropertiesForKeys:resourceKeys
  555. options:NSDirectoryEnumerationSkipsHiddenFiles
  556. errorHandler:NULL];
  557. NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
  558. NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
  559. NSUInteger currentCacheSize = 0;
  560. // Enumerate all of the files in the cache directory. This loop has two purposes:
  561. //
  562. // 1. Removing files that are older than the expiration date.
  563. // 2. Storing file attributes for the size-based cleanup pass.
  564. NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
  565. for (NSURL *fileURL in fileEnumerator) {
  566. NSError *error;
  567. NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
  568. // Skip directories and errors.
  569. if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
  570. continue;
  571. }
  572. // Remove files that are older than the expiration date;
  573. NSDate *modifiedDate = resourceValues[cacheContentDateKey];
  574. if ([[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
  575. [urlsToDelete addObject:fileURL];
  576. continue;
  577. }
  578. // Store a reference to this file and account for its total size.
  579. NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
  580. currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
  581. cacheFiles[fileURL] = resourceValues;
  582. }
  583. for (NSURL *fileURL in urlsToDelete) {
  584. [self.fileManager removeItemAtURL:fileURL error:nil];
  585. }
  586. // If our remaining disk cache exceeds a configured maximum size, perform a second
  587. // size-based cleanup pass. We delete the oldest files first.
  588. if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
  589. // Target half of our maximum cache size for this cleanup pass.
  590. const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
  591. // Sort the remaining cache files by their last modification time or last access time (oldest first).
  592. NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
  593. usingComparator:^NSComparisonResult(id obj1, id obj2) {
  594. return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
  595. }];
  596. // Delete files until we fall below our desired cache size.
  597. for (NSURL *fileURL in sortedFiles) {
  598. if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
  599. NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
  600. NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
  601. currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
  602. if (currentCacheSize < desiredCacheSize) {
  603. break;
  604. }
  605. }
  606. }
  607. }
  608. if (completionBlock) {
  609. dispatch_async(dispatch_get_main_queue(), ^{
  610. completionBlock();
  611. });
  612. }
  613. });
  614. }
  615. #if SD_UIKIT
  616. - (void)backgroundDeleteOldFiles {
  617. Class UIApplicationClass = NSClassFromString(@"UIApplication");
  618. if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
  619. return;
  620. }
  621. UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
  622. __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
  623. // Clean up any unfinished task business by marking where you
  624. // stopped or ending the task outright.
  625. [application endBackgroundTask:bgTask];
  626. bgTask = UIBackgroundTaskInvalid;
  627. }];
  628. // Start the long-running task and return immediately.
  629. [self deleteOldFilesWithCompletionBlock:^{
  630. [application endBackgroundTask:bgTask];
  631. bgTask = UIBackgroundTaskInvalid;
  632. }];
  633. }
  634. #endif
  635. #pragma mark - Cache Info
  636. - (NSUInteger)getSize {
  637. __block NSUInteger size = 0;
  638. dispatch_sync(self.ioQueue, ^{
  639. NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtPath:self.diskCachePath];
  640. for (NSString *fileName in fileEnumerator) {
  641. NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
  642. NSDictionary<NSString *, id> *attrs = [self.fileManager attributesOfItemAtPath:filePath error:nil];
  643. size += [attrs fileSize];
  644. }
  645. });
  646. return size;
  647. }
  648. - (NSUInteger)getDiskCount {
  649. __block NSUInteger count = 0;
  650. dispatch_sync(self.ioQueue, ^{
  651. NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtPath:self.diskCachePath];
  652. count = fileEnumerator.allObjects.count;
  653. });
  654. return count;
  655. }
  656. - (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock {
  657. NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
  658. dispatch_async(self.ioQueue, ^{
  659. NSUInteger fileCount = 0;
  660. NSUInteger totalSize = 0;
  661. NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
  662. includingPropertiesForKeys:@[NSFileSize]
  663. options:NSDirectoryEnumerationSkipsHiddenFiles
  664. errorHandler:NULL];
  665. for (NSURL *fileURL in fileEnumerator) {
  666. NSNumber *fileSize;
  667. [fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL];
  668. totalSize += fileSize.unsignedIntegerValue;
  669. fileCount += 1;
  670. }
  671. if (completionBlock) {
  672. dispatch_async(dispatch_get_main_queue(), ^{
  673. completionBlock(fileCount, totalSize);
  674. });
  675. }
  676. });
  677. }
  678. @end