SDWebImage主線之緩存(不附源碼)

寫(xiě)在前面

  1. 緩存模塊的功能最主要的就是"存"和"取","取"(查找)緩存已經(jīng)在SDWebImage主線梳理(一)SDWebImage主線梳理(二)里跟隨主線流程介紹過(guò),本篇不再贅述。

  2. 本篇主要介紹緩存模塊的"存"。"存"主要分成兩條線,一條線是沒(méi)有緩存需要網(wǎng)絡(luò)請(qǐng)求,接收到圖片后要保存到緩存;另一條線是有緩存不需要網(wǎng)絡(luò)請(qǐng)求,但是磁盤(pán)緩存取出后要存到內(nèi)存緩存一份。

  3. 還有內(nèi)存緩存(SDMemoryCache)和磁盤(pán)緩存(SDDiskCache)單個(gè)類的解析。

  4. 另外再介紹一下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è)方向
    1. 查詢緩存(必定走):-[SDImageCache queryImageForKey]
    2. 下載圖片(等回調(diào)):等待查詢結(jié)果,如果沒(méi)緩存則正常走網(wǎng)絡(luò)請(qǐng)求

保存緩存的大致調(diào)用流程:

  1. -[SDWebImageManager callDownloadProcessForOperation]

  2. -[SDWebImageDownloader requestImageWithURL] 方法的 block(LoaderCompletedBlock) 實(shí)現(xiàn)

  3. 走啊走,走到開(kāi)始網(wǎng)絡(luò)請(qǐng)求。請(qǐng)參考SDWebImage主線梳理(二)

  4. 網(wǎng)絡(luò)請(qǐng)求已回調(diào)

  5. -[SDWebImageDownloaderOperation URLSession:task:didCompleteWithError:]

  6. -[SDWebImageDownloaderOperation callCompletionBlocksWithImage:imageData:error:finished:]

  7. 調(diào)用LoaderCompletedBlock回調(diào)

  8. LoaderCompletedBlock 實(shí)現(xiàn)中的最后一個(gè) else 分支調(diào)用 -[SDWebImageManager callStoreCacheProcessForOperation]

  9. -[SDImageCache storeImage:imageData:forKey:cacheType:completion:]

  10. -[SDImageCache storeImage:imageData:forKey:toMemory:toDisk:completion:]

    1. 內(nèi)存緩存
      1. if分支,默認(rèn)走內(nèi)存緩存

      2. 計(jì)算圖片占用的內(nèi)存空間

        1. UIImage 的關(guān)聯(lián)屬性 sd_memoryCost,key=@selector(sd_memoryCost)

        2. SDMemoryCacheCostForImage()函數(shù)

          1. 從 UIImage 獲取 CGImageRef
          2. CGImageGetBytesPerRow() * CGImageGetHeight() 獲取一幀的全部字節(jié)量
          3. 如果是動(dòng)圖還需要乘以幀數(shù),否則直接返回全部字節(jié)量
      3. SETTER:-[SDMemoryCache setObject:forKey:cost:]

        1. 上來(lái)就走 -[super setObject:forKey:cost:],SDMemoryCache 繼承于 NSCache
        2. shouldUseWeakMemoryCache == YES 繼續(xù),否則直接返回
        3. 保存到 weakCache(強(qiáng)鍵-弱值)的NSMapTable中
    2. 磁盤(pán)緩存
      1. if分支,判斷入?yún)?toDisk;

      2. 整個(gè)分支內(nèi)都是在 ioQueue(異步、串行)隊(duì)列中

      3. 處理特殊情況01:沒(méi)有data只有image;需要先確定圖片格式,即如果包含alpha通道就定義為PNG,否則定義為JPEG;

        1. 確定是否包含alpha通道:-[SDImageCoderHelper CGImageContainsAlpha:]
          1. 調(diào)用CGImageGetAlphaInfo()函數(shù)獲取alpha信息
          2. kCGImageAlphaNone、kCGImageAlphaNoneSkipFirst、kCGImageAlphaNoneSkipLast,只要是alpha信息等于其中一個(gè)就代表不包含alpha通道
      4. 處理特殊情況02:將image轉(zhuǎn)碼為data;-[SDImageCodersManager encodedDataWithImage:format:options:]

      5. -[SDImageCache _storeImageDataToDisk:forKey:]

        1. -[SDDiskCache setData:forKey:]
          1. fileManager 判斷該路徑下是否存在文件 self.diskCachePath,不存在就創(chuàng)建一個(gè)文件夾

          2. -[SDDiskCache cachePathForKey:],一頓調(diào)整key和path,得到新的path

            1. -[SDDiskCache cachePathForKey:inPath:],在這里把self.diskCachePath傳進(jìn)去一起折騰
              1. SDDiskCacheFileNameForKey(key),專門(mén)處理key,看起來(lái)像是搞成MD5的樣子,然后返回處理完的key
              2. 把處理成“MD5”的key拼接在self.diskCachePath后面就OK了
          3. path 轉(zhuǎn)為 NSURL

          4. data 保存到 URL 路徑下

