圖片加載:
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
# 其實(shí)是通過 SDWebImageManager類進(jìn)行協(xié)調(diào),調(diào)用 SDImageCache與 SDWebImageDownloader來實(shí)現(xiàn)圖片的緩存查詢與網(wǎng)絡(luò)下載的。
1. SDImageCache
該類維護(hù)了一個(gè)內(nèi)存緩存與一個(gè)可選的磁盤緩存. 同時(shí), 磁盤緩存的寫操作是異步的, 所以他不會(huì)對(duì)UI造成不必要的影響.
*每次查詢圖片時(shí), 首先會(huì)根據(jù)圖片的URL對(duì)應(yīng)的key值檢測(cè)內(nèi)存中是否有對(duì)應(yīng)的圖片:
@ 如果有則直接返回;
@ 如果沒有則在ioQueue中去磁盤中查找;
其key是根據(jù)URL生成的MD5值, 找到圖片緩存在內(nèi)存中, 然后把圖片返回.
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// 首先檢查內(nèi)存緩存(查詢是同步的),如果查找到,則直接回調(diào) doneBlock 并返回
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
// 創(chuàng)建自動(dòng)釋放池,內(nèi)存及時(shí)釋放
@autoreleasepool {
// 檢查磁盤緩存(查詢是異步的),如果查找到,則將其放到內(nèi)存緩存,并調(diào)用 doneBlock 回調(diào)
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage) {
NSUInteger cost = SDCacheCostForImage(diskImage);
// 緩存至內(nèi)存(NSCache)中
[self.memCache setObject:diskImage forKey:key cost:cost];
}
// 返回主線程設(shè)置圖片
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
2. NSCache
NSCache 是蘋果官方提供的緩存類,用法與 NSMutableDictionary 的用法很相似,在 SDWebImage 和 AFNetworking 中,使用它來管理緩存。同樣是以 key-value 的形式進(jìn)行存儲(chǔ),那么 NSCache 與 NSMutableDictionary 等集合類的區(qū)別或者說優(yōu)勢(shì)又是哪些呢?
- NSCache 類結(jié)合了各種自動(dòng)刪除策略,以確保不會(huì)占用過多的系統(tǒng)內(nèi)存。如果其它應(yīng)用需要內(nèi)存時(shí),系統(tǒng)自動(dòng)執(zhí)行這些策略。當(dāng)調(diào)用這些策略時(shí),會(huì)從緩存中刪除一些對(duì)象,以最大限度減少內(nèi)存的占用
- NSCache 是線程安全的,我們可以在不同的線程中添加、刪除和查詢緩存中的對(duì)象,而不需要鎖定緩存區(qū)域
- 不像 NSMutableDictionary 對(duì)象,NSCache 對(duì)象并不會(huì)拷貝鍵(key),而是會(huì)強(qiáng)引用它
要點(diǎn)
- 在開發(fā)者自己編寫加鎖代碼的前提下, 多個(gè)線程便可以同時(shí)訪問NSCache
- NSCache對(duì)象不拷貝鍵的原因在于: 很多時(shí)候, 鍵都是由不支持拷貝操作的對(duì)象來充當(dāng)?shù)? 所以說, 在不支持拷貝操作的情況下, 該類用起來比字典更方便.
- 可以給NSCache對(duì)象設(shè)置上限, 用以限制緩存中的對(duì)象總個(gè)數(shù), 而這些尺度則定義了緩存刪減中對(duì)象的時(shí)間. 但是絕對(duì)不要把這些尺度當(dāng)成靠山, 他們僅對(duì)于NSCache起指導(dǎo)作用.
- 將NSPurgeableData與NSCache搭配使用, 可實(shí)現(xiàn)自動(dòng)清除數(shù)據(jù)的功能, 也就是說, 當(dāng)NSPurgeableData對(duì)象所占內(nèi)存為系統(tǒng)所丟棄時(shí), 該對(duì)象自身也會(huì)從緩存中移除.
- 如果緩存使用得當(dāng), 那么應(yīng)用程序的響應(yīng)速度就能提高. 只有那種(重新計(jì)算起來哼費(fèi)時(shí)的)數(shù)據(jù), 才值得放入緩存, 比如那些需要從網(wǎng)絡(luò)獲取或者從磁盤讀取的數(shù)據(jù).
- 內(nèi)存查詢是同步, 磁盤查詢是異步.
3. 磁盤
磁盤緩存的處理則是使用NSFileManager對(duì)象來實(shí)現(xiàn)的. 默認(rèn)以
com.hackemist.SDWebImageCache.default為磁盤的緩存命名空間, 程序運(yùn)行后, 可以在程序的文件夾Library/Caches/default/com.hackemist.SDWebImageCache.default下看到一些緩存文件. 另外, SDImageCache還定義了一個(gè)串行隊(duì)列, 來異存儲(chǔ)圖片.
在磁盤查詢的時(shí)候, 會(huì)在后臺(tái)將NSData轉(zhuǎn)場(chǎng)UIImage, 并完成相關(guān)的解碼工作:
- (UIImage *)diskImageForKey:(NSString *)key {
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
if (data) {
UIImage *image = [UIImage sd_imageWithData:data];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
return image;
} else {
return nil;
}
}
4. 存儲(chǔ)圖片
當(dāng)下載玩圖片后, 會(huì)先將圖片保存到NSCache中, 并把圖片像素大小作為該對(duì)象的cost值, 同時(shí)如果需要保存到硬盤, 會(huì)先判斷圖片的格式, PNG 和JPEG, 并保存對(duì)應(yīng)的NSData到緩存路徑中, 文件名為URL 的MD5值:
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
...
// 內(nèi)存緩存,將其存入 NSCache 中,同時(shí)傳入圖片的消耗值,cost 為像素值(當(dāng)內(nèi)存受限或者所有緩存對(duì)象的總代價(jià)超過了最大允許的值時(shí),緩存會(huì)移除其中的一些對(duì)象)
[self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale * image.scale];
if (toDisk) {
// 如果確定需要磁盤緩存,則將緩存操作作為一個(gè)任務(wù)放入 ioQueue 中
dispatch_async(self.ioQueue, ^{
// 構(gòu)建一個(gè) data,用來存儲(chǔ)到 disk 中,默認(rèn)值為 imageData
NSData *data = imageData;
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// 需要確定圖片是 PNG 還是 JPEG。PNG 圖片容易檢測(cè),因?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í),我們檢測(cè)其前綴,確定圖片的類型
BOOL imageIsPng = YES;
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
// 如果 image 是 PNG 格式,就是用 UIImagePNGRepresentation 將其轉(zhuǎn)化為 NSData,否則按照 JPEG 格式轉(zhuǎn)化,并且壓縮質(zhì)量為 1,即無壓縮
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ǔ)圖片(使用 fileManager)
if (data) {
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// 保存 data 到指定的路徑中
[_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
}
});
}
}
5. 清理圖片
SDImageCache 會(huì)在系統(tǒng)發(fā)出低內(nèi)存警告時(shí)釋放內(nèi)存,并且在程序進(jìn)入 UIApplicationWillTerminateNotification 時(shí),清理磁盤緩存,清理磁盤的機(jī)制是:
刪除過期的圖片,默認(rèn) 7 天過期,可以通過 maxCacheAge 修改過期天數(shù)。
如果緩存的數(shù)據(jù)大小超過設(shè)置的最大緩存 maxCacheSize,則會(huì)按照文件最后修改時(shí)間的逆序,以每次一半的遞歸來移除那些過早的文件,直到緩存的實(shí)際大小小于我們?cè)O(shè)置的最大使用空間,可以通過修改 maxCacheSize 來改變最大緩存大小。
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{ NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// 該枚舉器預(yù)先獲取緩存文件的有用的屬性
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; }
// 移除早于有效期的老文件
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL]; continue;
}
// 存儲(chǔ)文件的引用并計(jì)算所有文件的總大小,以備后用
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]];
}];
// 刪除文件,直到緩存總大小降到我們期望的大小
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(); });
}
});
}
http://itangqi.me/2016/03/23/the-notes-of-learning-sdwebimage-three/
http://itangqi.me/2016/03/24/the-notes-of-learning-sdwebimage-four/