SDWebImage解析之SDImageCache

我们知道移动端有三级缓存:远程的服务器,还有本地的内存缓存,硬盘缓存。SDImageCache的存在就是为了解决图片的本地缓存。SDImageCache是SDWebImage一个很重要的部分。为SDWebImage提供了以下功能:

  • 内存缓存,基于NSCache

  • 异步的硬盘缓存,不会阻塞主线程

  • 提供同步和异步两种方式来查询是否有缓存图片

  • 提供同步和异步来获取缓存的图片

  • 删除缓存图片

SDImageCacheConfig

我们先来看看SDImageCacheConfig,SDImageCacheConfig是用来图片缓存的配置信息。

//SDImageCacheConfigExpireType定义的是以什么方式来计算图片的过期时间
typedef NS_ENUM(NSUInteger, SDImageCacheConfigExpireType) {
    //图片最近访问的时间
    SDImageCacheConfigExpireTypeAccessDate,
    //默认:图片最近修改的时间
    SDImageCacheConfigExpireTypeModificationDate
};

SDImageCacheConfig的配置属性和注释如下:

@interface SDImageCacheConfig : NSObject

//预解码图片,默认YES;
//预解码图片可以提升性能,但会消耗太多的内存
@property (assign, nonatomic) BOOL shouldDecompressImages;

//取消iCloud备份
@property (assign, nonatomic) BOOL shouldDisableiCloud;

//是否禁用内存缓存
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;

//开启SDMemoryCache内部维护的一张图片弱引用表
//好处:当收到内存警告,SDMemoryCache会移除图片的缓存,但是有些图片此时已经被一些诸如UIImageView强引用这,使用这个弱引用表就能访问到图片,避免后面再去query硬盘缓存
@property (assign, nonatomic) BOOL shouldUseWeakMemoryCache;

//硬盘图片读取的配置选项,默认是0
@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions;

//把图片存入硬盘的配置选项,默认NSDataWritingAtomic原子操作
@property (assign, nonatomic) NSDataWritingOptions diskCacheWritingOptions;

//图片最大的缓存时间,默认1星期
@property (assign, nonatomic) NSInteger maxCacheAge;

//最大的缓存大小,如果maxCacheSize>0,在清除硬盘缓存的时候会先把缓存时间操作maxCacheAge的图片清除掉,然后再清除图片到总缓存大小在maxCacheSize * 0.5以下
@property (assign, nonatomic) NSUInteger maxCacheSize;

//硬盘缓存图片过期时间的计算方式,默认是最近修改的时间
@property (assign, nonatomic) SDImageCacheConfigExpireType diskCacheExpireType;

@end

SDImageCacheConfig的初始化函数:

static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
- (instancetype)init {
    if (self = [super init]) {
        _shouldDecompressImages = YES;
        _shouldDisableiCloud = YES;
        _shouldCacheImagesInMemory = YES;
        _shouldUseWeakMemoryCache = YES;
        _diskCacheReadingOptions = 0;
        _diskCacheWritingOptions = NSDataWritingAtomic;
        //默认一周的缓存时间
        _maxCacheAge = kDefaultCacheMaxCacheAge;
        _maxCacheSize = 0;
        _diskCacheExpireType = SDImageCacheConfigExpireTypeModificationDate;
    }
    return self;
}

SDImageCache

SDImageCache使用SDMemoryCache(继承NSCache)来实现内存缓存,使用NSFileManager来进行图片的硬盘存取。

SDMemoryCache.h

下面我们先来阅读下SDMemoryCache.h文件

扫描二维码关注公众号,回复: 5214942 查看本文章
//用来表示图片的缓存类型
typedef NS_ENUM(NSInteger, SDImageCacheType) {
    //图片来自服务器
    SDImageCacheTypeNone,
    //图片来自硬盘缓存
    SDImageCacheTypeDisk,
    //图片来自内存缓存
    SDImageCacheTypeMemory
};
//缓存图片的方式
typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
    //当内存有图片,仍然查询硬盘缓存
    SDImageCacheQueryDataWhenInMemory = 1 << 0,
    //同步的方式来获取硬盘缓存(默认异步)
    SDImageCacheQueryDiskSync = 1 << 1,
    //缩小大图(>60M)
    SDImageCacheScaleDownLargeImages = 1 << 2
};
@interface SDImageCache : NSObject

