前言
使用SDWebImage為我們帶來的另一個方便就是它提供圖片的緩存功能,自動將下載好的圖片緩存到本地,防止重復下載。本篇文章主要學習的就是SDWebImage的緩存功能是怎么實現的。
Cache
提供緩存相關的文件有2個,分別是SDImageCacheConfig
,SDImageCache
。
- SDImageCacheConfig:提供緩存操作的配置屬性
//是否解壓下載的圖片,默認是YES,但是會消耗掉很多內存,如果遇到內存不足的crash時,將值設為NO
@property (assign, nonatomic) BOOL shouldDecompressImages;
//允許自動上傳的iCloud
@property (assign, nonatomic) BOOL shouldDisableiCloud;
//允許緩存到內存
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
//讀取磁盤緩存的策略
@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions;
//最大緩存時間
@property (assign, nonatomic) NSInteger maxCacheAge;
//最大緩存大小
@property (assign, nonatomic) NSUInteger maxCacheSize;
- SDImageCache:實現主要的緩存邏輯,緩存分為內存緩存
memCache
和磁盤緩存fileManager
兩部分,并提供存儲,查詢,讀取,刪除相關操作。
內存緩存
內存緩存是使用NSCache
實現的,NSCache
使用上類似字典,可以用key-Value的方式存取數據。但是NSCache
底層實現和NSDictionary
不同(NSCache是線程安全的)。
@interface AutoPurgeCache : NSCache
@end
@implementation AutoPurgeCache
- (nonnull instancetype)init {
self = [super init];
if (self) {
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
return self;
}
可以看到,里面實現了一個NSCache的子類。添加了一個觀察者,當收到內存警告的時候,移除所有的緩存。
磁盤緩存
磁盤緩存使用NSFileManager
實現,通過一個串行隊列進行異步任務管理。并要求所有寫操作都必須放在這個隊列中執行
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
通過標簽檢測隊列是不是ioQueue
- (void)checkIfQueueIsIOQueue {
const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
const char *ioQueueLabel = dispatch_queue_get_label(self.ioQueue);
if (strcmp(currentQueueLabel, ioQueueLabel) != 0) {
NSLog(@"This method should be called from the ioQueue");
}
}
核心緩存方法是- (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock
通過這個方法將圖片進行內存緩存或磁盤緩存。
//檢測正確性
if (!image || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
//如果需要進行內存緩存,則將圖片直接緩存進NSCache中
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
//如果需要磁盤緩存
if (toDisk) {
//在串行隊列中異步執行緩存操作
dispatch_async(self.ioQueue, ^{
//大量對象生成釋放,使用autoreleasepool控制內存
@autoreleasepool {
NSData *data = imageData;
if (!data && image) {
// 圖片二進制數據不存在,重新生成,首先獲取圖片的格式,然后使用`UIImagePNGRepresentation`或者`UIImageJPEGRepresentation`生成圖片,之后會查看NSData的這些分類方法
data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:SDImageFormatPNG];
}
// 磁盤緩存核心方法
[self storeImageDataToDisk:data forKey:key];
}
if (completionBlock) {
// 主線程回調
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
通過- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key
方法將數據寫入進磁盤
//正確性校驗
if (!imageData || !key) {
return;
}
//驗證當前隊列是否正確
[self checkIfQueueIsIOQueue];
//檢測磁盤中是否已存在該文件,如果不存在則創建這個目錄
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
//創建文件名,并轉化為URL格式
NSString *cachePathForKey = [self defaultCachePathForKey:key];
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
//存儲圖片到該路徑
[_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
// 如果需要,上傳到iCloud
if (self.config.shouldDisableiCloud) {
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
緩存查詢
核心方法- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock
,當查詢圖片時,該操作會在內存中放置一份緩存,如果確定需要緩存到磁盤,則將磁盤緩存操作作為一個task放到串行隊列中處理。
//正確性校驗
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 檢測內存中是否已緩存該圖片
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
NSData *diskData = nil;
// isGIF單獨處理
if (image.images) {
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
// 查詢完成,是內存緩存,查詢操作不需要在io隊列中執行
if (doneBlock) {
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
return nil;
}
//內存上沒有,創建一個任務
NSOperation *operation = [NSOperation new];
//在io隊列執行
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
// 檢測任務狀態
return;
}
@autoreleasepool {
//及時釋放內存
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage = [self diskImageForKey:key];
//如果需要緩存到內存,則把讀取出來的數據拷貝一份到內存中
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
if (doneBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
//回調
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
});
//返回該任務給調度層
return operation;
移除緩存
可以通過removeImageForKey
方法移除指定的緩存。這個操作也是異步的,需要放在ioQueue中執行。
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion {
if (key == nil) {
return;
}
if (self.config.shouldCacheImagesInMemory) {
[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();
}
}
除此之外,SDWebImage還支持批量移除,可以根據配置的緩存時間,緩存大小等參數管理緩存數據的聲明周期。
使用deleteOldFilesWithCompletionBlock
刪除時間太久的數據
使用clearMemory
清除NSCache中的數據
使用clearDiskOnCompletion
清除所有磁盤上的數據,這個函數會刪除創建的目錄結構
小結
SDWebImage緩存主要分為內存緩存和磁盤緩存兩部分,內存緩存使用NSCache進行緩存,在內存占用過多時可以釋放多余內存。磁盤緩存使用NSFileManager實現,使用了一個串行隊列來保證操作的正確性,異步執行讀寫操作保證不影響主線程。提供配置項管理緩存策略讓內存緩存和磁盤緩存協調工作。防止了冗余的緩存操作。
相關文章
SDWebImage源碼閱讀筆記
SDWebImage源碼閱讀
SDWebImage 源碼閱讀筆記(二)
SDWebImage源碼閱讀筆記