有緩存不需要網(wǎng)絡(luò)請(qǐng)求

有緩存的情況時(shí),先查詢,然后返回緩存,使用圖片

  1. -[SDWebImageManager callCacheProcessForOperation]

  2. -[SDImageCache queryImageForKey]

  3. -[SDImageCache queryCacheOperationForKey]

    1. 調(diào)用自己實(shí)現(xiàn)的 queryDiskBlock
      1. -[SDImageCache diskImageDataBySearchingAllPathsForKey:], 拿出磁盤(pán)緩存;

      2. 來(lái)到?jīng)]內(nèi)存緩存 && 有磁盤(pán)緩存的分支; -[SDImageCache diskImageForKey:data:options:context:],返回解碼后的UIImage

        1. SDImageCacheDecodeImageData(); SDImageCacheDefine.m 唯一的函數(shù), 真真是在解碼
      3. 計(jì)算 image 的 cost, 把 image 保存到 memoryCache 中

      4. 異步調(diào)用 doneBlock(diskImage, diskData, cacheType)

        1. -[SDImageCache queryCacheOperationForKey...] 的 doneBlock
        2. 實(shí)現(xiàn)在 -[SDWebImageManager callCacheProcessForOperation...] 的實(shí)現(xiàn)中
  4. 回到 SDWebImageManager, -[SDWebImageManager callCacheProcessForOperation...], else if 分支

  5. -[SDWebImageManager callCompletionBlockForOperation], 調(diào)用 completionBlock

    1. completionBlock 是 -[SDWebImageManager loadImageWithURL...] 的 block(InternalBlock2)
    2. 實(shí)現(xiàn)在 -[UIView(WebCache) sd_internalSetImageWithURL...]
  6. 回到 UIView(WebCache) InternalBlock2 的實(shí)現(xiàn)中, -[UIView(WebCache) sd_setImage:imageData:basedOnClassOrViaCustomSetImageBlock:transition:cacheType:imageURL:]

    1. 自己實(shí)現(xiàn)的 finalSetImageBlock ,自己調(diào)用