@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;

//内存缓存的最大值(图片像素)
@property (assign, nonatomic) NSUInteger maxMemoryCost;

//内存缓存的最大数量(图片数量)
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;

//暴露的单例对象
+ (nonnull instancetype)sharedImageCache;

//指定命名空间,图片存到对应的沙盒目录中
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns;

//指定命名空间和沙盒目录
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory;

#pragma mark - Cache paths
//获取磁盘缓存路径
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace;

//添加只读路径
- (void)addReadOnlyCachePath:(nonnull NSString *)path;

#pragma mark - Store Ops

//异步的方式存储图片到内存和硬盘
- (void)storeImage:(nullable UIImage *)image
            forKey:(nullable NSString *)key
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;

//异步的方式存储图片到内存和硬盘(可选,toDisk为YES才会存入硬盘)
- (void)storeImage:(nullable UIImage *)image
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;

//当我们把图片存入硬盘时,需要把图片压缩为二进制格式才能存。当我们下载完图片后,得到的是图片的二进制数据,这个方法提供了imageData的入口,来避免对图片额外的压缩消耗
- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;

//同步的方式把图片的二进制数据存入硬盘
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key;

#pragma mark - Query and Retrieve Ops

//异步的方式查询硬盘中是否有key对应的缓存图片
- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

//同步的方式查询硬盘中是否有key对应的缓存图片
- (BOOL)diskImageDataExistsWithKey:(nullable NSString *)key;

//同步的方式获取硬盘缓存的图片二进制数据
- (nullable NSData *)diskImageDataForKey:(nullable NSString *)key;

//异步的方式来获取硬盘缓存的图片
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;

//异步的方式来获取硬盘缓存的图片
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock;

//同步的方式来获取内存缓存的图片
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key;

/同步的方式获取硬盘缓存的图片
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key;

//同步的方式,先查询内存中有没有缓存的图片,如果没有再查询硬盘中有没有缓存的图片
- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key;

#pragma mark - Remove Ops

//异步的方式移除缓存中的图片,包括内存和硬盘
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;

//异步的方式移除缓存中的图片,包括内存和硬盘(可选,fromDisk为YES移除硬盘缓存)
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;

#pragma mark - Cache clean Ops

//清除内存缓存
- (void)clearMemory;

//异步方式清除硬盘缓存
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;

//异步方式清除过期的图片
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;

#pragma mark - Cache Info

//同步方式计算缓存目录的大小
- (NSUInteger)getSize;

//同步方式计算缓存的图片数量
- (NSUInteger)getDiskCount;

//异步的方式获取缓存图片数量和大小
- (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock;

#pragma mark - Cache Paths

//指定key,获取图片的缓存路径
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path;

//指定key,获取图片默认的缓存路径
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key;

@end

SDImageCache.m

SDMemoryCache继承自NSCache。NSCache可以设置totalCostLimit,来限制缓存的总成本消耗,所以我们再添加缓存的时候需要

- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;

通过cost来指定缓存对象消耗的成本。

SDImageCache如何计算图片消耗的成本?

SDImageCache把图片的像素点来计算图片的消耗成本

FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
#if SD_MAC
    return image.size.height * image.size.width;
#elif SD_UIKIT || SD_WATCH
    return image.size.height * image.size.width * image.scale * image.scale;
#endif
}
SDMemoryCache

解析以注释的形式在代码中体现:

@interface SDMemoryCache <KeyType, ObjectType> ()

@property (nonatomic, strong, nonnull) SDImageCacheConfig *config;
//弱引用表
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; 
//多线程锁保证多线程环境下weakCache数据安全
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock; 

- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithConfig:(nonnull SDImageCacheConfig *)config;

@end

@implementation SDMemoryCache

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

