SDWebImage原理和緩存機制

這篇文章將主要介紹SDWebImage針對于獲取網(wǎng)絡(luò)圖片的原理和緩存機制扇调,當(dāng)然我只是用文字去介紹大體的一個流程,學(xué)無止境,需要更詳細東西的朋友自行百度深入了解茫打。

先介紹一下兩個重要的功能

1. 獨立的異步圖像下載
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

上面函數(shù)用來建立一個SDWebImageDownLoader的實例待秃。利用下載進度的回調(diào)和下載完成的回調(diào)拜秧,可以在回調(diào)完成進度條相關(guān)的操作和顯示圖片相關(guān)的操作。

2. 獨立的異步圖像緩存
(存章郁、取枉氮、刪的詳細代碼介紹附在最后)
SDImageCache *imageCache = [SDImageCache sharedImageCache];

SDImageCache類提供一個管理緩存的單例類。查找和緩存圖片時簡單理解為以URL作為key暖庄。(先查找圖片緩存聊替,如果緩存不存在該圖片,再查找沙盒培廓;查找沙盒時惹悄,以URL組合成文件路徑進行的MD5值作為key).
查找圖片:

UIImage *cacheImage = [imageCache mageFromKey:myCacheKey];

緩存圖片:

[imageCache storeImage:myImage forKey:myCacheKey];

默認情況下,圖片是被存儲到內(nèi)存緩存和磁盤緩存中的肩钠。如果僅僅是想緩存到內(nèi)存中泣港,可以用下面方法:

storeImage: forKey: toDisk: 

第三個參數(shù)傳NO即可象缀。

主要用到的對象:

  1. UIImageView(WebCache)入口封裝,實現(xiàn)讀取圖片完成后的回調(diào)爷速。

  2. SDWebImagemanager對圖片進行管理的中轉(zhuǎn)站央星,記錄那些圖片正在讀取。向下層讀取Cache(調(diào)用SDImageCache)惫东,或者向網(wǎng)絡(luò)讀取對象(調(diào)用SDWebImageDownloader)莉给。實現(xiàn)SDImageCacheSDWebImageDownLoader的回調(diào)。

  3. SDImageCache,根據(jù)URL作為key廉沮,對圖片進行存儲和讀韧嵌簟(存在內(nèi)存(以URL作為key)和存在硬盤兩種(以URL組合成文件路徑進行的MD5值作為key))。實現(xiàn)圖片和內(nèi)存清理工作滞时。

SDWebImage加載圖片的流程

  1. 入口 setImageWithURL:placeholderImage:options:會先把 placeholderImage顯示叁幢,然后 SDWebImageManager根據(jù) URL 開始處理圖片。

  2. 進入SDWebImageManager 類中downloadWithURL:delegate:options:userInfo:坪稽,交給
    SDImageCache從緩存查找圖片是否已經(jīng)下載
    queryDiskCacheForKey:delegate:userInfo:.

  3. 先從內(nèi)存圖片緩存查找是否有圖片曼玩,如果內(nèi)存中已經(jīng)有圖片緩存,SDImageCacheDelegate回調(diào) imageCache:didFindImage:forKey:userInfo:
    SDWebImageManager窒百。

  4. SDWebImageManagerDelegate 回調(diào)
    webImageManager:didFinishWithImage:UIImageView+WebCache,等前端展示圖片黍判。

  5. 如果內(nèi)存緩存中沒有,生成 `NSURLSession `
    添加到隊列篙梢,開始從沙盒查找圖片是否已經(jīng)緩存顷帖。

  6. 根據(jù) URL組合成的Key在沙河緩存目錄下嘗試讀取圖片文件。這一步是在 NSOperation 進行的操作渤滞,所以回主線程進行結(jié)果回調(diào) notifyDelegate:贬墩。

  7. 如果上一操作從沙盒讀取到了圖片,將圖片添加到內(nèi)存緩存中(如果空閑內(nèi)存過小妄呕, 會先清空內(nèi)存緩存)陶舞。SDImageCacheDelegate回調(diào) imageCache:didFindImage:forKey:userInfo:。進而回調(diào)展示圖片趴腋。

  8. 如果從沙盒目錄讀取不到圖片吊说,說明所有緩存都不存在該圖片,需要下載圖片优炬, 回調(diào) imageCache:didNotFindImageForKey:userInfo:

  9. 共享或重新生成一個下載器 SDWebImageDownloader開始下載圖片厅贪。

  10. 圖片下載由 NSURLConnection來做蠢护,實現(xiàn)相關(guān) delegate
    來判斷圖片下載中、下載完成和下載失敗养涮。

  11. connection:didReceiveData:中利用 ImageIO做了按圖片下載進度加載效果葵硕。

  12. connectionDidFinishLoading: 數(shù)據(jù)下載完成后交給 SDWebImageDecoder做圖片解碼處理眉抬。

  13. 圖片解碼處理在一個NSOperationQueue完成,不會拖慢主線程 UI.如果有需要 對下載的圖片進行二次處理懈凹,最好也在這里完成蜀变,效率會好很多。

  14. 在主線程notifyDelegateOnMainThreadWithInfo:
    宣告解碼完成imageDecoder:didFinishDecodingImage:userInfo:回調(diào)給SDWebImageDownloader介评。

