原始碼閱讀:SDWebImage(十一)——SDImageCache

堯少羽發表於2018-06-27

該文章閱讀的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為弱引用。

那這有什麼好處呢?當接收到記憶體警告時,本類會清空快取,雖然快取中的影像物件都被清除了,但是部分影像物件會被UIImageViewUIButton等例項物件所持有,這也就意味著有部分影像物件還在記憶體中。

為了提高效率,可以在快取被清除,但是圖片物件還被其它例項物件所持有的情況下,直接將例項物件持有的影像物件拿來做快取,這樣就比去磁碟中去取效率更高。這就用到了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(一)——從使用入手

原始碼閱讀: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

相關文章