該文章閱讀的SDWebImage的版本為4.3.3。
這個類是SDWebImage
中負責快取相關功能的類。
1.型別定義
/**
這個列舉定義了快取型別
*/
typedef NS_ENUM(NSInteger, SDImageCacheType) {
/**
不快取
*/
SDImageCacheTypeNone,
/**
用磁碟快取
*/
SDImageCacheTypeDisk,
/**
用記憶體快取
*/
SDImageCacheTypeMemory
};
複製程式碼
/**
這個列舉定義了快取選項
*/
typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
/**
預設情況下,如果記憶體中有快取,是不會再去磁碟中查詢的,設定這個選項,可以在查詢記憶體中快取的同時也查詢磁碟中的快取
*/
SDImageCacheQueryDataWhenInMemory = 1 << 0,
/**
預設情況下,同步查詢記憶體快取,非同步訪問磁碟快取,設定這個選項,可以強制同步查詢磁碟快取
*/
SDImageCacheQueryDiskSync = 1 << 1
};
複製程式碼
/**
定義了用於快取查詢結果的回撥block
*/
typedef void(^SDCacheQueryCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType);
複製程式碼
/**
定義了用於快取是否存在的回撥block
*/
typedef void(^SDWebImageCheckCacheCompletionBlock)(BOOL isInCache);
複製程式碼
/**
定義了用於計算磁碟快取大小的回撥block
*/
typedef void(^SDWebImageCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize);
複製程式碼
2.公共屬性
/**
配置物件
*/
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;
複製程式碼
/**
記憶體中分配給快取的最大大小
*/
@property (assign, nonatomic) NSUInteger maxMemoryCost;
複製程式碼
/**
快取最大物件數量
*/
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;
複製程式碼
3.公共方法
- 初始化相關方法
/**
獲取單例物件
*/
+ (nonnull instancetype)sharedImageCache;
複製程式碼
/**
通過指定名稱空間(也就是儲存影像的資料夾名字)來初始化圖片快取類
*/
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns;
複製程式碼
/**
通過指定名稱空間和磁碟目錄來初始化圖片快取類
*/
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;
複製程式碼
- 快取路徑相關方法
/**
根據名稱空間獲取快取儲存的磁碟路徑
*/
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace;
複製程式碼
/**
新增一個只讀的影像快取磁碟路徑,這個方法的意義在於將磁碟中其他目錄下的影像也作為SDWebImage載入影像的快取
*/
- (void)addReadOnlyCachePath:(nonnull NSString *)path;
複製程式碼
/**
獲取指定目錄下,指定金鑰的快取路徑
*/
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path;
複製程式碼
/**
獲取預設目錄下指定金鑰的快取路徑
*/
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key;
複製程式碼
- 儲存相關方法
/**
將影像非同步快取到指定金鑰下的記憶體和磁碟中
*/
- (void)storeImage:(nullable UIImage *)image
forKey:(nullable NSString *)key
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
複製程式碼
/**
將影像非同步快取到指定金鑰下的記憶體中,可以選擇是否快取到磁碟中
*/
- (void)storeImage:(nullable UIImage *)image
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
複製程式碼
/**
將影像非同步快取到指定金鑰下的記憶體中,
可以選擇是否快取到磁碟中,
並且可以直接將影像的資料儲存到磁碟中,
就不必再通過將原影像編碼後獲取影像的資料再儲存到磁碟中,
以節省硬體資源
*/
- (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;
複製程式碼
- 查詢相關方法
/**
非同步查詢影像是否在磁碟中
*/
- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;
複製程式碼
/**
同步查詢影像是否在磁碟中
*/
- (BOOL)diskImageDataExistsWithKey:(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;
複製程式碼
- 刪除相關方法
/**
非同步刪除記憶體和磁碟中快取的指定金鑰的影像
*/
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;
複製程式碼
/**
非同步刪除記憶體中快取的指定金鑰的影像,
並且可以選擇是否也從磁碟中刪除對應快取
*/
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;
複製程式碼
- 清理快取相關方法
/**
清除記憶體中所有快取的影像
*/
- (void)clearMemory;
複製程式碼
/**
非同步清除磁碟中所有的快取影像
*/
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;
複製程式碼
/**
非同步清除磁碟中過期的快取影像
*/
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;
複製程式碼
- 快取資訊相關方法
/**
獲取磁碟中快取佔用的大小
*/
- (NSUInteger)getSize;
複製程式碼
/**
獲取磁碟中快取影像的數量
*/
- (NSUInteger)getDiskCount;
複製程式碼
/**
非同步獲取磁碟中快取佔用的大小
*/
- (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock;
複製程式碼
4.私有巨集
/**
利用GCD的訊號量實現加鎖的效果
*/
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
複製程式碼
/**
利用GCD的訊號量實現解鎖的效果
*/
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
複製程式碼
5.私有函式
/**
獲取影像物件記憶體開銷
*/
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
#if SD_MAC
// 如果是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
}
複製程式碼
6.私有類SDMemoryCache
這個類是繼承自NSCache
,負責管理影像的記憶體快取。
6.1 類擴充套件
/**
影像快取
*/
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache;
/**
保證快取操作的執行緒安全
*/
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock;
複製程式碼
6.2 實現
- (void)dealloc {
// 移除對記憶體警告通知的監聽
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
複製程式碼
- (instancetype)init {
self = [super init];
if (self) {
// 初始化屬性
self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
self.weakCacheLock = dispatch_semaphore_create(1);
// 新增通知監聽記憶體警告
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didReceiveMemoryWarning:)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
}
return self;
}
複製程式碼
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
// 監聽到記憶體警告後移除掉所有的快取
[super removeAllObjects];
}
複製程式碼
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
// 呼叫父類方法賦值
[super setObject:obj forKey:key cost:g];
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 (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 (key) {
// 從弱快取中移除
LOCK(self.weakCacheLock);
[self.weakCache removeObjectForKey:key];
UNLOCK(self.weakCacheLock);
}
}
複製程式碼
- (void)removeAllObjects {
// 呼叫父類方法移除
[super removeAllObjects];
// 移除弱快取
LOCK(self.weakCacheLock);
[self.weakCache removeAllObjects];
UNLOCK(self.weakCacheLock);
}
複製程式碼
6.3 分析
這個類本身就是繼承自NSCache
具有快取功能,但是還是新增了一個NSMapTable
型別的類擴充套件屬性weakCache
進行二次快取。
為什麼還要再通過屬性再快取一遍?關鍵就在於weakCache
屬性它是NSMapTable
型別的,可以設定其value
為弱引用。
那這有什麼好處呢?當接收到記憶體警告時,本類會清空快取,雖然快取中的影像物件都被清除了,但是部分影像物件會被UIImageView
、UIButton
等例項物件所持有,這也就意味著有部分影像物件還在記憶體中。
為了提高效率,可以在快取被清除,但是圖片物件還被其它例項物件所持有的情況下,直接將例項物件持有的影像物件拿來做快取,這樣就比去磁碟中去取效率更高。這就用到了weakCache
屬性,因為weakCache
屬性的value
為弱引用,所以只要還有例項物件持有,就可以通過weakCache
屬性找到對應影像物件。
7.類擴充套件屬性
/**
影像快取物件
*/
@property (strong, nonatomic, nonnull) SDMemoryCache *memCache;
/**
磁碟快取路徑
*/
@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;
複製程式碼
8.實現
- 生命週期方法
+ (nonnull instancetype)sharedImageCache {
// 獲取單例物件
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
複製程式碼
- (instancetype)init {
// 以default為指定名稱空間進行初始化
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];
// 生成用於讀寫的序列佇列
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
// 生成快取配置物件
_config = [[SDImageCacheConfig alloc] init];
// 生成記憶體快取物件
_memCache = [[SDMemoryCache alloc] init];
_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;
}
複製程式碼
- (void)dealloc {
// 移除對通知的監聽
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
複製程式碼
- 快取路徑方法
- (void)addReadOnlyCachePath:(nonnull NSString *)path {
// 如果沒有自定義路徑陣列物件就生成一個
if (!self.customPaths) {
self.customPaths = [NSMutableArray new];
}
// 如果要新增的路徑不在陣列中就新增進去
if (![self.customPaths containsObject:path]) {
[self.customPaths addObject:path];
}
}
複製程式碼
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
// 根據金鑰獲取快取檔名字
NSString *filename = [self cachedFileNameForKey:key];
// 返回快取檔案的路徑
return [path stringByAppendingPathComponent:filename];
}
複製程式碼
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key {
// 獲取當前快取目錄下指定金鑰的快取檔案的路徑
return [self cachePathForKey:key inPath:self.diskCachePath];
}
複製程式碼
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
// 獲取傳入金鑰的UTF8編碼
const char *str = key.UTF8String;
// 如果獲取失敗就設定為空
if (str == NULL) {
str = "";
}
// 進行MD5加密
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
// 獲取金鑰的URL物件
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;
}
複製程式碼
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace {
// 獲取Caches目錄
NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
// 建立Caches目錄下的路徑
return [paths[0] stringByAppendingPathComponent:fullNamespace];
}
複製程式碼
- 儲存方法
- (void)storeImage:(nullable UIImage *)image
forKey:(nullable NSString *)key
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
// 預設是快取到磁碟中的
[self storeImage:image imageData:nil forKey:key toDisk:YES completion:completionBlock];
}
複製程式碼
- (void)storeImage:(nullable UIImage *)image
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
// 直接呼叫全能方法
[self storeImage:image imageData:nil forKey:key toDisk:toDisk completion:completionBlock];
}
複製程式碼
- (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) {
// 如果有透明度就是PNG型別,否則就是JPEG型別
SDImageFormat format;
if (SDCGImageRefContainsAlpha(image.CGImage)) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
// 對影像物件編碼獲取影像資料
data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
}
// 將影像資料儲存到磁碟中
[self _storeImageDataToDisk:data forKey:key];
}
// 如果有完成回撥block就主執行緒非同步呼叫一下
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
// 如果不需要快取到磁碟就直接回撥block
if (completionBlock) {
completionBlock();
}
}
}
複製程式碼
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
// 影像物件和金鑰是必傳的
if (!imageData || !key) {
return;
}
// 非同步序列佇列
dispatch_sync(self.ioQueue, ^{
// 將影像資料儲存到磁碟中
[self _storeImageDataToDisk:imageData forKey:key];
});
}
複製程式碼
- (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];
}
// 根據金鑰獲取要儲存到的磁碟路徑
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// 轉換成url物件
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];
}
}
複製程式碼
- 查詢方法
- (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);
});
}
});
}
複製程式碼
- (BOOL)diskImageDataExistsWithKey:(nullable NSString *)key {
// 如果沒有金鑰就返回NO
if (!key) {
return NO;
}
// 同步序列佇列
__block BOOL exists = NO;
dispatch_sync(self.ioQueue, ^{
// 根據傳入的金鑰判斷是否存在
exists = [self _diskImageDataExistsWithKey:key];
});
// 返回存在情況
return exists;
}
複製程式碼
- (BOOL)_diskImageDataExistsWithKey:(nullable NSString *)key {
// 如果沒有金鑰就返回NO
if (!key) {
return NO;
}
// 判斷快取路徑是否存在
BOOL exists = [self.fileManager fileExistsAtPath:[self defaultCachePathForKey:key]];
// 如果不存在就刪除檔案字尾後再查詢一遍
if (!exists) {
exists = [self.fileManager fileExistsAtPath:[self defaultCachePathForKey:key].stringByDeletingPathExtension];
}
// 返回存在情況
return exists;
}
複製程式碼
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
// 從記憶體快取物件中查詢指定金鑰對應的影像物件是否存在
return [self.memCache objectForKey:key];
}
複製程式碼
- (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;
}
// 如果記憶體中沒有,就查詢磁碟中是否有快取
image = [self imageFromDiskCacheForKey:key];
// 返回影像物件
return image;
}
複製程式碼
- (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;
}
複製程式碼
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
// 獲取金鑰對應的影像資料
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
// 返回金鑰對應的影像物件
return [self diskImageForKey:key data:data];
}
複製程式碼
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data {
// 如果有資料才解碼
if (data) {
// 呼叫編解碼管理器解碼影像資料
UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
// 根據金鑰縮放影像物件
image = [self scaledImageForKey:key image:image];
// 如果設定了需要解壓影像物件
if (self.config.shouldDecompressImages) {
// 解壓影像物件
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
}
// 返回影像物件
return image;
} else {
// 如果沒有影像資料就直接返回空
return nil;
}
}
複製程式碼
- (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
// 將函式封裝成方法
return SDScaledImageForKey(key, image);
}
複製程式碼
- (NSOperation *)queryCacheOperationForKey:(NSString *)key done:(SDCacheQueryCompletedBlock)doneBlock {
// 呼叫下面的萬能方法
return [self queryCacheOperationForKey:key options:0 done:doneBlock];
}
複製程式碼
- (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];
// 是否只查詢記憶體快取:從記憶體中獲取到了快取並且沒有設定需要查詢磁碟快取
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
// 建立操作物件
NSOperation *operation = [NSOperation new];
// 建立一個查詢磁碟快取的block
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
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];
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);
});
}
}
}
};
// 如果設定了同步查詢,就直接呼叫查詢block,否則就序列佇列非同步呼叫
if (options & SDImageCacheQueryDiskSync) {
queryDiskBlock();
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
複製程式碼
- 移除快取方法
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion {
// 預設磁碟中的快取也刪除
[self removeImageForKey:key fromDisk:YES withCompletion:completion];
}
複製程式碼
- (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();
}
}
複製程式碼
- 記憶體快取物件設定方法
這些方法都是對記憶體快取物件屬性setting和getting的二次封裝
- (void)setMaxMemoryCost:(NSUInteger)maxMemoryCost {
self.memCache.totalCostLimit = maxMemoryCost;
}
- (NSUInteger)maxMemoryCost {
return self.memCache.totalCostLimit;
}
- (NSUInteger)maxMemoryCountLimit {
return self.memCache.countLimit;
}
- (void)setMaxMemoryCountLimit:(NSUInteger)maxCountLimit {
self.memCache.countLimit = maxCountLimit;
}
複製程式碼
- 快取清理方法
- (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) {
// 主佇列非同步回撥block
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
});
}
複製程式碼
- (void)deleteOldFiles {
// 呼叫下面的方法
[self deleteOldFilesWithCompletionBlock:nil];
}
複製程式碼
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
// 序列佇列非同步呼叫
dispatch_async(self.ioQueue, ^{
// 獲取磁碟快取目錄
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
// 生成來源key陣列
NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// 獲取磁碟快取目錄下的內容
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
// 獲取過期時間
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
// 建立變數儲存快取檔案
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
// 建立變數儲存當前快取大小
NSUInteger currentCacheSize = 0;
// 建立變數儲存要刪除的快取路徑
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
// 遍歷列舉物件
for (NSURL *fileURL in fileEnumerator) {
// 獲取檔案路徑下的資源值
NSError *error;
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
// 如果出錯,或者沒有資源值,或者是目錄就跳過
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// 如果資源值的時間比過期時間還要早,就新增到待刪連結陣列中
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate 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];
}
// 如果設定了最大快取大小,並且當前快取大小比設定的最大值還要大
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]];
}];
// 遍歷快取
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;
}
}
}
}
// 主佇列非同步回撥block
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
複製程式碼
- (void)backgroundDeleteOldFiles {
// 獲取UIApplication物件
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
// 向系統申請時間
__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
// 清除過期快取
[self deleteOldFilesWithCompletionBlock:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
}
複製程式碼
- 快取資訊方法
- (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;
}
複製程式碼
- (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock {
// 建立磁碟快取目錄url
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;
}
// 主佇列非同步回撥block
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock(fileCount, totalSize);
});
}
});
}
複製程式碼
9.總結
這個類負責影像快取的處理,分為記憶體快取和硬碟快取,功能包括儲存、查詢、刪除等相關操作。邏輯很清晰,也比較簡單。
原始碼閱讀系列:SDWebImage
原始碼閱讀:SDWebImage(二)——SDWebImageCompat
原始碼閱讀:SDWebImage(三)——NSData+ImageContentType
原始碼閱讀:SDWebImage(四)——SDWebImageCoder
原始碼閱讀:SDWebImage(五)——SDWebImageFrame
原始碼閱讀:SDWebImage(六)——SDWebImageCoderHelper
原始碼閱讀:SDWebImage(七)——SDWebImageImageIOCoder
原始碼閱讀:SDWebImage(八)——SDWebImageGIFCoder
原始碼閱讀:SDWebImage(九)——SDWebImageCodersManager
原始碼閱讀:SDWebImage(十)——SDImageCacheConfig
原始碼閱讀:SDWebImage(十一)——SDImageCache
原始碼閱讀:SDWebImage(十二)——SDWebImageDownloaderOperation
原始碼閱讀:SDWebImage(十三)——SDWebImageDownloader
原始碼閱讀:SDWebImage(十四)——SDWebImageManager
原始碼閱讀:SDWebImage(十五)——SDWebImagePrefetcher
原始碼閱讀:SDWebImage(十六)——SDWebImageTransition
原始碼閱讀:SDWebImage(十七)——UIView+WebCacheOperation
原始碼閱讀:SDWebImage(十八)——UIView+WebCache
原始碼閱讀:SDWebImage(十九)——UIImage+ForceDecode/UIImage+GIF/UIImage+MultiFormat
原始碼閱讀:SDWebImage(二十)——UIButton+WebCache
原始碼閱讀:SDWebImage(二十一)——UIImageView+WebCache/UIImageView+HighlightedWebCache