- (instancetype)initWithConfig:(SDImageCacheConfig *)config {
    self = [super init];
    if (self) {
        //初始化弱引用表,当收到内存警告,内存缓存虽然被清理,但是有些图片已经被其他对象强引用着,这时weakCache维持这些图片的弱引用。如果需要获取这些图片就不用去硬盘获取了
        //NSPointerFunctionsWeakMemory,对值进行弱引用,不会对引用计数+1
        self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
        self.weakCacheLock = dispatch_semaphore_create(1);
        self.config = config;
        //监听内存警告通知
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(didReceiveMemoryWarning:)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];
    }
    return self;
}

- (void)didReceiveMemoryWarning:(NSNotification *)notification {
    //当收到内存警告通知,移除内存中缓存的图片
    //同时保留weakCache,维持对被强引用着的图片的访问
    [super removeAllObjects];
}

- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
    [super setObject:obj forKey:key cost:g];
    if (!self.config.shouldUseWeakMemoryCache) {
        return;
    }
    if (key && obj) {
        // 存入弱引用表
        LOCK(self.weakCacheLock);
        [self.weakCache setObject:obj forKey:key];
        UNLOCK(self.weakCacheLock);
    }
}

- (id)objectForKey:(id)key {
    id obj = [super objectForKey:key];
    if (!self.config.shouldUseWeakMemoryCache) {
        return obj;
    }
    if (key && !obj) {
        LOCK(self.weakCacheLock);
        obj = [self.weakCache objectForKey:key];
        UNLOCK(self.weakCacheLock);
        if (obj) {
            // 把通过弱引用表获取的图片添加到内存缓存中
            NSUInteger cost = 0;
            if ([obj isKindOfClass:[UIImage class]]) {
                cost = SDCacheCostForImage(obj);
            }
            [super setObject:obj forKey:key cost:cost];
        }
    }
    return obj;
}

- (void)removeObjectForKey:(id)key {
    [super removeObjectForKey:key];
    if (!self.config.shouldUseWeakMemoryCache) {
        return;
    }
    if (key) {
        // 从weakCache移除
        LOCK(self.weakCacheLock);
        [self.weakCache removeObjectForKey:key];
        UNLOCK(self.weakCacheLock);
    }
}

- (void)removeAllObjects {
    [super removeAllObjects];
    if (!self.config.shouldUseWeakMemoryCache) {
        return;
    }
    // 清除弱引用表
    LOCK(self.weakCacheLock);
    [self.weakCache removeAllObjects];
    UNLOCK(self.weakCacheLock);
}

#else

- (instancetype)initWithConfig:(SDImageCacheConfig *)config {
    self = [super init];
    return self;
}

#endif

@end

SDImageCache私有拓展的属性:

//内存缓存管理实例
@property (strong, nonatomic, nonnull) SDMemoryCache *memCache;
//硬盘缓存路径
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
//读取缓存图片额外的路径,以通过addReadOnlyCachePath:这个方法往里边添加路径
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
//访问操作硬盘缓存时用到的串行队列
@property (strong, nonatomic, nullable) dispatch_queue_t ioQueue;
//存储图片到硬盘的文件管理者
@property (strong, nonatomic, nonnull) NSFileManager *fileManager;

初始化函数最终都会调用

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns

​ diskCacheDirectory:(nonnull NSString *)directory

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory {
    if ((self = [super init])) {
        //初始化参数
        NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
        _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
        _config = [[SDImageCacheConfig alloc] init];
        _memCache = [[SDMemoryCache alloc] initWithConfig:_config];
        _memCache.name = fullNamespace;
        if (directory != nil) {
            _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
        } else {
            NSString *path = [self makeDiskCachePath:ns];
            _diskCachePath = path;
        }
        dispatch_sync(_ioQueue, ^{
            self.fileManager = [NSFileManager new];
        });

#if SD_UIKIT
        //监听通知来清除过期的图片缓存数据
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(deleteOldFiles)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundDeleteOldFiles)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];
#endif
    }

    return self;
}
  • 如果进入沙盒查看缓存的图片,可以发现文件名是用过md5的格式来命名

    - (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
        const char *str = key.UTF8String;
        if (str == NULL) {
            str = "";
        }
        unsigned char r[CC_MD5_DIGEST_LENGTH];
        //计算key的md5值
        CC_MD5(str, (CC_LONG)strlen(str), r);
        NSURL *keyURL = [NSURL URLWithString:key];
        NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
        //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], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]];
        return filename;
    }
    
  • 异步缓存图片

    - (void)storeImage:(nullable UIImage *)image
             imageData:(nullable NSData *)imageData
                forKey:(nullable NSString *)key
                toDisk:(BOOL)toDisk
            completion:(nullable SDWebImageNoParamsBlock)completionBlock {
        if (!image || !key) {
            if (completionBlock) {
                completionBlock();
            }
            return;
        }
        // 如果允许内存缓存,先把图片缓存到内存
        if (self.config.shouldCacheImagesInMemory) {
            NSUInteger cost = SDCacheCostForImage(image);
            [self.memCache setObject:image forKey:key cost:cost];
        }
        
        if (toDisk) {
            dispatch_async(self.ioQueue, ^{
              //一般图片的大小都不会很小,对图片进行编码过程中也会产出一些开销不小的临时对象,在子线程中添加自动释放池,可以提前释放这些对象,缓解内存压力。
                @autoreleasepool {
                    NSData *data = imageData;
                    if (!data && image) {
                        SDImageFormat format;
                        if (SDCGImageRefContainsAlpha(image.CGImage)) {
                            format = SDImageFormatPNG;
                        } else {
                            format = SDImageFormatJPEG;
                        }
                        //把图片进行编码,得到可以存储的二进制数据
                        data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
                    }
                    [self _storeImageDataToDisk:data forKey:key];
                }
                
                if (completionBlock) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        completionBlock();
                    });
                }
            });
        } else {
            if (completionBlock) {
                completionBlock();
            }
        }
    }
    
  • 内部方法:使用的把图片二进制数据存入硬盘

  • - (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
        if (!imageData || !key) {
            return;
        }
        
        if (![self.fileManager fileExistsAtPath:_diskCachePath]) {
          //如果还没有缓存目录,通过fileManager生成缓存目录
            [self.fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
        }
        
        // 获取缓存的文件名,对key进行md5加密
        NSString *cachePathForKey = [self defaultCachePathForKey:key];
        NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
        //保存到硬盘
        [imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
        
        // 禁止iCloud备份
        if (self.config.shouldDisableiCloud) {
            [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
        }
    }
    
  • 因为从硬盘查询是否有缓存图片会是一个比较耗时的操作,所以SDImageCache还提供了异步的方式,通过block回调查询结果

    - (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {
        dispatch_async(self.ioQueue, ^{
            BOOL exists = [self _diskImageDataExistsWithKey:key];
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock(exists);
                });
            }
        });
    }
    
  • 在命名空间指定的目录和自定义只读路径中查询获取图片

    - (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
        NSString *defaultPath = [self defaultCachePathForKey:key];
        //在缓存目录查找获取图片
        NSData *data = [NSData dataWithContentsOfFile:defaultPath options:self.config.diskCacheReadingOptions error:nil];
        if (data) {
            return data;
        }
        data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
        if (data) {
            return data;
        }
      //在自定义的只读路径中查询获取图片
        NSArray<NSString *> *customPaths = [self.customPaths copy];
        for (NSString *path in customPaths) {
            NSString *filePath = [self cachePathForKey:key inPath:path];
            NSData *imageData = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
            if (imageData) {
                return imageData;
            }
    
            imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
            if (imageData) {
                return imageData;
            }
        }
    
        return nil;
    }
    
  • 从硬盘中获取图片(同步的方式获取),查询到图片后shouldCacheImagesInMemory=YES同步保存到内存缓存中,下次再获取的时候直接从内存中获取

    - (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key {
        UIImage *diskImage = [self diskImageForKey:key];
        if (diskImage && self.config.shouldCacheImagesInMemory) {
            NSUInteger cost = SDCacheCostForImage(diskImage);
            //同步缓存到内存
            [self.memCache setObject:diskImage forKey:key cost:cost];
        }
        return diskImage;
    }
    
    - (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key {
      //先从内存中尝试获取缓存图片
        UIImage *image = [self imageFromMemoryCacheForKey:key];
        if (image) {
            return image;
        }
        //内存中没有缓存,尝试从硬盘中获取 imageFromDiskCacheForKey 就是上面的方法
        image = [self imageFromDiskCacheForKey:key];
        return image;
    }
    
    - (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
        NSData *data = [self diskImageDataForKey:key];
        return [self diskImageForKey:key data:data];
    }
    
    - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data {
        return [self diskImageForKey:key data:data options:0];
    }
    
    - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options {
        if (data) {
          //实例化一个uiimage,此时的uiimage还没有进行解码
            UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
            image = [self scaledImageForKey:key image:image];
            if (self.config.shouldDecompressImages) {
              //对image进行预解码
                BOOL shouldScaleDown = options & SDImageCacheScaleDownLargeImages;
                image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
            }
            return image;
        } else {
            return nil;
        }
    }
    
  • 异步的方式获取硬盘缓存图片

    - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
        if (!key) {
            if (doneBlock) {
                doneBlock(nil, nil, SDImageCacheTypeNone);
            }
            return nil;
        }
        
        // 首先检查内存中是否有缓存的图片
        UIImage *image = [self imageFromMemoryCacheForKey:key];
        //当内存中有缓存,并且SDImageCacheQueryDataWhenInMemory(内存中有缓存仍然查询硬盘缓存)为false,立即返回内存缓存的图片
        BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
        if (shouldQueryMemoryOnly) {
            if (doneBlock) {
                doneBlock(image, nil, SDImageCacheTypeMemory);
            }
            return nil;
        }
        
        NSOperation *operation = [NSOperation new];
        void(^queryDiskBlock)(void) =  ^{
            if (operation.isCancelled) {
                // operation取消,直接返回
                return;
            }
            
            @autoreleasepool {
              //从硬盘获取缓存
                NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
                UIImage *diskImage;
                SDImageCacheType cacheType = SDImageCacheTypeDisk;
                if (image) {
                    // 如果内存本来就有缓存图片,则把内存缓存的图片返回,可以省去对图片再一次解码
                    diskImage = image;
                    cacheType = SDImageCacheTypeMemory;
                } else if (diskData) {
                    // 如果内存没有缓存的图片,那么从硬盘得到的是图片压缩的二进制数据,使用前需要先解码
                    diskImage = [self diskImageForKey:key data:diskData options:options];
                    if (diskImage && self.config.shouldCacheImagesInMemory) {
                      //同步保存到内存缓存中
                        NSUInteger cost = SDCacheCostForImage(diskImage);
                        [self.memCache setObject:diskImage forKey:key cost:cost];
                    }
                }
                
                if (doneBlock) {
                    if (options & SDImageCacheQueryDiskSync) {
                        doneBlock(diskImage, diskData, cacheType);
                    } else {
                        dispatch_async(dispatch_get_main_queue(), ^{
                            doneBlock(diskImage, diskData, cacheType);
                        });
                    }
                }
            }
        };
        
        if (options & SDImageCacheQueryDiskSync) {
            queryDiskBlock();
        } else {
            dispatch_async(self.ioQueue, queryDiskBlock);
        }
        
        return operation;
    }
    
  • 从缓存中移除图片
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion {
    if (key == nil) {
        return;
    }

    if (self.config.shouldCacheImagesInMemory) {
        [self.memCache removeObjectForKey:key];
    }

    if (fromDisk) {
        dispatch_async(self.ioQueue, ^{
            [self.fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
            
            if (completion) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completion();
                });
            }
        });
    } else if (completion){
        completion();
    }
    
}
  • 清空缓存
