美文网首页源码图片iOS Swift && Objective-C
SDWebImage 缓存模块实现分析

SDWebImage 缓存模块实现分析

作者: 王大屁帅2333 | 来源:发表于2016-02-07 17:10 被阅读2371次

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

    相关文章

      网友评论

        本文标题:SDWebImage 缓存模块实现分析

        本文链接:https://www.haomeiwen.com/subject/aosykttx.html