SDWebImage 缓存模块实现分析

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


缓存过程

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

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

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

保存时先将下载的图片存入内存缓存,然后存入磁盘缓存,
读取时先从内存缓存中读取,如果不存在,再去磁盘中读取缓存,
节省流量,图片加载时间,提升用户体验

内存缓存使用 NSCache

NSCache 只有如下方法

NSCache.png

使用方法类似 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 内唯一

cacheUrl.png

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];

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容