前言
这是本系列的第 5 篇,也是最后一篇,主要讨论处理缓存的类 SDImageCache
及相关类 SDMemoryCache
、SDImageCacheConfig
等。
正文
先介绍 SDImageCache.h 中定义的 2 个枚举:SDImageCacheType
和 SDImageCacheOptions
。
typedef NS_ENUM(NSInteger, SDImageCacheType) {
// 不缓存,从网络下载数据
SDImageCacheTypeNone,
// 磁盘缓存
SDImageCacheTypeDisk,
// 内存缓存
SDImageCacheTypeMemory
};
typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
// 即使内存中有缓存,也要强制查询磁盘缓存
SDImageCacheQueryDataWhenInMemory = 1 << 0,
// 强制同步查询磁盘缓存
SDImageCacheQueryDiskSync = 1 << 1,
// 压缩大图
SDImageCacheScaleDownLargeImages = 1 << 2
};
包含几个重要属性:
/// *** 缓存配置信息 ***
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;
/// 内存缓存的最大消耗
@property (assign, nonatomic) NSUInteger maxMemoryCost;
/// 内存缓存的最大缓存数量
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;
/// *** 内存缓存 ***
@property (strong, nonatomic, nonnull) SDMemoryCache *memCache;
/// 磁盘缓存路径
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
///
@property (strong, nonatomic, nullable) NSMutableArray *customPaths;
/// 读写操作的串行队列
@property (strong, nonatomic, nullable) dispatch_queue_t ioQueue;
/// 用于操作文件的 fileManager
@property (strong, nonatomic, nonnull) NSFileManager *fileManager;
其中 2 个属性需要重点关注一下:
① config
所属类 SDImageCacheConfig
定义了很短属性,只提供了一个init方法,在里边给所有属性付了初值,详见下方法代码:
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
@implementation SDImageCacheConfig
- (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;
}
@end
② memCache
它属于一个继承自 NSCache 的缓存类 SDMemoryCache
, 他有一个关键属性:
// weakCache 是 NSMapTable 类型
@property (nonatomic, strong, nonnull) NSMapTable *weakCache;
下面观察一下 SDMemoryCache 的初始化及相关代码:
- (instancetype)initWithConfig:(SDImageCacheConfig *)config {
self = [super init];
if (self) {
self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
// 其他省略 ...
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didReceiveMemoryWarning:)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
}
return self;
}
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
// 注意:此处是调用的 suoper 方法,所以并没有移除 weak cache,如果是调用 self 重写的 removeAllObjects 方法,就会移除 weak cache。
[super removeAllObjects];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
}
当遇到内存警告的时候,缓存会被清除,但 weak cache 并不会被移除,如果手动清除的话,weak cache 当然会被移除。
这里 value 设置成 weak 可以避免可能的循环引用,虽然是 weak,不过,image 实例可以被其他对象持有,像 imageView,这种情况下,value 就不是 nil。
似乎扯远了O(∩_∩)O哈哈~,好了,我们还是切回来继续讨论 SDImageCache 提供的方法吧!
首先,是 SDImageCache
的创建方法,它给我们提供了一个单例方法 + (nonnull instancetype)sharedImageCache
,下边是他的方法实现:
+ (nonnull instancetype)sharedImageCache {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
然后,看看初始化方法,我们发现所有初始化方法最后均调用了同一个核心方法 - (nonnull instancetype)initWithNamespace: diskCacheDirectory:
,具体作用见下方代码注释。
- (instancetype)init {
return [self initWithNamespace:@"default"];
}
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns {
NSString *path = [self makeDiskCachePath:ns];
return [self initWithNamespace:ns diskCacheDirectory:path];
}
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory {
if ((self = [super init])) {
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
// 创建一个 IO 串行队列 (依次执行操作)
_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
// App 即将关闭的时候,清除过期缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
// App 即将进入后台的时候,清除过期缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
}
return self;
}
记得 上一篇 介绍 SDWebImageManager 的时候,是这样使用 imageCache 的:
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key
options:cacheOptions
done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType)
{
// 查询完成后的操作在这里,可能查到了,也可能没查到 ...
}
现在,我们就来揭开这个方法的什么面纱。
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key
options:(SDImageCacheOptions)options
done:(nullable SDCacheQueryCompletedBlock)doneBlock
{
// 1.校验参数
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 2.查询内存缓存 (NSCache)
UIImage *image = [self imageFromMemoryCacheForKey:key];
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
NSOperation *operation = [NSOperation new];
// 3.将获取缓存及解压的 ‘耗时’ 操作封装成一个 block
void(^queryDiskBlock)(void) = ^{
// 如果已经取消,不作任何处理,直接返回。
if (operation.isCancelled) {
return;
}
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeDisk;
if (image) {
// A > 从 memery 取的
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
// B > 如果内存没有,但是从 disc 取到了,需要解压
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);
});
}
}
}
};
// 4.执行查询磁盘缓存的 block
if (options & SDImageCacheQueryDiskSync) {
queryDiskBlock();
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
如上边的代码所示,主要分了这么 4 步:
① 校验参数 --- 如果 key 不存在,直接 doneBlock,返回 nil。
② 查询内存缓存 NSCache --- 如果内存中有,并且没有强制要求必须查询磁盘,则 执行 doneBlock,将 image 返回。
③ 将获取缓存及解压的 ‘耗时’ 操作封装成一个 block --- 这是为了最后执行异步操作的方便。
④ 执行查询磁盘缓存的 block --- 如果设置了 SDImageCacheQueryDiskSync
,则同步执行;否则,默认是异步执行。
第 ③ 步中查询磁盘缓存的 queryDiskBlock
里边有两个比较重要的方法:- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:
和 - (nullable UIImage *)diskImageForKey: data: options:
,下边分别了解一下这两个方法:
- diskImageDataBySearchingAllPathsForKey: 这个方法用于查询磁盘缓存,实现及代码注释如下,拼接缓存路径的方法就不展开了,其中文件名的生成是对传入的 key 执行了一次 MD5。
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
// 1.尝试 通过默认路径查询磁盘缓存
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath options:self.config.diskCacheReadingOptions error:nil];
if (data) {
return data;
}
// 2.异常情况的处理:更换了路径再取一次,新路径是将默认路径的后缀去掉 (如果有的话)
data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
if (data) {
return data;
}
// 3.遍历所有用户自定义的路径,执行类似 1、2 的操作,查询磁盘缓存
NSArray *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;
}
}
// 没查到的话,返回 nil
return nil;
}
- diskImageForKey: data: options: 此方法的作用是对从 Disc 直接取的 data,进行 解码、解压操作,实现代码如下。
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options {
if (data) {
// 1.解码
UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
image = [self scaledImageForKey:key image:image];
// 2.解压
if (self.config.shouldDecompressImages) {
BOOL shouldScaleDown = options & SDImageCacheScaleDownLargeImages;
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
}
return image;
} else {
return nil;
}
}
此方法分别使用了 SDWebImageCodersManager 的 2 个重要方法:
- 解码方法
- (UIImage *)decodedImageWithData:data
,其中 coder 可以理解为一个解码器,SDWebImage
提供了多种 coder,如SDWebImageIOCoder
、SDWebImageGIFCoder
分别用于解码某一种类型的图片,如果新增一种图片,可以将对应的 coder(需遵守协议:SDWebImageCoder) 添加到 coders 里即可。
// SDWebImageCodersManager.m
- (UIImage *)decodedImageWithData:(NSData *)data {
LOCK(self.codersLock);
NSArray> *coders = self.coders;
UNLOCK(self.codersLock);
for (id coder in coders.reverseObjectEnumerator) {
if ([coder canDecodeFromData:data]) {
return [coder decodedImageWithData:data];
}
}
return nil;
}
- 解压方法
- (UIImage *)decompressedImageWithImage:image data:data options:optionsDict
,这个方法和上边的解码方法都属于SDWebImageCoder
这个协议,与解码方法类似,也是针对不同类型的 image 有不同的 coder。
// SDWebImageCodersManager.m
- (UIImage *)decompressedImageWithImage:(UIImage *)image
data:(NSData *__autoreleasing _Nullable *)data
options:(nullable NSDictionary*)optionsDict {
if (!image) {
return nil;
}
LOCK(self.codersLock);
NSArray> *coders = self.coders;
UNLOCK(self.codersLock);
for (id coder in coders.reverseObjectEnumerator) {
if ([coder canDecodeFromData:*data]) {
UIImage *decompressedImage = [coder decompressedImageWithImage:image data:data options:optionsDict];
decompressedImage.sd_imageFormat = image.sd_imageFormat;
return decompressedImage;
}
}
return nil;
}
那么,这些 coder 是什么时候加进去的,又是怎么添加的呢?其实,这些逻辑都在 SDWebImageCodersManager 的实现代码里。
// SDWebImageCodersManager.m
- (instancetype)init {
if (self = [super init]) {
// 初始化 coders
NSMutableArray> *mutableCoders = [@[[SDWebImageImageIOCoder sharedCoder]] mutableCopy];
#ifdef SD_WEBP
[mutableCoders addObject:[SDWebImageWebPCoder sharedCoder]];
#endif
_coders = [mutableCoders copy];
_codersLock = dispatch_semaphore_create(1);
}
return self;
}
从初始化方法可以看出来,此时只给 coders 添加了一种 coder,即 SDWebImageImageIOCoder,它是用来对普通的 JPG、PNG 等图片解码的。
为了支持对其他类型图片(如 GIF)的解码,manager 给我们提供了下边这个添加 coder 的方法,代码逻辑很简单,就不多做解释了。
// SDWebImageCodersManager.m
- (void)addCoder:(nonnull id)coder {
if (![coder conformsToProtocol:@protocol(SDWebImageCoder)]) {
return;
}
LOCK(self.codersLock);
NSMutableArray> *mutableCoders = [self.coders mutableCopy];
if (!mutableCoders) {
mutableCoders = [NSMutableArray array];
}
[mutableCoders addObject:coder];
self.coders = [mutableCoders copy];
UNLOCK(self.codersLock);
}
小结
关于缓存相关的类暂时就先介绍到这里,当然还有很多细节没来得及讨论,不过可以查看 demo 中的注释。
到此,关于 SDWebImage 的源码学习就告一段落了,目前的理解可能有点肤浅,以后随着理解的深入,会不定时的更新。