寫(xiě)在前面
緩存模塊的功能最主要的就是"存"和"取","取"(查找)緩存已經(jīng)在SDWebImage主線梳理(一)和SDWebImage主線梳理(二)里跟隨主線流程介紹過(guò),本篇不再贅述。
本篇主要介紹緩存模塊的"存"。"存"主要分成兩條線,一條線是沒(méi)有緩存需要網(wǎng)絡(luò)請(qǐng)求,接收到圖片后要保存到緩存;另一條線是有緩存不需要網(wǎng)絡(luò)請(qǐng)求,但是磁盤(pán)緩存取出后要存到內(nèi)存緩存一份。
還有內(nèi)存緩存(SDMemoryCache)和磁盤(pán)緩存(SDDiskCache)單個(gè)類的解析。
另外再介紹一下SD的緩存過(guò)期問(wèn)題,以及緩存過(guò)期的靈魂六問(wèn)。
保存緩存
沒(méi)有緩存需要網(wǎng)絡(luò)請(qǐng)求
沒(méi)有緩存或第一次使用這張圖片的時(shí)候,需要走網(wǎng)絡(luò)請(qǐng)求,然后解碼,接著保存到內(nèi)存緩存,最后保存到磁盤(pán)緩存
-
-[SDWebImageManager callCacheProcessForOperation]分成兩個(gè)方向- 查詢緩存(必定走):
-[SDImageCache queryImageForKey] - 下載圖片(等回調(diào)):等待查詢結(jié)果,如果沒(méi)緩存則正常走網(wǎng)絡(luò)請(qǐng)求
- 查詢緩存(必定走):
保存緩存的大致調(diào)用流程:
-[SDWebImageManager callDownloadProcessForOperation]-[SDWebImageDownloader requestImageWithURL]方法的 block(LoaderCompletedBlock) 實(shí)現(xiàn)走啊走,走到開(kāi)始網(wǎng)絡(luò)請(qǐng)求。請(qǐng)參考SDWebImage主線梳理(二)
網(wǎng)絡(luò)請(qǐng)求已回調(diào)
-[SDWebImageDownloaderOperation URLSession:task:didCompleteWithError:]-[SDWebImageDownloaderOperation callCompletionBlocksWithImage:imageData:error:finished:]調(diào)用LoaderCompletedBlock回調(diào)
LoaderCompletedBlock 實(shí)現(xiàn)中的最后一個(gè) else 分支調(diào)用
-[SDWebImageManager callStoreCacheProcessForOperation]-[SDImageCache storeImage:imageData:forKey:cacheType:completion:]-
-[SDImageCache storeImage:imageData:forKey:toMemory:toDisk:completion:]- 內(nèi)存緩存
if分支,默認(rèn)走內(nèi)存緩存
-
計(jì)算圖片占用的內(nèi)存空間
UIImage 的關(guān)聯(lián)屬性 sd_memoryCost,key=@selector(sd_memoryCost)
-
SDMemoryCacheCostForImage()函數(shù)
- 從 UIImage 獲取 CGImageRef
- CGImageGetBytesPerRow() * CGImageGetHeight() 獲取一幀的全部字節(jié)量
- 如果是動(dòng)圖還需要乘以幀數(shù),否則直接返回全部字節(jié)量
-
SETTER:
-[SDMemoryCache setObject:forKey:cost:]- 上來(lái)就走 -[super setObject:forKey:cost:],SDMemoryCache 繼承于 NSCache
- shouldUseWeakMemoryCache == YES 繼續(xù),否則直接返回
- 保存到 weakCache(強(qiáng)鍵-弱值)的NSMapTable中
- 磁盤(pán)緩存
if分支,判斷入?yún)?toDisk;
整個(gè)分支內(nèi)都是在 ioQueue(異步、串行)隊(duì)列中
-
處理特殊情況01:沒(méi)有data只有image;需要先確定圖片格式,即如果包含alpha通道就定義為PNG,否則定義為JPEG;
- 確定是否包含alpha通道:
-[SDImageCoderHelper CGImageContainsAlpha:]- 調(diào)用CGImageGetAlphaInfo()函數(shù)獲取alpha信息
- kCGImageAlphaNone、kCGImageAlphaNoneSkipFirst、kCGImageAlphaNoneSkipLast,只要是alpha信息等于其中一個(gè)就代表不包含alpha通道
- 確定是否包含alpha通道:
處理特殊情況02:將image轉(zhuǎn)碼為data;
-[SDImageCodersManager encodedDataWithImage:format:options:]-
-[SDImageCache _storeImageDataToDisk:forKey:]- -[SDDiskCache setData:forKey:]
fileManager 判斷該路徑下是否存在文件 self.diskCachePath,不存在就創(chuàng)建一個(gè)文件夾
-
-[SDDiskCache cachePathForKey:],一頓調(diào)整key和path,得到新的path
- -[SDDiskCache cachePathForKey:inPath:],在這里把self.diskCachePath傳進(jìn)去一起折騰
- SDDiskCacheFileNameForKey(key),專門(mén)處理key,看起來(lái)像是搞成MD5的樣子,然后返回處理完的key
- 把處理成“MD5”的key拼接在self.diskCachePath后面就OK了
- -[SDDiskCache cachePathForKey:inPath:],在這里把self.diskCachePath傳進(jìn)去一起折騰
path 轉(zhuǎn)為 NSURL
data 保存到 URL 路徑下
- -[SDDiskCache setData:forKey:]
- 內(nèi)存緩存
有緩存不需要網(wǎng)絡(luò)請(qǐng)求
有緩存的情況時(shí),先查詢,然后返回緩存,使用圖片
-[SDWebImageManager callCacheProcessForOperation]-[SDImageCache queryImageForKey]-
-[SDImageCache queryCacheOperationForKey]- 調(diào)用自己實(shí)現(xiàn)的 queryDiskBlock
-[SDImageCache diskImageDataBySearchingAllPathsForKey:], 拿出磁盤(pán)緩存;-
來(lái)到?jīng)]內(nèi)存緩存 && 有磁盤(pán)緩存的分支;
-[SDImageCache diskImageForKey:data:options:context:],返回解碼后的UIImage-
SDImageCacheDecodeImageData(); SDImageCacheDefine.m 唯一的函數(shù), 真真是在解碼
-
計(jì)算 image 的 cost, 把 image 保存到 memoryCache 中
-
異步調(diào)用 doneBlock(diskImage, diskData, cacheType)
-
-[SDImageCache queryCacheOperationForKey...]的 doneBlock - 實(shí)現(xiàn)在
-[SDWebImageManager callCacheProcessForOperation...]的實(shí)現(xiàn)中
-
- 調(diào)用自己實(shí)現(xiàn)的 queryDiskBlock
回到 SDWebImageManager,
-[SDWebImageManager callCacheProcessForOperation...], else if 分支-
-[SDWebImageManager callCompletionBlockForOperation], 調(diào)用 completionBlock- completionBlock 是
-[SDWebImageManager loadImageWithURL...]的 block(InternalBlock2) - 實(shí)現(xiàn)在
-[UIView(WebCache) sd_internalSetImageWithURL...]
- completionBlock 是
-
回到 UIView(WebCache) InternalBlock2 的實(shí)現(xiàn)中,
-[UIView(WebCache) sd_setImage:imageData:basedOnClassOrViaCustomSetImageBlock:transition:cacheType:imageURL:]- 自己實(shí)現(xiàn)的 finalSetImageBlock ,自己調(diào)用
SDMemoryCache解析
SDMemoryCache 繼承自 NSCache
唯一公開(kāi)屬性 SDImageCacheConfig
私有屬性 NSMapTable *weakCach:strong-weak cache, 用來(lái)弱引用 UIImage
-
配置中的 shouldUseWeakMemoryCache 選項(xiàng)
- SDMemoryCache 之所以支持弱內(nèi)存緩存,是為了避免重復(fù)從磁盤(pán)加載,因?yàn)閮?nèi)存警告時(shí)會(huì)清除內(nèi)存緩存,以致于SD失去了對(duì)圖片的持有,無(wú)法進(jìn)一步操作,需要重新從磁盤(pán)加載圖片。
- 可以解決因App進(jìn)入后臺(tái)或者內(nèi)存警告時(shí)清空內(nèi)存而引起進(jìn)入前臺(tái)時(shí)的cell閃爍問(wèn)題
當(dāng)系統(tǒng)發(fā)出內(nèi)存警告通知時(shí),SDMemoryCache 只會(huì)移除內(nèi)存緩存,而留下弱緩存(weakCache)
SETTER 方法:先保存一份 UIImage 到內(nèi)存緩存(NSCache), 如果 shouldUseWeakMemoryCache 再保存一份到 weakCache
-
GETTER 方法:
- 與SETTER類似,上來(lái)走
-[super objectForKey:],然后判斷 shouldUseWeakMemoryCache,YES繼續(xù),否則直接返回對(duì)象。 - 唯一值得說(shuō)的地方是在從 weakCache 取完值后,有可能 weakCache 存的值和內(nèi)存緩存的值不一致。因此做一次同步操作,將 weakCache 的值賦值給內(nèi)存緩存。
- 與SETTER類似,上來(lái)走
內(nèi)存緩存(SDMemoryCache)只保存 UIImage, 不保存NSData。image 已解碼。
單說(shuō)儲(chǔ)存, SDMemoryCache 的內(nèi)存緩存完全由其父類 NSCache 完成; 弱緩存由 strong-weak 屬性 weakCache 完成;
再說(shuō)存取, 存只是普通的向 NSCache 或 NSMapTable 賦值; 取也是從 NSCache 和 NSMapTable 普通的取值,只不過(guò)如果是從弱緩存取值還要同步給內(nèi)存緩存。
SDDiskCache解析
SDDiskCache 繼承自 NSObject
兩個(gè)私有屬性:
1.diskCachePath, 初始化就要有
2.fileManager, 初始化時(shí)沒(méi)有則 new 一個(gè)-
-[SDDiskCache cachePathForKey:]- 入?yún)⒅挥幸粋€(gè)key(圖片的URL地址), 還有一個(gè)隱藏參數(shù)就是 diskCachePath;
- 處理 key, 就是 MD5 散列;
- 將key(MD5散列值) 拼接在 diskCachePath 后面,返回新路徑;
SETTER 方法:
1.如果 diskCachePath 文件夾不存在則創(chuàng)建一個(gè);
2.獲取新路徑 diskCachePath/key(MD5); 新路徑轉(zhuǎn)成 NSURL
3.圖片data writeToURL:
4.禁止iCloud備份GETTER 方法:
1.獲取新路徑 diskCachePath/key(MD5);
2.從新路徑恢復(fù)(初始化) NSData
3.如果data不存在,則去掉新路徑的擴(kuò)展名再試一次-
-[NSFileManager createDirectoryAtPath:withIntermediateDirectories:attributes:error:]- 這個(gè) withIntermediateDirectories 是說(shuō),目前真實(shí)路徑是 AAA/BBB/CCC, 給定路徑(想創(chuàng)建的文件夾路徑)AAA/BBB/CCC/DDD/EEE, 如果是YES則D和E的文件夾都會(huì)被創(chuàng)建,否則啥也不創(chuàng)建
當(dāng)調(diào)用
-[SDDiskCache containsDataForKey:]和-[SDDiskCache dataForKey:]兩個(gè)方法的時(shí)候,都會(huì)有一個(gè)操作就是當(dāng)把key重新組裝完之后還是找不到data,那么會(huì)嘗試將key的擴(kuò)展名去掉再試一遍。-
存取, SDDiskCache 的磁盤(pán)緩存全靠 NSFileManager 和 NSData 二者配合; NSFileManager 來(lái)確定文件夾路徑是否存在以及創(chuàng)建文件夾
-
-[NSFileManager fileExistsAtPath]-[NSFileManager createDirectoryAtPath]-[NSFileManager removeItemAtPath]
- NSData:按照路徑讀寫(xiě)即可
- 存
-[NSData writeToURL] - 取
-[NSData dataWithContentsOfFile]
- 存
-
磁盤(pán)緩存(SDDiskCache)只保存 NSData, 不保存 UIImage。data就是二進(jìn)制流,不存在解碼還是不解碼的問(wèn)題,如果非要問(wèn),就是未解碼。
removeExpiredData 詳情見(jiàn)下下節(jié)
FAQ
-
Q:網(wǎng)絡(luò)請(qǐng)求完成時(shí),我們緩存的是已解碼的圖片嗎? -
A:是解碼的圖片
-
Q:既然緩存的是已解碼的圖片,那在查詢緩存時(shí)是否有解碼操作,為什么? -
A:decode image data only if in-memory cache missed,只有在沒(méi)有內(nèi)存緩存且有data的情況下才再次解碼data;也就是App下一次啟動(dòng),磁盤(pán)有緩存,內(nèi)存沒(méi)有緩存- 詳情請(qǐng)?jiān)谠创a中搜:
-[SDImageCache queryCacheOperationForKey]
- 詳情請(qǐng)?jiān)谠创a中搜:
-
Q:圖片存取的key是什么? -
A:圖片的URL地址
-
Q:儲(chǔ)存到 SDDiskCache 的緩存是什么? -
A:是 imageData; 在 URLSession 的回調(diào)方法 didReceiveData 中一點(diǎn)一點(diǎn)拼接完成的data;在 didCompleteWithError 通過(guò) block 回調(diào)回來(lái)
-
Q:儲(chǔ)存到 SDDiskCache 的 imageData 是解碼過(guò)的嗎? -
A:imageData 就是二進(jìn)制流(不存在解不解碼的問(wèn)題),而 didCompleteWithError 中說(shuō)的解碼是 imageData 解碼變成位圖保存到 UIImage 的產(chǎn)物;
緩存過(guò)期
源碼解析:
-[SDDiskCache removeExpiredData]
- (void)removeExpiredData {
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
// 確定文件最后的時(shí)間,是最后一次修改的時(shí)間還是訪問(wèn)的時(shí)間; 默認(rèn)是最后一次修改的時(shí)間
NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey; // 最后一次修改的時(shí)間
switch (self.config.diskCacheExpireType) {
case SDImageCacheConfigExpireTypeAccessDate:
cacheContentDateKey = NSURLContentAccessDateKey; // 最后一次訪問(wèn)的時(shí)間
break;
case SDImageCacheConfigExpireTypeModificationDate:
cacheContentDateKey = NSURLContentModificationDateKey;
break;
default:
break;
}
// NSURLIsDirectoryKey: 判斷文件是否是文件夾; cacheContentDateKey: 獲取文件最后一次修改的時(shí)間; NSURLTotalFileAllocatedSizeKey: 整個(gè)文件被分配的磁盤(pán)空間大小
NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
// 枚舉器(Enumerator) 預(yù)先把需要的信息都保存下來(lái)
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
// 默認(rèn)從此刻倒推7天
NSDate *expirationDate = (self.config.maxDiskAge < 0) ? nil: [NSDate dateWithTimeIntervalSinceNow:-self.config.maxDiskAge];
// 一會(huì)用來(lái)保存文件信息(resourceValues), {fileURL : resourceValues}
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
// 保存所有文件占用的磁盤(pán)空間大小
NSUInteger currentCacheSize = 0;
// 清除操作分為兩部分:1.刪除過(guò)期的緩存文件; 2.磁盤(pán)緩存大小超過(guò)限制后,刪除一部分(從最舊的文件開(kāi)始),直到剩下的緩存大小低于“限制的一半”大小
// Enumerate all of the files in the cache directory. This loop has two purposes:
//
// 1. Removing files that are older than the expiration date.
// 2. Storing file attributes for the size-based cleanup pass.
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSError *error;
// 獲取文件信息
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
// Skip directories and errors.
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// Remove files that are older than the expiration date;
NSDate *modifiedDate = resourceValues[cacheContentDateKey];
// laterDate: 會(huì)返回調(diào)用者和入?yún)烧咧凶钔淼娜掌? if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// Store a reference to this file and account for its total size.
// 獲取文件的大小, 字節(jié)數(shù)
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
// 累加
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
// 保存文件信息
cacheFiles[fileURL] = resourceValues;
}
// 逐個(gè)刪除過(guò)期的緩存文件
for (NSURL *fileURL in urlsToDelete) {
[self.fileManager removeItemAtURL:fileURL error:nil];
}
// If our remaining disk cache exceeds a configured maximum size, perform a second
// size-based cleanup pass. We delete the oldest files first.
NSUInteger maxDiskSize = self.config.maxDiskSize;
if (maxDiskSize > 0 && currentCacheSize > maxDiskSize) {
// Target half of our maximum cache size for this cleanup pass.
// 定個(gè)小目標(biāo):從最舊的文件開(kāi)始刪除,一直刪,一直刪,直到剩下的緩存總字節(jié)數(shù)小于 maxDiskSize 的一半
const NSUInteger desiredCacheSize = maxDiskSize / 2;
// Sort the remaining cache files by their last modification time or last access time (oldest first).
// 通過(guò)比較value(NSDate)之間的大小來(lái)排序key,時(shí)間最早的key排在前面
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
}];
// Delete files until we fall below our desired cache size.
for (NSURL *fileURL in sortedFiles) {
if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
// 剛才在上一個(gè)for循環(huán)中保存的文件信息,按照f(shuō)ileURL取出來(lái)
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
// 從文件信息中取出總字節(jié)數(shù)(占用磁盤(pán)空間的大小)
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
// 剛才一直在累加,現(xiàn)在刪除一個(gè)往下減一個(gè)
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
// 一直刪,一直刪,直到緩存總字節(jié)數(shù)小于目標(biāo)字節(jié)數(shù)(前面定的小目標(biāo))
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
}
?
緩存過(guò)期之靈魂六問(wèn)
-
Q1:緩存過(guò)期后,如何在代碼中編寫(xiě)清除功能? -
A1:參照源碼解析,分成兩部分:一是緩存文件有過(guò)期的需要進(jìn)行清除,一是緩存文件占用磁盤(pán)空間超過(guò)大小限制需要進(jìn)行清除
-
Q2:怎么確定這個(gè)緩存已過(guò)期? -
A2:過(guò)期時(shí)間檢測(cè)不用我們費(fèi)心,因?yàn)槲募约簳?huì)記錄自己的最后一次時(shí)間(最后一次訪問(wèn)時(shí)間和最后一次修改時(shí)間),通過(guò) NSDirectoryEnumerator 就可以獲取
-
Q3:怎么知道緩存文件是否超過(guò)限制大小? -
A3:文件占用的磁盤(pán)空間大小也不用我們費(fèi)心,同樣可以通過(guò) NSDirectoryEnumerator 獲取
-
Q4:什么時(shí)機(jī)清除? -
A4:在 SDImageCache 中注冊(cè)了系統(tǒng)進(jìn)入后臺(tái)的通知,當(dāng)App進(jìn)入后臺(tái)時(shí),會(huì)開(kāi)啟一個(gè)后臺(tái)任務(wù),調(diào)用 -[SDImageCache deleteOldFilesWithCompletionBlock:] -> -[SDDiskCache removeExpiredData] 進(jìn)行一次清除。目前看是能持續(xù)50s。
-
Q5:多久清除一次? -
A5:每次進(jìn)入后臺(tái)都會(huì)調(diào)用清除方法,超過(guò)了限制(時(shí)間或者空間大小)即進(jìn)行相應(yīng)的清除工作。
-
Q6:按什么順序清除? -
A6:過(guò)期的緩存不看順序,只要是過(guò)期的就毫不留情的刪除; 但是當(dāng)緩存占用磁盤(pán)空間大小超過(guò)限制的時(shí)候,我們開(kāi)始不斷的刪除緩存文件中最舊的,最后刪到還剩 maxDiskSize 的一半大小的時(shí)候?yàn)橹埂?/li>