15.imageDownloader:didFinishWithImage:回調(diào)給 SDWebImageManager告知圖片 下載完成库北。

  1. 通知所有的downloadDelegates下載完成,回調(diào)給需要的地方展示圖片们陆。

  2. 將圖片保存到 SDImageCache中寒瓦,內(nèi)存緩存和沙盒緩存同時保存。寫文件到沙盒也在以單獨NSOperation完成坪仇,避免拖慢主線程杂腰。

18.SDImageCache在初始化的時候會注冊一些消息通知,
在內(nèi)存警告或退到后臺的時 候清理內(nèi)存圖片緩存椅文,應(yīng)用結(jié)束的時候清理過期圖片喂很。


存:

-(void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
    return;
}
// if memory cache is enabled
if (self.shouldCacheImagesInMemory) {
    NSUInteger cost = SDCacheCostForImage(image);
    [self.memCache setObject:image forKey:key cost:cost];
}

if (toDisk) {
    dispatch_async(self.ioQueue, ^{
        NSData *data = imageData;
        // 如果image存在,但是需要重新計算(recalculate)或者data為空
        // 那就要根據(jù)image重新生成新的data
        // 不過要是連image也為空的話皆刺,那就別存了
        if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
            // 我們需要判斷image是PNG還是JPEG
            // PNG的圖片很容易檢測出來恤筛,因為它們有一個特定的標示 (http://www.w3.org/TR/PNG-Structure.html)
            // PNG圖片的前8個字節(jié)不許符合下面這些值(十進制表示)
            // 137 80 78 71 13 10 26 10
            
            // 如果imageData為空l (舉個例子,比如image在下載后需要transform芹橡,那么就imageData就會為空)
            // 并且image有一個alpha通道, 我們將該image看做PNG以避免透明度(alpha)的丟失(因為JPEG沒有透明色)
            int alphaInfo = CGImageGetAlphaInfo(image.CGImage);// 獲取image中的透明信息
            // 該image中確實有透明信息毒坛,就認為image為PNG
            BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                              alphaInfo == kCGImageAlphaNoneSkipFirst ||
                              alphaInfo == kCGImageAlphaNoneSkipLast);
            BOOL imageIsPng = hasAlpha;

            // 但是如果我們已經(jīng)有了imageData,我們就可以直接根據(jù)data中前幾個字節(jié)判斷是不是PNG
            if ([imageData length] >= [kPNGSignatureData length]) {
                // ImageDataHasPNGPreffix就是為了判斷imageData前8個字節(jié)是不是符合PNG標志
                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
            // 當(dāng)然豪直,如果不是在iPhone平臺上,就使用下面這個方法珠移。不過不在我們研究范圍之內(nèi)
            data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
        }

        // 獲取到需要存儲的data后弓乙,下面就要用fileManager進行存儲了
        if (data) {
            // 首先判斷disk cache的文件路徑是否存在,不存在的話就創(chuàng)建一個
            // disk cache的文件路徑是存儲在_diskCachePath中的
            if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
            }

            // 根據(jù)image的key(一般情況下理解為image的url)組合成最終的文件路徑
            // 上面那個生成的文件路徑只是一個文件目錄钧惧,就跟/cache/images/img1.png和cache/images/的區(qū)別一樣
            NSString *cachePathForKey = [self defaultCachePathForKey:key];
            // 這個url可不是網(wǎng)絡(luò)端的url暇韧,而是file在系統(tǒng)路徑下的url
            // 比如/foo/bar/baz --------> file:///foo/bar/baz
            NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];

            // 根據(jù)存儲的路徑(cachePathForKey)和存儲的數(shù)據(jù)(data)將其存放到iOS的文件系統(tǒng)
            [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];

            // disable iCloud backup
            if (self.shouldDisableiCloud) {
                [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
            }
        }
    });
}
}

