打算用几篇文章整理一下 SDWebImage 的源码
源码有点小多, 决定把每个模块分开来整理
这其中包括 : 调度模块、下载模块、缓存模块、解码模块和一些代码整理
调度模块看这里
缓存模块看这里
下载模块看这里
解码模块看这里
整理模块看这里
本文是缓存模块
SDImageCache
SDWebImage 的缓存包括内存缓存和磁盘缓存
内存缓存和磁盘存储都交给 SDImageCache 这个类
先看一下 SDImageCache 重要属性的作用
/** 用于管理内存缓存 */
@property (strong, nonatomic, nonnull) SDMemoryCache *memCache;
/** 磁盘路径, Library/Caches + 默认命名空间 @"default" */
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
/** */
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
/** 执行任务用的串行队列 */
@property (strong, nonatomic, nullable) dispatch_queue_t ioQueue;
/** 文件管理 */
@property (strong, nonatomic, nonnull) NSFileManager *fileManager;
1: 内存管理 memCache
是集成自 NSCache 的一个自定义类
整理了一篇 NSCache 看这里
用于管理图片的内存缓存
存取方式如下 :
// 取
[self.memCache objectForKey:key];
// 存
[self.memCache setObject:diskImage forKey:key cost:cost];
2 : 串行队列 ioQueue
图片的磁盘操作都是在这个串行队列同步执行
防止多线程同时访问同一个文件引起崩溃
例如 :
dispatch_sync(self.ioQueue, ^{
imageData = [self diskImageDataBySearchingAllPathsForKey:key];
});
dispatch_sync(_ioQueue, ^{
self.fileManager = [NSFileManager new];
});
3 :磁盘路径 diskCachePath
存放图片的磁盘路径
SDImageCache 的初始化:
SDWebImage 下载的图片有一个统一的目录
就是在 app 磁盘根目录的/Library/Caches
创建自己的命名空间 default
在default
下添加文件夹com.hackemist.SDWebImageCache.default
话说 hackemist
是名字叫 mist(薄雾) 的黑客吗?不明觉厉😲
文件夹下每个图片的名字是用图片的下载链接生成的 MD5 串
所以一张图片的链接大概是这样 :
/Library/Caches/default/com.hackemist.SDWebImageCache.default/9eac3e9559cf1c405943aa16ed59f395.jpg
路径生成是在 SDImageCache 第一次初始化时
同时也初始化了其他属性
源码如下(省略了无关部分):
- (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];
// Create IO serial queue 创建自己的串行队列, 用于同步查找磁盘数据
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
// 缓存配置
_config = [[SDImageCacheConfig alloc] init];
// Init the memory cache
_memCache = [[SDMemoryCache alloc] initWithConfig:_config];
_memCache.name = fullNamespace
// Init the disk cache
if (directory != nil) {
_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
} else {
NSString *path = [self makeDiskCachePath:ns];
_diskCachePath = path;
}
dispatch_sync(_ioQueue, ^{
self.fileManager = [NSFileManager new];
});
}
return self;
}
/** 生成存放图片的文件夹 */
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace {
/** 查找 Library/Caches 文件夹路径*/
NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
/** 拼接自己的子路径 @"default" */
return [paths[0] stringByAppendingPathComponent:fullNamespace];
}
对应文件夹的生成是在第一次存储图片时检查是否有这个文件夹
没有的话就生成 :
if (![self.fileManager fileExistsAtPath:_diskCachePath]) {
[self.fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
文件夹生成成功了
就要准备 存储图片了
图片存储过程 :
当一张图片下载成功后
先存储到内存缓存
再存储到磁盘
存储方法 :
- (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 memory cache is enabled
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) {
// 如果我们没有任何数据来检测图像格式,请检查它是否包含使用PNG或JPEG格式的Alpha通道
// If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
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]) {
[self.fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// 根据图片 key 获取存储路径
NSString *cachePathForKey = [self defaultCachePathForKey:key];
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
// disable iCloud backup
if (self.config.shouldDisableiCloud) {
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
用图片的下载链接生成 MD5 串, 作为图片的名字
生成图片名字的代码如下 :
/** 下载后用于生成每张图片路径 */
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
// 根据 imageKey(也就是下载链接) 生成的 MD5 串
NSString *filename = [self cachedFileNameForKey:key];
return [path stringByAppendingPathComponent:filename];
}
/** 根据下载链接生成 MD5 串 作为图片的名字*/
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
const char *str = key.UTF8String;
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSURL *keyURL = [NSURL URLWithString:key];
NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
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;
}
图片的查找过程 :
有存储就会有查找
SDWebImage 在每次下载图片之前
都会查找内存和磁盘中有没有要下载的图
找到了就直接返回
没找到才去下载
查找的工作也是在 SDImageCache 中进行
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
// 没有 key 就返回了
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 首先检查内存缓存
UIImage *image = [self imageFromMemoryCacheForKey:key];
// 如果用户只想在内存中查找 那么 return
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) {
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;
}
/// 磁盘查找方法
- (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;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
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;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
if (imageData) {
return imageData;
}
}
return nil;
}
/// 内存查找方法
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
return [self.memCache objectForKey:key];
}
图片的删除 :
删除指定图片
- (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();
});
}
});
}