SDMemoryCache解析

  1. SDMemoryCache 繼承自 NSCache

  2. 唯一公開(kāi)屬性 SDImageCacheConfig

  3. 私有屬性 NSMapTable *weakCach:strong-weak cache, 用來(lái)弱引用 UIImage

  4. 配置中的 shouldUseWeakMemoryCache 選項(xiàng)

    1. SDMemoryCache 之所以支持弱內(nèi)存緩存,是為了避免重復(fù)從磁盤(pán)加載,因?yàn)閮?nèi)存警告時(shí)會(huì)清除內(nèi)存緩存,以致于SD失去了對(duì)圖片的持有,無(wú)法進(jìn)一步操作,需要重新從磁盤(pán)加載圖片。
    2. 可以解決因App進(jìn)入后臺(tái)或者內(nèi)存警告時(shí)清空內(nèi)存而引起進(jìn)入前臺(tái)時(shí)的cell閃爍問(wèn)題
  5. 當(dāng)系統(tǒng)發(fā)出內(nèi)存警告通知時(shí),SDMemoryCache 只會(huì)移除內(nèi)存緩存,而留下弱緩存(weakCache)

  6. SETTER 方法:先保存一份 UIImage 到內(nèi)存緩存(NSCache), 如果 shouldUseWeakMemoryCache 再保存一份到 weakCache

  7. GETTER 方法:

    1. 與SETTER類似,上來(lái)走 -[super objectForKey:],然后判斷 shouldUseWeakMemoryCache,YES繼續(xù),否則直接返回對(duì)象。
    2. 唯一值得說(shuō)的地方是在從 weakCache 取完值后,有可能 weakCache 存的值和內(nèi)存緩存的值不一致。因此做一次同步操作,將 weakCache 的值賦值給內(nèi)存緩存。
  8. 內(nèi)存緩存(SDMemoryCache)只保存 UIImage, 不保存NSData。image 已解碼。

  9. 單說(shuō)儲(chǔ)存, SDMemoryCache 的內(nèi)存緩存完全由其父類 NSCache 完成; 弱緩存由 strong-weak 屬性 weakCache 完成;

  10. 再說(shuō)存取, 存只是普通的向 NSCache 或 NSMapTable 賦值; 取也是從 NSCache 和 NSMapTable 普通的取值,只不過(guò)如果是從弱緩存取值還要同步給內(nèi)存緩存。


SDDiskCache解析

  1. SDDiskCache 繼承自 NSObject

  2. 兩個(gè)私有屬性:
    1.diskCachePath, 初始化就要有
    2.fileManager, 初始化時(shí)沒(méi)有則 new 一個(gè)

  3. -[SDDiskCache cachePathForKey:]

    1. 入?yún)⒅挥幸粋€(gè)key(圖片的URL地址), 還有一個(gè)隱藏參數(shù)就是 diskCachePath;
    2. 處理 key, 就是 MD5 散列;
    3. 將key(MD5散列值) 拼接在 diskCachePath 后面,返回新路徑;
  4. SETTER 方法:
    1.如果 diskCachePath 文件夾不存在則創(chuàng)建一個(gè);
    2.獲取新路徑 diskCachePath/key(MD5); 新路徑轉(zhuǎn)成 NSURL
    3.圖片data writeToURL:
    4.禁止iCloud備份

  5. GETTER 方法:
    1.獲取新路徑 diskCachePath/key(MD5);
    2.從新路徑恢復(fù)(初始化) NSData
    3.如果data不存在,則去掉新路徑的擴(kuò)展名再試一次

  6. -[NSFileManager createDirectoryAtPath:withIntermediateDirectories:attributes:error:]

    1. 這個(gè) withIntermediateDirectories 是說(shuō),目前真實(shí)路徑是 AAA/BBB/CCC, 給定路徑(想創(chuàng)建的文件夾路徑)AAA/BBB/CCC/DDD/EEE, 如果是YES則D和E的文件夾都會(huì)被創(chuàng)建,否則啥也不創(chuàng)建
  7. 當(dāng)調(diào)用-[SDDiskCache containsDataForKey:]-[SDDiskCache dataForKey:] 兩個(gè)方法的時(shí)候,都會(huì)有一個(gè)操作就是當(dāng)把key重新組裝完之后還是找不到data,那么會(huì)嘗試將key的擴(kuò)展名去掉再試一遍。

  8. 存取, SDDiskCache 的磁盤(pán)緩存全靠 NSFileManager 和 NSData 二者配合; NSFileManager 來(lái)確定文件夾路徑是否存在以及創(chuàng)建文件夾

    1. -[NSFileManager fileExistsAtPath]
      1. -[NSFileManager createDirectoryAtPath]
      2. -[NSFileManager removeItemAtPath]
    2. NSData:按照路徑讀寫(xiě)即可
      1. -[NSData writeToURL]
      2. -[NSData dataWithContentsOfFile]
  9. 磁盤(pán)緩存(SDDiskCache)只保存 NSData, 不保存 UIImage。data就是二進(jìn)制流,不存在解碼還是不解碼的問(wèn)題,如果非要問(wèn),就是未解碼。

  10. 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: 圖片存取的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>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容