取:

內(nèi)存緩存使用NSCache的objectForKey取數(shù)據(jù):

- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
    return [self.memCache objectForKey:key];
}

磁盤取數(shù)據(jù) 不斷用 dataWithContentsOfFile來試數(shù)據(jù)是否在key對應(yīng)的路徑中

- (UIImage *)imageFromDiskCacheForKey:(NSString *)key {

// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
    return image;
}

// Second check the disk cache...
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
    NSUInteger cost = SDCacheCostForImage(diskImage);
    [self.memCache setObject:diskImage forKey:key cost:cost];
}

return diskImage;
}

刪:

  1. removeImageForKeyfromDisk:withCompletion: // 異步地將image從緩存(內(nèi)存緩存以及可選的磁盤緩存)中移除
  2. clearMemory // 清楚內(nèi)存緩存上的所有image
  3. clearDisk // 清除磁盤緩存上的所有image
  4. cleanDisk // 清除磁盤緩存上過期的image
    看其中最長的一個:
// 實現(xiàn)了一個簡單的緩存清除策略:清除修改時間最早的file
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
    // 這兩個變量主要是為了下面生成NSDirectoryEnumerator準備的
    // 一個是記錄遍歷的文件目錄浓瞪,一個是記錄遍歷需要預(yù)先獲取文件的哪些屬性
    NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
    NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

    // 遞歸地遍歷diskCachePath這個文件夾中的所有目錄懈玻,此處不是直接使用diskCachePath,而是使用其生成的NSURL
    // 此處使用includingPropertiesForKeys:resourceKeys乾颁,這樣每個file的resourceKeys對應(yīng)的屬性也會在遍歷時預(yù)先獲取到
    // NSDirectoryEnumerationSkipsHiddenFiles表示不遍歷隱藏文件
    NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                               includingPropertiesForKeys:resourceKeys
                                                                  options:NSDirectoryEnumerationSkipsHiddenFiles
                                                             errorHandler:NULL];
    // 獲取文件的過期時間涂乌,SDWebImage中默認是一個星期
    // 不過這里雖然稱*expirationDate為過期時間艺栈,但是實質(zhì)上并不是這樣。
    // 其實是這樣的湾盒,比如在2015/12/12/00:00:00最后一次修改文件湿右,對應(yīng)的過期時間應(yīng)該是
    // 2015/12/19/00:00:00,不過現(xiàn)在時間是2015/12/27/00:00:00罚勾,我先將當(dāng)前時間減去1個星期毅人,得到
    // 2015/12/20/00:00:00,這個時間才是我們函數(shù)中的expirationDate荧库。
    // 用這個expirationDate和最后一次修改時間modificationDate比較看誰更晚就行堰塌。
    NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
    // 用來存儲對應(yīng)文件的一些屬性,比如文件所需磁盤空間
    NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
    // 記錄當(dāng)前已經(jīng)使用的磁盤緩存大小
    NSUInteger currentCacheSize = 0;

    // 在緩存的目錄開始遍歷文件.  此次遍歷有兩個目的:
    //
    //  1. 移除過期的文件
    //  2. 同時存儲每個文件的屬性(比如該file是否是文件夾分衫、該file所需磁盤大小场刑,修改時間)
    NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
    for (NSURL *fileURL in fileEnumerator) {
        NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];

        // 當(dāng)前掃描的是目錄,就跳過
        if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
            continue;
        }

        // 移除過期文件
        // 這里判斷過期的方式:對比文件的最后一次修改日期和expirationDate誰更晚蚪战,如果expirationDate更晚牵现,就認為該文件已經(jīng)過期,具體解釋見上面
        NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
        if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
            [urlsToDelete addObject:fileURL];
            continue;
        }

        // 計算當(dāng)前已經(jīng)使用的cache大小邀桑,
        // 并將對應(yīng)file的屬性存到cacheFiles中
        NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
        currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
        [cacheFiles setObject:resourceValues forKey:fileURL];
    }
    
    for (NSURL *fileURL in urlsToDelete) {
        // 根據(jù)需要移除文件的url來移除對應(yīng)file
        [_fileManager removeItemAtURL:fileURL error:nil];
    }

    // 如果我們當(dāng)前cache的大小已經(jīng)超過了允許配置的緩存大小瞎疼,那就刪除已經(jīng)緩存的文件。
    // 刪除策略就是壁畸,首先刪除修改時間更早的緩存文件
    if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
        // 直接將當(dāng)前cache大小降到允許最大的cache大小的一般
        const NSUInteger desiredCacheSize = self.maxCacheSize / 2;

        // 根據(jù)文件修改時間來給所有緩存文件排序贼急,按照修改時間越早越在前的規(guī)則排序
        NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                        usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                            return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                        }];

        // 每次刪除file后,就計算此時的cache的大小
        // 如果此時的cache大小已經(jīng)降到期望的大小了捏萍,就停止刪除文件了
        for (NSURL *fileURL in sortedFiles) {
            if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                // 獲取該文件對應(yīng)的屬性
                NSDictionary *resourceValues = cacheFiles[fileURL];
    // 根據(jù)resourceValues獲取該文件所需磁盤空間大小
                NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
    // 計算當(dāng)前cache大小
                currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];

                if (currentCacheSize < desiredCacheSize) {
                    break;
                }
            }
        }
    }
    // 如果有completionBlock太抓,就在主線程中調(diào)用
    if (completionBlock) {
        dispatch_async(dispatch_get_main_queue(), ^{
            completionBlock();
        });
    }
});
}

