SDWebImage緩存機制


存 取 刪 路徑

1.1 存

是在storeImage這個方法里:

將圖片儲存到內存和硬盤上

-(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為空
        // 那就要根據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個字節不許符合下面這些值(十進制表示)
            // 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;

            // 但是如果我們已經有了imageData,我們就可以直接根據data中前幾個字節判斷是不是PNG
            if ([imageData length] >= [kPNGSignatureData length]) {
                // ImageDataHasPNGPreffix就是為了判斷imageData前8個字節是不是符合PNG標志
                imageIsPng = ImageDataHasPNGPreffix(imageData);
            }

            // 如果image是PNG格式,就是用UIImagePNGRepresentation將其轉化為NSData,否則按照JPEG格式轉化,並且壓縮質量為1,即無壓縮
            if (imageIsPng) {
                data = UIImagePNGRepresentation(image);
            }
            else {
                data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
            }
#else
            // 當然,如果不是在iPhone平台上,就使用下面這個方法。不過不在我們研究范圍之內
            data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
        }

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

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

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

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

1.2 取

內存緩存使用NSCache的objectForKey取數據:

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

磁盤取數據 不斷用 dataWithContentsOfFile來試數據是否在key對應的路徑中

- (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.3 刪

  1. removeImageForKeyfromDisk:withCompletion: // 異步地將image從緩存(內存緩存以及可選的磁盤緩存)中移除
  2. clearMemory // 清楚內存緩存上的所有image
  3. clearDisk // 清除磁盤緩存上的所有image
  4. cleanDisk // 清除磁盤緩存上過期的image

看其中最長的一個:

// 實現了一個簡單的緩存清除策略:清除修改時間最早的file
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
    // 這兩個變量主要是為了下面生成NSDirectoryEnumerator准備的
    // 一個是記錄遍歷的文件目錄,一個是記錄遍歷需要預先獲取文件的哪些屬性
    NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
    NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

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

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

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

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

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

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

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

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

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

1.4 圖片儲存路徑

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

// cachePathForKey:inPath
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path {
// 根據傳入的key創建最終要存儲時的文件名
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字節(128位:md5加密出來就是128bit)的空間
unsigned char r[CC_MD5_DIGEST_LENGTH];
// 官方封裝好的加密方法
// 把str字符串轉換成了32位的16進制數列(這個過程不可逆轉) 存儲到了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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM