SDWebImage 缓存模块实现分析

上一篇 文章我们已经了解 SDWebImage 下载模块的实现,这篇文章我们继续了解其 缓存模块的实现

SDImageCache 的主要属性和方法都在第一篇的使用中介绍过了,下面主要讲解详细实现


缓存过程

因为网络速度和流量的考虑

SDWebImage 将下载的图片进行内存和磁盘缓存

  • 内存缓存保证读取缓存的速度
  • 磁盘缓存空间大,可以缓存的大量的图片

保存时先将下载的图片存入内存缓存,然后存入磁盘缓存,

读取时先从内存缓存中读取,如果不存在,再去磁盘中读取缓存,

节省流量,图片加载时间,提升用户体验

内存缓存使用 NSCache

NSCache 只有如下方法

使用方法类似 NSDictionary 只需设置 NSCache 能占用的最大内存totalCostLimit或者最多缓存数量countLimit,然后将需要缓存的图片,对象等 setValue:forKey:cost即可

比如我们设置缓存最多占用20mb,然后每次存入缓存图片时将图片大小作为 cost 参数传入,

当缓存大小或数量超过限定值时,内部的缓存机制就会自动为我们执行清理操作

而且NSCache 是线程安全的.

但是 SDWebImage 并不是这样使用的, 并没有设置缓存可以占用的最大内存量,也没有设置最大可缓存的对象数量

// See https://github.com/rs/SDWebImage/pull/1141 for discussion
@interface AutoPurgeCache : NSCache
@end

@implementation AutoPurgeCache

- (id)init
{
    self = [super init];
    if (self) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
    }
    return self;
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];

}

@end

SDWebImage 自定义了一个自动清理的缓存,监听 UIApplicationDidReceiveMemoryWarningNotification 通知,来清理缓存

我们仍可以主动设置 SDWebImageCache的

NSUInteger maxMemoryCost //缓存最多能占用多少内存,默认是0,无限大

NSUInteger maxMemoryCountLimit //最多能缓存多少张图片

来限制 SDWebImage 的内存占用

磁盘缓存使用 NSFileManager

在沙盒的Dictionary中,建立 com.hackemist.SDWebImageCache.default 目录,将每一个下载完成的图片存储为一个单独文件,文件名为根据图片对应的 Url用 MD5加密生成的字符串,类似 1d067b6f4457574b8165aef42643752e,这个字符串在 App 内唯一

ioQueue

_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);//串行队列,队列里的一个任务执行完毕才执行下一个  
磁盘缓存操作都在这个队列里异步执行,因为它是串行队列,任务一个执行完毕才执行下一个,所以不会出现一个文件同时被读取和写入的情况, 所以用 dispatch_async 而不必使用 disathc_barrier_async

缓存图片策略

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
    if (!image || !key) {
        return;
    }
    1.先存入内存缓存
    if (self.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }

    if (toDisk) {
    2.在 ioQueue 中串行处理所有磁盘缓存,
        dispatch_async(self.ioQueue, ^{
            NSData *data = imageData;

            if (data) {
            3.创建放缓存文件的文件夹
                if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                    [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                }

                4.根据 image 的 远程url 生成本地缓存图片对应的 url 
                先将远程的 url 进行 md5加密,作为文件名,然后拼接到默认的缓存路径下,作为缓存文件的 url
                com.hackemist.SDWebImageCache.default/1d067b6f4457574b8165aef42643752e
                // get cache Path for image key
                NSString *cachePathForKey = [self defaultCachePathForKey:key];
                // transform to NSUrl
                NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];

                5.将图片在磁盘中以文件的形式缓存起来,创建一个文件,写入 image 的 data
                [_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];

                6. 防止 icloud 备份缓存
                if (self.shouldDisableiCloud) {
                    [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
                }
            }
        });
    }
}

取出缓存图片的策略

取出缓存

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

    1. 先搜索内存缓存
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        return image;
    }

    2.再搜索磁盘缓存
    UIImage *diskImage = [self diskImageForKey:key];
    if (diskImage && self.shouldCacheImagesInMemory) {

    3.如果磁盘缓存中存在,将缓存图片放入内存缓存,并返回它
        NSUInteger cost = SDCacheCostForImage(diskImage);
        [self.memCache setObject:diskImage forKey:key cost:cost];
    }

    return diskImage;
}

取出内存缓存

//像 NSDictionary 一样,传入键,获取内存缓存的 image
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
    return [self.memCache objectForKey:key];
}

取出磁盘缓存

- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {

    1.根据图片的远程 url 生成本地缓存文件的 url, 根据 url 获取图片的 data
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath];
    if (data) {
        return data;
    }

    2.我们可以自定义缓存文件的存放路径,在自定义路径中搜索图片缓存
    NSArray *customPaths = [self.customPaths copy];
    for (NSString *path in customPaths) {
        NSString *filePath = [self cachePathForKey:key inPath:path];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath];
        if (imageData) {
            return imageData;
        }
    }
    return nil;
}

获取磁盘缓存大小

- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock {
    NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];

    dispatch_async(self.ioQueue, ^{
        NSUInteger fileCount = 0;
        NSUInteger totalSize = 0;
        1.遍历缓存目录下的所有文件
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:@[NSFileSize]
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];
        2.累加所有缓存文件的大小
        for (NSURL *fileURL in fileEnumerator) {
            NSNumber *fileSize;
            [fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL];
            totalSize += [fileSize unsignedIntegerValue];
            fileCount += 1;
        }

		3.主线程中回调
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock(fileCount, totalSize);
            });
        }
    });
}

清除缓存

清除缓存的方式非常简单,删掉缓存目录,再重新创建一个即可,这会删掉 App 的所有缓存

- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion
{
    dispatch_async(self.ioQueue, ^{
        [_fileManager removeItemAtPath:self.diskCachePath error:nil];
        [_fileManager createDirectoryAtPath:self.diskCachePath
                withIntermediateDirectories:YES
                                 attributes:nil
                                      error:NULL];

        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion();
            });
        }
    });
}

清理缓存,会删掉过期的缓存,根据 LRU (最近最少使用)算法,删除不常用的部分缓存

如果我们设置了 磁盘缓存最大占用空间 maxCacheSize, 那么清理缓存会保证磁盘缓存大小 < maxCacheSize / 2

- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

       1.遍历缓存目录下的所有缓存文件
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
        NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;

        2.删除所有过期的缓存文件
        3.存储缓存文件的大小,为接下来 清理缓存防止其占用过大的磁盘空间,做准备
        NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];

            4.跳过文件夹
            if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            5.记录过期的缓存,一会一起删除
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }

            6.记录缓存文件的大小
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
            [cacheFiles setObject:resourceValues forKey:fileURL];
        }

        7.删除过期的缓存
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }

        8.如果设置了缓存最大可占用的磁盘空间 self.maxCacheSize,那么接下来进行第二轮清理,防止缓存过大
        if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {

            const NSUInteger desiredCacheSize = self.maxCacheSize / 2;

            9.根据修改时间排序缓存文件,
            NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                            usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                            }];

            10.删除最旧的缓存文件,直到缓存文件大小 < 我们设定的 self.maxCacheSize /2
            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;
                    }
                }
            }
        }

        11.主线程回调
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

注意

默认情况下, SDWebImage已经监听广播来自动为我们执行清理操作

  • 当收到内存警告时,清空内存缓存
  • 当 App 进入关闭或进入后台时,清理磁盘缓存
[[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(clearMemory)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(cleanDisk)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundCleanDisk)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];

下一篇 中我会分析 SDWebImage 源码中的 Manager 的实现