圖片儲存路徑:

// 簡單封裝了cachePathForKey:inPath
- (NSString *)defaultCachePathForKey:(NSString *)key {
    return [self cachePathForKey:key inPath:self.diskCachePath];
}

// cachePathForKey:inPath
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path {
// 根據(jù)傳入的key創(chuàng)建最終要存儲時的文件名
NSString *filename = [self cachedFileNameForKey:key];
// 將存儲的文件路徑和文件名綁定在一起,作為最終的存儲路徑
return [path stringByAppendingPathComponent:filename];
}

// cachedFileNameForKey:
- (NSString *)cachedFileNameForKey:(NSString *)key {
const char *str = [key UTF8String];
if (str == NULL) {
    str = "";
}
// 使用了MD5進行加密處理
// 開辟一個16字節(jié)(128位:md5加密出來就是128bit)的空間
unsigned char r[CC_MD5_DIGEST_LENGTH];
// 官方封裝好的加密方法
// 把str字符串轉(zhuǎn)換成了32位的16進制數(shù)列(這個過程不可逆轉(zhuǎn)) 存儲到了r這個空間中
CC_MD5(str, (CC_LONG)strlen(str), r);
// 最終生成的文件名就是 "md5碼"+".文件類型"
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;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末令杈,一起剝皮案震驚了整個濱河市走敌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌逗噩,老刑警劉巖掉丽,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異异雁,居然都是意外死亡捶障,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門片迅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來残邀,“玉大人,你說我怎么就攤上這事柑蛇〗嬲酰” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵耻台,是天一觀的道長空免。 經(jīng)常有香客問我,道長盆耽,這世上最難降的妖魔是什么蹋砚? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮摄杂,結(jié)果婚禮上坝咐,老公的妹妹穿的比我還像新娘。我一直安慰自己析恢,他們只是感情好墨坚,可當(dāng)我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著映挂,像睡著了一般泽篮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上柑船,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天帽撑,我揣著相機與錄音,去河邊找鬼鞍时。 笑死亏拉,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的逆巍。 我是一名探鬼主播及塘,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蒸苇!你這毒婦竟也來了磷蛹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤溪烤,失蹤者是張志新(化名)和其女友劉穎味咳,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體檬嘀,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡槽驶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了鸳兽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掂铐。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出全陨,到底是詐尸還是另有隱情爆班,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布辱姨,位于F島的核電站柿菩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏雨涛。R本人自食惡果不足惜枢舶,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望替久。 院中可真熱鬧凉泄,春花似錦、人聲如沸蚯根。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽稼锅。三九已至吼具,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間矩距,已是汗流浹背拗盒。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留锥债,地道東北人陡蝇。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像哮肚,于是被迫代替她去往敵國和親登夫。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容