SDWebImage源碼詳解 - 緩存
緩存的實(shí)現(xiàn)可以顯著的減少網(wǎng)絡(luò)流量的消耗,先將下載的圖片緩存到本地,下次獲取同一張圖片的時(shí)候,可以直接在本地緩存中獲取,而不用訪問服務(wù)器重新獲取圖片,這樣不僅可以減少網(wǎng)絡(luò)流量的消耗,并且提升了用戶體驗(yàn)(圖片加載速度快)。SDWebImage的緩存由SDImageCache類來實(shí)現(xiàn),這是一個(gè)單例類,該類負(fù)責(zé)處理內(nèi)存緩存及一個(gè)可選的磁盤緩存,其中磁盤緩存的寫操作是異步的,這樣就不會(huì)對UI操作造成影響。此外還提供了若干屬性和接口來配置和操作緩存對象。
先來看看SDImageCache的頭文件內(nèi)容
//定義三個(gè)枚舉常量,以控制緩存的存儲(chǔ)選項(xiàng)
typedef NS_ENUM(NSInteger, SDImageCacheType) {
//不使用緩存策略,從網(wǎng)絡(luò)下載
SDImageCacheTypeNone,
//從磁盤中緩存中獲取圖片
SDImageCacheTypeDisk,
//從內(nèi)存中獲取圖片
SDImageCacheTypeMemory
};
//回調(diào)函數(shù)類型變量
typedef void(^SDWebImageQueryCompletedBlock)(UIImage *image, SDImageCacheType cacheType);
typedef void(^SDWebImageCheckCacheCompletionBlock)(BOOL isInCache);
typedef void(^SDWebImageCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize);
@interface SDImageCache : NSObject
//是否在緩存之前解壓圖片,此項(xiàng)操作可以提升性能,但是會(huì)消耗較多的內(nèi)存,默認(rèn)是YES。注意:如果內(nèi)存不足,可以置為NO
@property (assign, nonatomic) BOOL shouldDecompressImages;
//是否禁止iCloud備份,默認(rèn)是YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;
//是否啟用內(nèi)存緩存 默認(rèn)是YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
//內(nèi)存最大容量
@property (assign, nonatomic) NSUInteger maxMemoryCost;
//內(nèi)存對象的最大數(shù)目
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;
//磁盤緩存保留的最長時(shí)間
@property (assign, nonatomic) NSInteger maxCacheAge;
//磁盤緩存最大容量,以字節(jié)為單位
@property (assign, nonatomic) NSUInteger maxCacheSize;
//返回緩存對象的單例
+ (SDImageCache *)sharedImageCache;
//以ns為緩存空間名字初始化緩存
- (id)initWithNamespace:(NSString *)ns;
//在directory目錄下,以ns為緩存空間名字初始化緩存
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;
//返回磁盤緩存空間的路徑
-(NSString *)makeDiskCachePath:(NSString*)fullNamespace;
//添加只讀內(nèi)存空間路徑,一般用在圖片已經(jīng)下載置相應(yīng)的緩存目錄
- (void)addReadOnlyCachePath:(NSString *)path;
//以key為鍵值將圖片image存儲(chǔ)置緩存中
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
//以key為鍵值將圖片image存儲(chǔ)置緩存中,toDisk控制是否寫入磁盤緩存
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
//以key為鍵值將圖片image存儲(chǔ)置緩存中,toDisk控制是否寫入磁盤緩存,此外如果recalculate為YES或imageData有數(shù)據(jù),則將imageData存儲(chǔ)置磁盤緩存中
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
//在內(nèi)存或磁盤緩存中以key為鍵值查找圖片緩存,如果找到則執(zhí)行doneBlock回調(diào)
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
//在內(nèi)存緩存中查找圖片緩存,并返回圖片對象
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
//在硬盤緩存中查找圖片緩存,并返回圖片對象
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
//在內(nèi)存或硬盤緩存中刪除指定key緩存
- (void)removeImageForKey:(NSString *)key;
//在內(nèi)存或硬盤緩存中刪除指定key緩存,完成后執(zhí)行響應(yīng)回調(diào)
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
//在內(nèi)存或硬盤緩存中刪除指定key緩存,fromDisk控制是否刪除磁盤緩存對象
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
//在內(nèi)存或硬盤緩存中刪除指定key緩存,完成后執(zhí)行響應(yīng)回調(diào),fromDisk控制是否刪除磁盤緩存對象
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
//清除內(nèi)存緩存
- (void)clearMemory;
//清除磁盤緩存,完成后執(zhí)行回調(diào)
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)clearDisk;
//清除過期緩存,如果緩存容量超過限制,則清除部分緩存直至達(dá)到預(yù)期目標(biāo)為止
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
- (void)cleanDisk;
//返回磁盤緩存的大小
- (NSUInteger)getSize;
//返回磁盤緩存對象的數(shù)目
- (NSUInteger)getDiskCount;
//異步計(jì)算磁盤緩存所需大小
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;
//異步查看磁盤緩存中是否存在指定key的圖片,完成后執(zhí)行回調(diào)
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
- (BOOL)diskImageExistsWithKey:(NSString *)key;
//返貨指定路徑path下的key對象的緩存路徑
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;
//返回默認(rèn)路徑下key對象的緩存路徑
- (NSString *)defaultCachePathForKey:(NSString *)key;
從頭文件可以看出,SDWebImage的緩存對象提供了幾個(gè)屬性(緩存時(shí)間,緩存大小限制等)和若干函數(shù)來對緩存對象進(jìn)行操作(獲取、移除及清空圖片)。對于這么多的函數(shù),有些其實(shí)僅僅是調(diào)用而已,只需關(guān)注幾個(gè)主要函數(shù)即可,稍后我們將會(huì)針對幾個(gè)主要函數(shù)進(jìn)行講解。
</br>
SDWebImage緩存的主要實(shí)現(xiàn)分別采用了內(nèi)存緩存和磁盤緩存,內(nèi)存緩存使用NSCash對象來實(shí)現(xiàn),NSCache是一個(gè)類似于集合的容器。它存儲(chǔ)key-value對,這一點(diǎn)類似于NSDictionary類,NSCache類的詳細(xì)用法,這里不過多介紹,以后有機(jī)會(huì)專門介紹。磁盤緩存則使用NSFileManager對象來實(shí)現(xiàn)。圖片存儲(chǔ)的位置是位于app的Cache文件夾下。另外,SDImageCache還定義了一個(gè)串行隊(duì)列,來異步存儲(chǔ)圖片。接下我們就代碼的執(zhí)行流程來詳細(xì)的看一下代碼的實(shí)現(xiàn):
初始化緩存空間
//獲取內(nèi)存對象的單例
+ (SDImageCache *)sharedImageCache {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
ImageCache單例對象由函數(shù)new來初始換,而new函數(shù)默認(rèn)調(diào)用init函數(shù)。
- (id)init {
return [self initWithNamespace:@"default"];
}
- (id)initWithNamespace:(NSString *)ns {
//獲取磁盤緩存的路徑
NSString *path = [self makeDiskCachePath:ns];
return [self initWithNamespace:ns diskCacheDirectory:path];
}
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
if ((self = [super init])) {
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
//初始化PNG圖片的簽名數(shù)據(jù)
kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];
// 創(chuàng)建IO 串行對壘
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
// 初始化最大緩存時(shí)間
_maxCacheAge = kDefaultCacheMaxCacheAge;
// 初始化內(nèi)存緩存
_memCache = [[AutoPurgeCache alloc] init];
_memCache.name = fullNamespace;
// 保存磁盤緩存的目錄路徑
if (directory != nil) {
_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
} else {
NSString *path = [self makeDiskCachePath:ns];
_diskCachePath = path;
}
// 設(shè)置默認(rèn)屬性
_shouldDecompressImages = YES;
_shouldCacheImagesInMemory = YES;
_shouldDisableiCloud = YES;
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
#if TARGET_OS_IPHONE
// 注冊系統(tǒng)通知事件
[[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];
#endif
}
return self;
}
通過代碼可以看出,ImageCache對象的初始化工作,分別創(chuàng)建了內(nèi)存緩存空間和磁盤緩存空間,這里面有一個(gè)函數(shù)-(NSString *)makeDiskCachePath:(NSString*)fullNamespace
木有出現(xiàn),這個(gè)函數(shù)的主要作用就是返回app的緩存目錄。
-(NSString *)makeDiskCachePath:(NSString*)fullNamespace{
//獲取app的緩存文件夾
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
//返回緩存文件夾下以fullNamespace命名的路徑
return [paths[0] stringByAppendingPathComponent:fullNamespace];
}
保存圖片
雖然ImageCache對外提供了許多保存圖片置緩存的函數(shù),但是這么多函數(shù)都調(diào)用一個(gè)基礎(chǔ)函數(shù)- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
,具體實(shí)現(xiàn)如下:
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
return;
}
// 如果保存置內(nèi)存緩存屬性為YES,則將圖片保留在內(nèi)存緩存中
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
//如果需要保存在磁盤緩存中,則將寫人磁盤緩存的隊(duì)列放入創(chuàng)建的串行隊(duì)列ioQueue中
if (toDisk) {
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
//如果recalculate為YES或者data數(shù)據(jù)為空,但是image有數(shù)據(jù),則對iamge圖片做處理
//如果recalculate為YES并且data數(shù)據(jù)非空,則直接對data數(shù)據(jù)進(jìn)行保存
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// 需要確定圖片是PNG還是JPEG。PNG圖片容易檢測,因?yàn)橛幸粋€(gè)唯一簽名。
// PNG圖像的前8個(gè)字節(jié)總是包含以下值:137 80 78 71 13 10 26 10
// 在imageData為nil的情況下假定圖像為PNG。我們將其當(dāng)作PNG以避免丟失透明度。
//而當(dāng)有圖片數(shù)據(jù)時(shí),我們檢測其前綴,確定圖片的類型
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;
// But if we have an image data, we will look at the preffix
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
//創(chuàng)建緩存文件并存儲(chǔ)圖片
if (data) {
//創(chuàng)建保留緩存文件的上層目錄
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
//以圖片的URL做MD5轉(zhuǎn)換后的文件名創(chuàng)建緩存文件
NSString *cachePathForKey = [self defaultCachePathForKey:key];
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];
// 是否啟用iCloud云備份
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
});
}
}
查詢圖片
ImageCache對外提供了三個(gè)查詢緩存圖片的接口函數(shù)
//在內(nèi)存和磁盤緩存中查找key指定的圖片
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
//在內(nèi)存緩存中查找key指定的圖片
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
//先在內(nèi)存緩存中查找,然后在磁盤緩存中查找key指定的圖片
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
這里看一下第一個(gè)函數(shù)的實(shí)現(xiàn),其他類似
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// 先在內(nèi)存緩存中查找
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
//如果內(nèi)存緩存中沒有找到,則去磁盤緩存中去查找- (UIImage *)diskImageForKey:(NSString *)key
//在磁盤緩存中找到后,同時(shí)更新置內(nèi)存緩存中
//有回調(diào)則調(diào)用doneBlock回調(diào)
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
移除圖片
ImageCache對外提供了四個(gè)刪除緩存圖片的函數(shù),
- (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;
移除函數(shù)比較簡單,也有一個(gè)基礎(chǔ)函數(shù)- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
,這個(gè)函數(shù)比較簡單,刪除內(nèi)存緩存,刪除磁盤下的緩存文件,看一看代碼就明白什么意思,這里就不過多說明
清理緩存
清理緩存圖片的清理操作有內(nèi)存清理和磁盤緩存清理,而磁盤緩存又可以分為完全清空和部分清理。完全清空操作是直接把緩存的文件夾移除,清空操作有以下三個(gè)方法:
- (void)clearMemory;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)clearDisk
這三個(gè)函數(shù)比較簡單,也不過多介紹
接下來我們詳細(xì)介紹一下部分清理,部分清空針對磁盤緩存,根據(jù)我們設(shè)定的一些參數(shù)值來移除一些文件,這里主要有兩個(gè)指標(biāo):文件的緩存有效期及最大緩存空間大小。文件的緩存有效期可以通過maxCacheAge屬性來設(shè)置,默認(rèn)是1周的時(shí)間。如果文件的緩存時(shí)間超過這個(gè)時(shí)間值,則將其移除。而最大緩存空間大小是通過maxCacheSize屬性來設(shè)置的,如果所有緩存文件的總大小超過這一大小,則會(huì)按照文件最后修改時(shí)間的逆序排序,循環(huán)移除那些較早的文件,直到磁盤緩存的實(shí)際大小小于或等于我們設(shè)置的空間預(yù)設(shè)目標(biāo),這里設(shè)為最大緩存大小的一半。清理的操作在-cleanDiskWithCompletionBlock:方法中
,其實(shí)現(xiàn)如下:
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// 該枚舉器預(yù)先獲取緩存文件的有用的屬性,文件夾,修改時(shí)間,文件大小
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
// 枚舉緩存文件夾中所有文件,
//該迭代有兩個(gè)目的:移除比過期日期更老的文件;存儲(chǔ)文件屬性以備后面執(zhí)行基于緩存大小的清理操作
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
// 跳過文件夾
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// 將需要?jiǎng)h除的文件,加入需要?jiǎng)h除的數(shù)組urlsToDelete中
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
//存儲(chǔ)有效期內(nèi)的文件大小,留作備用
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
//刪除過期緩存
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
// 如果磁盤緩存的大小大于我們配置的最大大小,則執(zhí)行基于文件大小的清理,首先刪除最老的文件
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
// 以設(shè)置的最大緩存大小的一半作為清理目標(biāo)
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
// 按照最后修改時(shí)間來排序剩下的緩存文件
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// 循環(huán)刪除文件,直到緩存總大小降到我們期望的大小
for (NSURL *fileURL in sortedFiles) {
if ([_fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
//有回調(diào)則執(zhí)行回調(diào)
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
到這里緩存的實(shí)現(xiàn)就講解的差不多了,這里我們主要分析了SDWebImage的SDImageCache緩存類的相關(guān)操作,著重介紹了幾個(gè)主要的操作,另外SDImageCache還提供了一些其他的輔助方法如獲取緩存大小、緩存中圖片的數(shù)量、判斷緩存中是否存在某個(gè)key指定的圖片,具體的實(shí)現(xiàn)可以參照源碼,實(shí)現(xiàn)都不怎么復(fù)雜。
</br>
下一節(jié)我們主要介紹一下異步下載器的實(shí)現(xiàn)。