在 SDImageCache.h 中你可以看到关于 SDImageCache 的描述:
SDImageCache maintains a memory cache and an optional disk cache.
SDImageCache包括内存缓存和磁盘缓存,内存缓存使用的是继承自 NSCache 的AutoPurgeCache,而磁盘缓存就是基于文件的读写。
先查看SDImageCache的接口,看下都包括哪些功能,然后一一讲解代码。
存储的功能:
1 2 3 4
| - (void)storeImage:(UIImage *)image forKey:(NSString *)key; - (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk; - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk; - (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key;
|
这四个方法的前两个直接调用的第三个,所以我们从第三个方法入手。
看代码:
1 2 3 4 5
| if (self.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(image); [self.memCache setObject:image forKey:key cost:cost]; }
|
如果内存缓存可用,就将图片通过 NSCache 的接口 - (void)setObject: forKey: cost: ;存入。计算 cost 的方法是:
1 2 3
| FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) { return image.size.height * image.size.width * image.scale * image.scale; }
|
也就是一张图片的像素数量。
如果需要存入磁盘,一般情况下我们是将 imageData 直接存入的,但是如果 recalculate 的值是 YES ,或者没有 imageData,那我们就需要将 image 转成 NSData 存入磁盘。具体的实现是判断这个 image 有没有透明通道或者它的前八个字节是不是规定的 PNG 那固定的八个字节,如果是则就调用 UIImagePNGRepresentation 方法转成 NSData ,如果不是那就调用 UIImageJPEGRepresentation 这个方法。有了 data 之后,就要调用那四个存储方法的第四个 storeImageDataToDisk 。
通过 key 和 _diskCachePath 得到缓存文件的具体路径,在使用 NSFileManager 中 - (BOOL)createFileAtPath: contents: attributes: ; 方法,将数据写入磁盘中。
1 2 3 4
| if (self.shouldDisableiCloud) { [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil]; }
|
这段代码是避免该文件被 iCloud 备份。
这些读写操作都放到了SDImageCache的一个串行队列中ioQueue。我觉得是因为_fileManager是自己创建的:
1 2 3
| dispatch_sync(_ioQueue, ^{ _fileManager = [NSFileManager new]; });
|
是为了保障它的线程安全,在 SDImageCache 这个类的所有文件读写操作,都会放到 ioQueue 这个队列执行。而[NSFileManager defaultManager]是系统提供,本身就是线程安全的。
查询的功能:
1 2 3
| - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock; - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key; - (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
|
第一个查询方法,在讲 SDWebImageManager 时已经讲过了。
第二个方法,就是调用的 NSCache 中的- (nullable ObjectType)objectForKey:的方法。
第三个方法中,会先到内存缓存去查找,如果没有命中,则去磁盘缓存中查找,大概就是通过 key 获取具体的路径找到对应的文件取出 NSData ,在经过一些处理转成 image 返回。
删除的功能:
1 2 3 4
| - (void)removeImageForKey:(NSString *)key; - (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion; - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk; - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
|
前三个方法都是调用的第四个,所以我们看第四个方法就好了。
如果有内存缓存则调用NSCache中的- (void)removeObjectForKey:,如果 fromDisk 为 YES,则调用NSFileManager的- (BOOL)removeItemAtPath: error:方法,删除指定缓存文件的路径即可。
清除的功能:
1 2 3
| - (void)clearMemory; - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion; - (void)clearDisk;
|
第一个方法直接调用NSCache的- (void)removeAllObjects;。第二个方法,直接调用了NSFileManager的- (BOOL)removeItemAtPath: error:删除指定缓存目录的路径即可。第三个方法调用的第二个方法。
清理的功能:
1 2
| - (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock; - (void)cleanDisk;
|
清理缓存就是清理掉一些过期的文件和超最大缓存大小限制的文件。
看第一个方法,首先获取磁盘缓存的路径 URL。然后通过以下代码获取所有缓存文件的一些属性:
1 2 3 4 5 6 7
| NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey]; NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL includingPropertiesForKeys:resourceKeys options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL];
|
这些属性分别是,是否是目录,文件的修改日期和文件大小。NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];这一句则是获取缓存过期的日期。
然后 for-in 遍历 fileEnumerator :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init]; for (NSURL *fileURL in fileEnumerator) { NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL]; if ([resourceValues[NSURLIsDirectoryKey] boolValue]) { continue; } NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey]; if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) { [urlsToDelete addObject:fileURL]; continue; } NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; currentCacheSize += [totalAllocatedSize unsignedIntegerValue]; [cacheFiles setObject:resourceValues forKey:fileURL]; }
|
获取文件路径的属性字典,如果是目录则跳过,比较修改日期和过期日期哪个更晚一些,如果是过期日期则说明该文件过期,放入urlsToDelete数组中。将文件大小累加到currentCacheSize上,并将不是过期的这些缓存文件记录到 cacheFiles 中,key 是文件的 URL ,value 是对应的属性字典。
之后,遍历urlsToDelete数组删除这些过期文件:
1 2 3
| for (NSURL *fileURL in urlsToDelete) { [_fileManager removeItemAtURL:fileURL error:nil]; }
|
然后,判断没有过期的这些文件的总大小有没有超过最大的缓存大小 self.maxCacheSize 。
如果有的话,将 cacheFiles 里的 value 按照文件的修改日期进行排序,返回一个排好序的数组。取 self.maxCacheSize 大小的一半,作为清理缓存的界限const NSUInteger desiredCacheSize = self.maxCacheSize / 2;。遍历排序后的数组,一个个文件删除,删除一个就从之前的总缓存文件大小的值减去删除后的文件大小,再比较有没有小于清理缓存的界限值 desiredCacheSize。如果小于了,则跳出循环。最后在主线程回调 completionBlock(); 。这样就达到了清理磁盘缓存的目的。
计算缓存大小:
1 2 3
| - (NSUInteger)getSize; - (NSUInteger)getDiskCount; - (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;
|
第一个方法就是遍历缓存目录的所有文件,获取这些文件路径,通过 [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil] 获得一个字典在通过 fileSize 方法获取文件大小,累加起来就是缓存的大小。
第二个和第三个方法都是获取指定缓存路径的 NSDirectoryEnumerator 遍历取对应的值,和上面相差不大,不在赘述。
查询缓存是否存在:
1 2
| - (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock; - (BOOL)diskImageExistsWithKey:(NSString *)key;
|
这些方法的实现基本就是调用
1
| exists = [[NSFileManager defaultManager] fileExistsAtPath:[self defaultCachePathForKey:key]];
|
这个方法,不在赘述。
最后说下 clearMemory ,cleanDisk ,backgroundCleanDisk 的调用时机,在 - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory{} 这个初始化方法中,注册了三个通知分别是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| [[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];
|
报内存警告时调用 clearMemory 清除内存缓存,程序即将终止的时候调用 cleanDisk 清理过期或超大小限制的磁盘缓存,而程序进入后台的时候,调用 backgroundCleanDisk ,在后台执行 cleanDiskWithCompletionBlock 清理任务。
至此,SDImageCache 的大部分方法就讲解完了。
相关阅读:
SDWebImage 源码阅读记录(一)
SDWebImageManager 源码阅读记录(二)
SDWebImageDownloader 源码阅读记录(三)
SDWebImageDownloaderOperation 源码阅读记录(四)
SDWebImageCache 源码阅读记录(五)
SDWebImage相关类 源码阅读记录(六)