SDWebImage 360°无死角分析之缓存

打算用几篇文章整理一下 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 *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 *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 *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();
            });
        }
    });
}

你可能感兴趣的:(SDWebImage 360°无死角分析之缓存)