Authored by Jon Parise

Support a maximum disk cache size in -cleanDisk.

Previously, -cleanDisk would only remove cache files that were older
than the configured expiration date.  This allowed the disk cache to
grow significantly if a large number of resources were cached over a
short period of time.

This change adds a second (optional) size-based cleaning pass that
removes files from the disk cache until its overall size falls below
half of the configured maximum size.  Older files are deleted first.

The size-based pass is disabled by default (maxCacheSize == 0).
@@ -38,6 +38,11 @@ typedef enum SDImageCacheType SDImageCacheType; @@ -38,6 +38,11 @@ typedef enum SDImageCacheType SDImageCacheType;
38 @property (assign, nonatomic) NSInteger maxCacheAge; 38 @property (assign, nonatomic) NSInteger maxCacheAge;
39 39
40 /** 40 /**
  41 + * The maximum size of the cache, in bytes.
  42 + */
  43 +@property (assign, nonatomic) unsigned long long maxCacheSize;
  44 +
  45 +/**
41 * Returns global shared cache instance 46 * Returns global shared cache instance
42 * 47 *
43 * @return SDImageCache global instance 48 * @return SDImageCache global instance
@@ -255,30 +255,76 @@ static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week @@ -255,30 +255,76 @@ static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
255 { 255 {
256 dispatch_async(self.ioQueue, ^ 256 dispatch_async(self.ioQueue, ^
257 { 257 {
258 - NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];  
259 - // convert NSString path to NSURL path 258 + NSFileManager *fileManager = [NSFileManager defaultManager];
260 NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; 259 NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
261 - // build an enumerator by also prefetching file properties we want to read  
262 - NSDirectoryEnumerator *fileEnumerator = [[NSFileManager defaultManager] enumeratorAtURL:diskCacheURL  
263 - includingPropertiesForKeys:@[ NSURLIsDirectoryKey, NSURLContentModificationDateKey ]  
264 - options:NSDirectoryEnumerationSkipsHiddenFiles  
265 - errorHandler:NULL]; 260 + NSArray *resourceKeys = @[ NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey ];
  261 +
  262 + // This enumerator prefetches useful properties for our cache files.
  263 + NSDirectoryEnumerator *fileEnumerator = [fileManager enumeratorAtURL:diskCacheURL
  264 + includingPropertiesForKeys:resourceKeys
  265 + options:NSDirectoryEnumerationSkipsHiddenFiles
  266 + errorHandler:NULL];
  267 +
  268 + NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
  269 + NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
  270 + unsigned long long currentCacheSize = 0;
  271 +
  272 + // Enumerate all of the files in the cache directory. This loop has two purposes:
  273 + //
  274 + // 1. Removing files that are older than the expiration date.
  275 + // 2. Storing file attributes for the size-based cleanup pass.
266 for (NSURL *fileURL in fileEnumerator) 276 for (NSURL *fileURL in fileEnumerator)
267 { 277 {
268 - // skip folder  
269 - NSNumber *isDirectory;  
270 - [fileURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:NULL];  
271 - if ([isDirectory boolValue]) 278 + NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
  279 +
  280 + // Skip directories.
  281 + if ([resourceValues[NSURLIsDirectoryKey] boolValue])
272 { 282 {
273 continue; 283 continue;
274 } 284 }
275 -  
276 - // compare file date with the max age  
277 - NSDate *fileModificationDate;  
278 - [fileURL getResourceValue:&fileModificationDate forKey:NSURLContentModificationDateKey error:NULL];  
279 - if ([[fileModificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) 285 +
  286 + // Remove files that are older than the expiration date;
  287 + NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
  288 + if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate])
280 { 289 {
281 - [[NSFileManager defaultManager] removeItemAtURL:fileURL error:nil]; 290 + [fileManager removeItemAtURL:fileURL error:nil];
  291 + continue;
  292 + }
  293 +
  294 + // Store a reference to this file and account for its total size.
  295 + NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
  296 + currentCacheSize += [totalAllocatedSize unsignedLongLongValue];
  297 + [cacheFiles setObject:resourceValues forKey:fileURL];
  298 + }
  299 +
  300 + // If our remaining disk cache exceeds a configured maximum size, perform a second
  301 + // size-based cleanup pass. We delete the oldest files first.
  302 + if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize)
  303 + {
  304 + // Target half of our maximum cache size for this cleanup pass.
  305 + const unsigned long long desiredCacheSize = self.maxCacheSize / 2;
  306 +
  307 + // Sort the remaining cache files by their last modification time (oldest first).
  308 + NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
  309 + usingComparator:^NSComparisonResult(id obj1, id obj2)
  310 + {
  311 + return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
  312 + }];
  313 +
  314 + // Delete files until we fall below our desired cache size.
  315 + for (NSURL *fileURL in sortedFiles)
  316 + {
  317 + if ([fileManager removeItemAtURL:fileURL error:nil])
  318 + {
  319 + NSDictionary *resourceValues = cacheFiles[fileURL];
  320 + NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
  321 + currentCacheSize -= [totalAllocatedSize unsignedLongLongValue];
  322 +
  323 + if (currentCacheSize < desiredCacheSize)
  324 + {
  325 + break;
  326 + }
  327 + }
282 } 328 }
283 } 329 }
284 }); 330 });