//清空内存缓存
- (void)clearMemory {
    [self.memCache removeAllObjects];
}
//清空硬盘缓存
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion {
    dispatch_async(self.ioQueue, ^{
        [self.fileManager removeItemAtPath:self.diskCachePath error:nil];
        [self.fileManager createDirectoryAtPath:self.diskCachePath
                withIntermediateDirectories:YES
                                 attributes:nil
                                      error:NULL];

        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion();
            });
        }
    });
}
  • SDImageCache在app退出,或者进入到后台,都会对缓存目录下的图片进行清理,把过期的图片移除掉
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];

        NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
        switch (self.config.diskCacheExpireType) {
            case SDImageCacheConfigExpireTypeAccessDate:
                //最近访问的时间到现在作为过期时间
                cacheContentDateKey = NSURLContentAccessDateKey;
                break;

            case SDImageCacheConfigExpireTypeModificationDate:
                //最近修改的时间到现在作为过期时间
                cacheContentDateKey = NSURLContentModificationDateKey;
                break;

            default:
                break;
        }
        
        NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];

        // This enumerator prefetches useful properties for our cache files.
        NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];
        //通过SDImageCacheConfig.maxCacheAge计算出该被移除的缓存时间边界值
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
        //用来保存未过期的缓存图片
        NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
        //用来记录当前缓存大小
        NSUInteger currentCacheSize = 0;
        //用来保存过期图片的url
        NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
        
        // 在缓存目录下遍历图片,把过期的缓存图片移除掉,把未过期的图片添加到cacheFiles来进行下一步的清理工作(以size作为清理标准)
        for (NSURL *fileURL in fileEnumerator) {
            NSError *error;
            NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];

            // 跳过文件夹
            if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            // 移除过期的图片
            NSDate *modifiedDate = resourceValues[cacheContentDateKey];
            //laterDate:返回的是较晚的时间,如果修改时间比上面计算的缓存时间边界值还早,就说明该缓存图片已经过期了,添加到urlsToDelete等待删除
            if ([[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }
            
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
            cacheFiles[fileURL] = resourceValues;
        }
        
        //循环遍历,移除过期的图片
        for (NSURL *fileURL in urlsToDelete) {
            [self.fileManager removeItemAtURL:fileURL error:nil];
        }

        // 如果我们设置了SDImageCacheConfig.maxCacheSize,并且当前缓存目录的大小大于config.maxCacheSize,需要对缓存目录进行二次清理,直到缓存目录大小 <= config.maxCacheSize/2
        if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
            const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
            //把缓存的图片从新排序,较早的图片放在前面
            NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                     usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                         return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                     }];

            // 删除图片知道缓存目录大小 <= config.maxCacheSize/2
            for (NSURL *fileURL in sortedFiles) {
                if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}
  • 同步的方式获取缓存目录的大小和文件数
