概述
SDWebImage是iOS開發(fā)中加載圖片的庫,文件目錄的結(jié)構(gòu)按照功能可以分為4個部分:
數(shù)據(jù)緩存類:SDImageCache
數(shù)據(jù)下載類:SDWebImageDownloader、SDWebImageDownloaderOperation
工具類:SDWebImageManager、NSData+ImageContentType、SDWebImageCompat、UIImage+GIF等
UI的category:UIImageView+WebCache、UIButton+WebCache、UIImageView+HighlightedWebCache等。
本篇學(xué)習(xí)并分析一下緩存類SDImageCache。SDImageCache提供了內(nèi)存和硬盤兩種方式緩存圖片數(shù)據(jù),對于內(nèi)存緩存,直接將圖片對象UIImage存入即可,對于硬盤緩存,先將UIImage對象序列化為NSData字節(jié)流,寫入文件中。下面是SDImageCache的主要屬性:
@interface SDImageCache ()
@property (strong, nonatomic) NSCache *memCache; //內(nèi)存緩存
@property (strong, nonatomic) NSString *diskCachePath; //硬盤緩存
@property (strong, nonatomic) NSMutableArray *customPaths; //自定義硬盤緩存路徑
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue; //串行隊列,執(zhí)行存儲操作時的隊列
@end
其中NSCache對象memCache負責(zé)將UIImage對象存入內(nèi)存中,diskCachePath是存入圖片數(shù)據(jù)存入硬盤的路徑,customPaths是自定義的硬盤存儲路徑。ioQueue是串行隊列,由于存儲圖片數(shù)據(jù)是I/O操作,在ioQueue中執(zhí)行優(yōu)化可以提升主線程的流暢度。
初始化方法
-
-(id)initWithNamespace:
- (id)initWithNamespace:(NSString *)ns { NSString *path = [self makeDiskCachePath:ns]; //創(chuàng)建文件讀寫路徑 return [self initWithNamespace:ns diskCacheDirectory:path]; //初始化參數(shù) }
該方法首先調(diào)用makeDiskCachePath方法創(chuàng)建文件讀寫路徑,在文件系統(tǒng)的cache目錄中添加子目錄,然后調(diào)用initWithNamespace:diskCacheDirectory:方法初始化參數(shù)。
-
-(id)initWithNamespace:diskCacheDirectory:
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory { if ((self = [super init])) { NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns]; //文件緩存目錄名 // PNG格式前綴 kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8]; // 串行隊列 _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL); // 最大緩存期限 _maxCacheAge = kDefaultCacheMaxCacheAge; // NSCache對象,用于內(nèi)存緩存 _memCache = [[AutoPurgeCache alloc] init]; _memCache.name = fullNamespace; // 創(chuàng)建文件緩存目錄路徑 if (directory != nil) { _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace]; } else { NSString *path = [self makeDiskCachePath:ns]; _diskCachePath = path; } //需要解壓縮 _shouldDecompressImages = YES; // 需要緩存到內(nèi)存 _shouldCacheImagesInMemory = YES; // 禁用icloud _shouldDisableiCloud = YES; dispatch_sync(_ioQueue, ^{ _fileManager = [NSFileManager new]; }); ... return self; }
該方法初始化一系列參數(shù),主要是創(chuàng)建用于內(nèi)存緩存的對象memCache,文件緩存的路徑diskCachePath,設(shè)置標志位(需要解壓縮、需要緩存內(nèi)存、禁用iCloud)。初始化完成后,SDImageCache提供了存儲數(shù)據(jù)、查詢數(shù)據(jù)、刪除數(shù)據(jù)等功能。
存儲圖片數(shù)據(jù)
存儲數(shù)據(jù)調(diào)用的方法是-(void)storeImage:recalculateFromImage:imageData:forKey:toDisk:,代碼注釋如下:
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
return;
}
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost]; //UIImage存入內(nèi)存
}
if (toDisk) {
dispatch_async(self.ioQueue, ^{ //在ioQueue中執(zhí)行存儲邏輯
NSData *data = imageData;
if (image && (recalculate || !data)) {
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha; //是否包含透明度信息
// 是否包含透明度信息
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
if (imageIsPng) { // 包含透明度信息,屬于PNG格式的圖片,image轉(zhuǎn)成NSData
data = UIImagePNGRepresentation(image);
}
else { // 不包含透明度信息,屬于JPEG格式的圖片,image轉(zhuǎn)成NSData
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
}
[self storeImageDataToDisk:data forKey:key]; //存儲數(shù)據(jù)
});
}
}
首先根據(jù)shouldCacheImagesInMemory判斷是否先寫入內(nèi)存,如果為YES,則將UIImage寫入memCache中,然后在ioQueue隊列中將UIImage轉(zhuǎn)成NSData,首先判斷是否有透明度信息,通過ImageDataHasPNGPreffix方法可以判斷圖片是否是PNG格式,如果是PNG格式,則調(diào)用UIImagePNGRepresentation方法轉(zhuǎn)換,否則用UIImageJPEGRepresentation方法轉(zhuǎn)成NSData,最后調(diào)用storeImageDataToDisk:forKey:方法存儲數(shù)據(jù)。storeImageDataToDisk:forKey:方法的代碼注釋如下:
- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key {
if (!imageData) {
return;
}
//創(chuàng)建文件存儲目錄
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
//寫入圖片數(shù)據(jù)的文件路徑
NSString *cachePathForKey = [self defaultCachePathForKey:key];
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
//寫入圖片數(shù)據(jù)imageData到文件路徑中
[_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
//禁用iCloud
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
該方法負責(zé)寫入圖片數(shù)據(jù)到文件中,首先判存儲數(shù)據(jù)的目錄是否存在,如不存在,先創(chuàng)建目錄,然后獲取寫入圖片數(shù)據(jù)的文件路徑,調(diào)用createFileAtPath:contents:attributes:方法寫入圖片數(shù)據(jù)到文件中。最后調(diào)用setResourceValue:forKey:error:方法設(shè)置禁用iCloud備份。其中defaultCachePathForKey:方法的注釋如下:
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path {
NSString *filename = [self cachedFileNameForKey:key];
return [path stringByAppendingPathComponent:filename];
}
- (NSString *)defaultCachePathForKey:(NSString *)key {
return [self cachePathForKey:key inPath:self.diskCachePath];
}
- (NSString *)cachedFileNameForKey:(NSString *)key {
const char *str = [key UTF8String];
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];
return filename;
}
該方法調(diào)用cachePathForKey:inPath:方法首先得到緩存key的md5值,然后將md5值拼接在path后面返回,作為寫入圖片數(shù)據(jù)的文件路徑。
訪問圖片數(shù)據(jù)
訪問圖片數(shù)據(jù)的方法主要有以下幾個:
-
-(UIImage *)imageFromMemoryCacheForKey:方法,代碼注釋如下:
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key { return [self.memCache objectForKey:key]; //從內(nèi)存中取數(shù)據(jù) }
該方法從內(nèi)存中取圖片對象。
-
-(UIImage *)imageFromDiskCacheForKey:方法,代碼注釋如下:
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key { // 首先從內(nèi)存中取 UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { return image; } // 如果內(nèi)存中沒有數(shù)據(jù),則從文件中取 UIImage *diskImage = [self diskImageForKey:key]; if (diskImage && self.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(diskImage); [self.memCache setObject:diskImage forKey:key cost:cost]; //數(shù)據(jù)寫入內(nèi)存 } return diskImage; }
該方法首先從內(nèi)存中去圖片,如果有直接返回,如果沒有,則調(diào)用diskImageForKey:方法從文件中取,如果存在,則將圖片數(shù)據(jù)存入內(nèi)存中,最后返回。diskImageForKey:方法返回文件中寫入的圖片,代碼注釋如下:
- (UIImage *)diskImageForKey:(NSString *)key { NSData *data = [self diskImageDataBySearchingAllPathsForKey:key]; //從文件中獲取NSData數(shù)據(jù) if (data) { UIImage *image = [UIImage sd_imageWithData:data]; //轉(zhuǎn)化為UIImage image = [self scaledImageForKey:key image:image]; //轉(zhuǎn)化UIImage if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:image]; //將image轉(zhuǎn)化為位圖image } return image; } else { return nil; } }
該方法首先調(diào)用diskImageDataBySearchingAllPathsForKey:方法從文件中獲取緩存數(shù)據(jù),然后調(diào)用sd_imageWithData:方法根據(jù)data創(chuàng)建UIImage對象,然后調(diào)用decodedImageWithImage:方法將image轉(zhuǎn)成位圖格式的image。該方法代碼注釋如下:
+ (UIImage *)decodedImageWithImage:(UIImage *)image { if (image == nil) { return nil; } @autoreleasepool{ if (image.images != nil) { return image; } CGImageRef imageRef = image.CGImage; //如果有alpha信息,則不轉(zhuǎn)化,直接返回image CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef); BOOL anyAlpha = (alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast); if (anyAlpha) { return image; } //獲取圖像的相關(guān)參數(shù) CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef)); CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef); BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown || imageColorSpaceModel == kCGColorSpaceModelMonochrome || imageColorSpaceModel == kCGColorSpaceModelCMYK || imageColorSpaceModel == kCGColorSpaceModelIndexed); if (unsupportedColorSpace) { colorspaceRef = CGColorSpaceCreateDeviceRGB(); } size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); NSUInteger bytesPerPixel = 4; NSUInteger bytesPerRow = bytesPerPixel * width; NSUInteger bitsPerComponent = 8; //創(chuàng)建context CGContextRef context = CGBitmapContextCreate(NULL, width, height, bitsPerComponent, bytesPerRow, colorspaceRef, kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast); // 畫圖像 CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); //獲取位圖圖像 CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context); //創(chuàng)建UIImage對象 UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation]; if (unsupportedColorSpace) { CGColorSpaceRelease(colorspaceRef); } CGContextRelease(context); CGImageRelease(imageRefWithoutAlpha); return imageWithoutAlpha; } }
該方法首先判斷圖片是否包含alpha信息,如果包含則不轉(zhuǎn)化直接返回。如果不包含則獲取相關(guān)參數(shù)創(chuàng)建上下文context對象,然后繪制位圖,并調(diào)用CGBitmapContextCreateImage方法取出位圖對象,最后調(diào)用imageWithCGImage:scale:orientation:方法生成UIImage對象并返回。imageRefWithoutAlpha是位圖,寬和高都是圖像的真是像素值,而imageWithoutAlpha對象則包含scale和imageOrientation信息,寬和高是根據(jù)scale比例縮放的寬和高。例如一張@2x的圖片,像素寬和高是512和256,在retina屏幕上的imageWithoutAlpha對象的寬和高是256和128,scale是2。imageRefWithoutAlpha的寬和高是512和256。
之所以需要調(diào)用decodedImageWithImage:方法的原因是,通常APP加載的圖片源是PNG或者JPEG格式,當(dāng)調(diào)用圖片控件UIImageView加載圖片的時候,首先需要將其轉(zhuǎn)成位圖格式,然后渲染在屏幕上。預(yù)先在子線程中進行轉(zhuǎn)化,可以使UIImageView直接加載位圖,提升圖片加載的性能。
-
-(NSOperation *)queryDiskCacheForKey: done: 方法
該方法通過block的方式異步返回緩存的圖片,首先從內(nèi)存中獲取圖片,如果有則直接回調(diào)給上層。如果沒有,再從文件中獲取圖片,如能獲取到,則寫入內(nèi)存,同時將獲取到圖片回調(diào)給上層。代碼注釋如下:
- (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); //能夠取到,直接回調(diào)給上層 return nil; } 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); //寫入內(nèi)存 [self.memCache setObject:diskImage forKey:key cost:cost]; } dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, SDImageCacheTypeDisk); //將圖片回調(diào)給上層 }); } }); return operation; }
刪除圖片緩存數(shù)據(jù)
-
-(void)removeImageForKey:fromDisk:withCompletion:方法
該方法刪除key對應(yīng)的緩存數(shù)據(jù),分別從內(nèi)存和文件中刪除,代碼注釋如下:
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion { if (key == nil) { return; } if (self.shouldCacheImagesInMemory) { [self.memCache removeObjectForKey:key]; //從內(nèi)存中刪除圖片數(shù)據(jù) } if (fromDisk) { dispatch_async(self.ioQueue, ^{ [_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil]; //從文件中刪除圖片數(shù)據(jù) if (completion) { dispatch_async(dispatch_get_main_queue(), ^{ completion(); }); } }); } else if (completion){ completion(); } }
-
-(void)clearDiskOnCompletion:方法
該方法通過刪除目錄的方式刪除文件中所有緩存的數(shù)據(jù),并重新緩存目錄,代碼注釋如下:
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion { dispatch_async(self.ioQueue, ^{ [_fileManager removeItemAtPath:self.diskCachePath error:nil]; //刪除存放圖片數(shù)據(jù)的目錄 [_fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL]; //重新創(chuàng)建目錄 if (completion) { dispatch_async(dispatch_get_main_queue(), ^{ completion(); }); } }); }
-
-(void)cleanDiskWithCompletionBlock:
該方法根據(jù)緩存期限和緩存容量,刪除文件中的部分圖片數(shù)據(jù)。代碼注釋如下:
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock { dispatch_async(self.ioQueue, ^{ NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey]; NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL includingPropertiesForKeys:resourceKeys options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL]; //緩存期限 NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge]; NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary]; NSUInteger currentCacheSize = 0; NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init]; for (NSURL *fileURL in fileEnumerator) { NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL]; //如果是目錄,忽略 if ([resourceValues[NSURLIsDirectoryKey] boolValue]) { continue; } //將過期的緩存數(shù)據(jù)加入urlsToDelete數(shù)組中 NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey]; if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) { [urlsToDelete addObject:fileURL]; continue; } //計算沒有過期的緩存數(shù)據(jù)的大小 NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; currentCacheSize += [totalAllocatedSize unsignedIntegerValue]; [cacheFiles setObject:resourceValues forKey:fileURL]; } //刪除過期的緩存數(shù)據(jù) for (NSURL *fileURL in urlsToDelete) { [_fileManager removeItemAtURL:fileURL error:nil]; } //判斷沒有過期的緩存數(shù)據(jù)大小是否大于最大緩存容量 if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) { //如果大于,則設(shè)置緩存最大容量的1/2為預(yù)留空間大小desiredCacheSize const NSUInteger desiredCacheSize = self.maxCacheSize / 2; //根據(jù)修改日期排序 NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(id obj1, id obj2) { return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];}]; // 刪除緩存數(shù)據(jù),直到緩存數(shù)據(jù)總大小小于desiredCacheSize 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; } } } } if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(); }); } }); }
一、該方法遍歷文件目錄下的所有緩存圖片數(shù)據(jù),將緩存日期超過最大期限expirationDate的數(shù)據(jù)刪除,同時計算剩余數(shù)據(jù)的總大小。
二、判斷剩余圖片數(shù)據(jù)的總大小是否大于最大容量maxCacheSize,如果超過,則保留maxCacheSize的1/2空間desiredCacheSize,其余數(shù)據(jù)刪除,具體做法是對剩余圖片數(shù)據(jù)按照修改日期排序,逐個刪除,直到總大小小于desiredCacheSize為止。
其他方法
SDWebImage還提供了一些工具方法例如:
-(NSUInteger)getSize方法,用于同步獲取緩存在文件中圖片數(shù)據(jù)總大小。
-(NSUInteger)getDiskCount方法,用于同步獲取緩存在文件中圖片數(shù)據(jù)的個數(shù)。
-(void)calculateSizeWithCompletionBlock:方法,用于異步獲取緩存在文件中圖片數(shù)據(jù)總大小。
-
-(void)backgroundCleanDisk方法,用于在程序進入后臺的時候刪除超過期限和總大小的圖片數(shù)據(jù)。代碼注釋如下:
- (void)backgroundCleanDisk { Class UIApplicationClass = NSClassFromString(@"UIApplication"); if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) { return; } UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)]; __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{ [application endBackgroundTask:bgTask]; //如果超時,則終止task bgTask = UIBackgroundTaskInvalid; }]; //根據(jù)緩存期限和緩存容量,刪除文件中的部分圖片數(shù)據(jù) [self cleanDiskWithCompletionBlock:^{ [application endBackgroundTask:bgTask]; bgTask = UIBackgroundTaskInvalid; }]; }
該方法調(diào)用系統(tǒng)方法UIApplication的beginBackgroundTaskWithExpirationHandler方法開啟一個用于后臺執(zhí)行任務(wù)的task,并且設(shè)置超時的block。該方法允許在程序進入后臺的時候提供一段時間用于執(zhí)行app未完成的任務(wù),如果超出這段時間,則執(zhí)行ExpirationHandler的block,調(diào)用endBackgroundTask:方法終止task。cleanDiskWithCompletionBlock方法是在后臺執(zhí)行的代碼,執(zhí)行完成后會調(diào)用endBackgroundTask:方法終止task。
總結(jié)
SDImageCache同時使用內(nèi)存和文件來緩存數(shù)據(jù),設(shè)計思路值得學(xué)習(xí)和借鑒。