- (NSUInteger)getSize {
    __block NSUInteger size = 0;
    dispatch_sync(self.ioQueue, ^{
        NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtPath:self.diskCachePath];
        for (NSString *fileName in fileEnumerator) {
            NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
            NSDictionary<NSString *, id> *attrs = [self.fileManager attributesOfItemAtPath:filePath error:nil];
            size += [attrs fileSize];
        }
    });
    return size;
}

- (NSUInteger)getDiskCount {
    __block NSUInteger count = 0;
    dispatch_sync(self.ioQueue, ^{
        NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtPath:self.diskCachePath];
        count = fileEnumerator.allObjects.count;
    });
    return count;
}
  • 异步的方式获取缓存目录的大小和文件数,block回调
- (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock {
    NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];

    dispatch_async(self.ioQueue, ^{
        NSUInteger fileCount = 0;
        NSUInteger totalSize = 0;

        NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:@[NSFileSize]
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

        for (NSURL *fileURL in fileEnumerator) {
            NSNumber *fileSize;
            [fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL];
            totalSize += fileSize.unsignedIntegerValue;
            fileCount += 1;
        }

        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock(fileCount, totalSize);
            });
        }
    });
}

猜你喜欢

转载自blog.csdn.net/weixin_34239592/article/details/87346862