iOS緩存 NSCache詳解及SDWebImage緩存策略源碼分析

你要知道的NSCache都在這里

轉載請注明出處 http://www.lxweimin.com/p/239226822bc6

本篇文章首先會詳細講解NSCache的基本使用,NSCacheFoundation框架提供的緩存類的實現,使用方式類似于可變字典,由于NSMutableDictionary的存在,很多人在實現緩存時都會使用可變字典,但NSCache在實現緩存功能時比可變字典更方便,最重要的是它是線程安全的,而NSMutableDictionary不是線程安全的,在多線程環境下使用NSCache是更好的選擇。接著,會通過源碼講解SDWebImage的緩存策略。最后簡要補充了第三方YYCache的實現思路。

NSCache

NSCache的使用很方便,提供了類似可變字典的使用方式,但它比可變字典更適用于實現緩存,最重要的原因為NSCache是線程安全的,使用NSMutableDictionary自定義實現緩存時需要考慮加鎖和釋放鎖,NSCache已經幫我們做好了這一步。其次,在內存不足時NSCache會自動釋放存儲的對象,不需要手動干預,如果是自定義實現需要監聽內存狀態然后做進一步的刪除對象的操作。還有一點就是NSCache的鍵key不會被復制,所以key不需要實現NSCopying協議。

上面講解的三點就是NSCache相比于NSMutableDictionary實現緩存功能的優點,在需要實現緩存時應當優先考慮使用NSCache

首先看一下NSCache提供的屬性和相關方法:

//名稱
@property (copy) NSString *name;

//NSCacheDelegate代理
@property (nullable, assign) id<NSCacheDelegate> delegate;

//通過key獲取value,類似于字典中通過key取value的操作
- (nullable ObjectType)objectForKey:(KeyType)key;

//設置key、value
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost

/*
設置key、value
cost表示obj這個value對象的占用的消耗?可以自行設置每個需要添加進緩存的對象的cost值
這個值與后面的totalCostLimit對應,如果添加進緩存的cost總值大于totalCostLimit就會自動進行刪除
感覺在實際開發中直接使用setObject:forKey:方法就可以解決問題了
*/
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;

//根據key刪除value對象
- (void)removeObjectForKey:(KeyType)key;

//刪除保存的所有的key-value
- (void)removeAllObjects;

/*
NSCache能夠占用的消耗?的限制
當NSCache緩存的對象的總cost值大于這個值則會自動釋放一部分對象直到占用小于該值
非嚴格限制意味著如果保存的對象超出這個大小也不一定會被刪除
這個值就是與前面setObject:forKey:cost:方法對應
*/
@property NSUInteger totalCostLimit;    // limits are imprecise/not strict

/*
緩存能夠保存的key-value個數的最大數量
當保存的數量大于該值就會被自動釋放
非嚴格限制意味著如果超出了這個數量也不一定會被刪除
*/
@property NSUInteger countLimit;    // limits are imprecise/not strict
/*
這個值與NSDiscardableContent協議有關,默認為YES
當一個類實現了該協議,并且這個類的對象不再被使用時意味著可以被釋放
*/
@property BOOL evictsObjectsWithDiscardedContent;

@end

//NSCacheDelegate協議
@protocol NSCacheDelegate <NSObject>
@optional
//上述協議只有這一個方法,緩存中的一個對象即將被刪除時被回調
- (void)cache:(NSCache *)cache willEvictObject:(id)obj;
@end

通過接口可以看出,NSCache提供的方法都很簡單,屬性的意義也很明確,接下來舉一個簡單的栗子:

//定義一個CacheTest類實現NSCacheDelegate代理
@interface CacheTest: NSObject <NSCacheDelegate>

@end

@implementation CacheTest

//當緩存中的一個對象即將被刪除時會回調該方法
- (void)cache:(NSCache *)cache willEvictObject:(id)obj
{
    NSLog(@"Remove Object %@", obj);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //創建一個NSCache緩存對象
        NSCache *cache = [[NSCache alloc] init];
        //設置緩存中的對象個數最大為5個
        [cache setCountLimit:5];
        //創建一個CacheTest類作為NSCache對象的代理
        CacheTest *ct = [[CacheTest alloc] init];
        //設置代理
        cache.delegate = ct;
        
        //創建一個字符串類型的對象添加進緩存中,其中key為Test
        NSString *test = @"Hello, World";
        [cache setObject:test forKey:@"Test"];
        
        //遍歷十次用于添加
        for (int i = 0; i < 10; i++)
        {
            [cache setObject:[NSString stringWithFormat:@"Hello%d", i] forKey:[NSString stringWithFormat:@"World%d", i]];
            NSLog(@"Add key:%@  value:%@ to Cache", [NSString stringWithFormat:@"Hello%d", i], [NSString stringWithFormat:@"World%d", i]);
        }
        
        for (int i = 0; i < 10; i++)
        {
            NSLog(@"Get value:%@ for key:%@", [cache objectForKey:[NSString stringWithFormat:@"World%d", i]], [NSString stringWithFormat:@"World%d", i]);
        }
        
        [cache removeAllObjects];
        
        for (int i = 0; i < 10; i++)
        {
            NSLog(@"Get value:%@ for key:%@", [cache objectForKey:[NSString stringWithFormat:@"World%d", i]], [NSString stringWithFormat:@"World%d", i]);
        }
        
        NSLog(@"Test %@", test);
    }
    
    return 0;
}

輸出結果如下:

//第一個for循環輸出
Add key:Hello0  value:World0 to Cache
Add key:Hello1  value:World1 to Cache
Add key:Hello2  value:World2 to Cache
Add key:Hello3  value:World3 to Cache
Remove Object Hello, World
Add key:Hello4  value:World4 to Cache
Remove Object Hello0
Add key:Hello5  value:World5 to Cache
Remove Object Hello1
Add key:Hello6  value:World6 to Cache
Remove Object Hello2
Add key:Hello7  value:World7 to Cache
Remove Object Hello3
Add key:Hello8  value:World8 to Cache
Remove Object Hello4
Add key:Hello9  value:World9 to Cache
//第二個for循環輸出
Get value:(null) for key:World0
Get value:(null) for key:World1
Get value:(null) for key:World2
Get value:(null) for key:World3
Get value:(null) for key:World4
Get value:Hello5 for key:World5
Get value:Hello6 for key:World6
Get value:Hello7 for key:World7
Get value:Hello8 for key:World8
Get value:Hello9 for key:World9
//removeAllObjects輸出
Remove Object Hello5
Remove Object Hello6
Remove Object Hello7
Remove Object Hello8
Remove Object Hello9
//最后一個for循環輸出
Get value:(null) for key:World0
Get value:(null) for key:World1
Get value:(null) for key:World2
Get value:(null) for key:World3
Get value:(null) for key:World4
Get value:(null) for key:World5
Get value:(null) for key:World6
Get value:(null) for key:World7
Get value:(null) for key:World8
Get value:(null) for key:World9
//輸出test字符串
Test Hello, World

上面的代碼創建了一個NSCache對象,設置了其最大可緩存對象的個數為5個,從輸出可以看出,當我們要添加第六個對象時NSCache自動刪除了我們添加的第一個對象并觸發了NSCacheDelegate的回調方法,添加第七個時也是同樣的,刪除了緩存中的一個對象才能添加進去。

在第二個for循環中,我們通過key取出所有的緩存對象,前五個對象取出都為nil,因為在添加后面的對象時前面的被刪除了,所以,當我們從緩存中獲取對象時一定要判斷是否為空,我們無法保證緩存中的某個對象不會被刪除。

接著調用了NSCacheremoveAllObjects方法,一旦調用該方法,NSCache就會將其中保存的所有對象都釋放掉,所以,可以看到調用該方法后NSCacheDelegate的回調方法執行了五次,將NSCache中的所有緩存對象都清空了。

在最后一個for循環中,根據key獲取緩存中的對象時可以發現都為空了,因為都被釋放了。

前面還創建了一個字符串的局部變量,在最開始將其加入到了緩存中,后來隨著其他對象的添加,該字符串被緩存釋放了,但由于局部變量對其持有強引用所以使用test還是可以訪問到的,這是最基本的ARC知識,所以,NSCache在釋放一個對象時只是不再指向這個對象,即,該對象的引用計數減一,如果有其他指針指向它,這個對象不會被釋放。

上面就是NSCache的基本用法了,我們只需要設置對象和獲取對象,其他事情NSCache都幫我們做完了,因此,實現緩存功能時,使用NSCache就是我們的不二之選。

再看一個栗子:

- (void)viewWillAppear:(BOOL)animated
{
    self.cache = [[NSCache alloc] init];
    [self.cache setCountLimit:5];
    self.cache.delegate = self;
    [self.cache setObject:@"AA" forKey:@"BBB"];
    [self.cache setObject:@"MMMM" forKey:@"CCC"];
}

- (void)cache:(NSCache *)cache willEvictObject:(id)obj
{
    NSLog(@"REMOVE %@", obj);
}

這是一個有視圖控制器的栗子,我們創建了一個NSCache對象,并在其中添加了對象,當點擊home鍵,程序進入后臺后,可以發現NSCacheDelegate的回調函數觸發了,所以,當程序進入后臺,NSCache對象會自動釋放所有的對象。如果在模擬器上模擬內存警告,也可以發現NSCache會釋放所有的對象。所以NSCache刪除緩存中的對象會在以下情形中發生:

  • NSCache緩存對象自身被釋放

  • 手動調用removeObjectForKey:方法

  • 手動調用removeAllObjects

  • 緩存中對象的個數大于countLimit,或,緩存中對象的總cost值大于totalCostLimit

  • 程序進入后臺后

  • 收到系統的內存警告

SDWebImage的緩存策略

在了解了NSCache的基本使用后,現在來通過SDWebImage的源碼看看它是怎樣進行圖片的緩存操作的。由于篇幅的問題,本文將源碼中的英文注釋刪掉了,有需要的讀者可以對照著注釋源碼查閱本文章。本節內容包括了GCDNSOperation等多線程相關的知識,有疑問的讀者可以查閱本博客iOS多線程——你要知道的GCD都在這里 以及 iOS多線程——你要知道的NSOperation都在這里 相關內容,本文就不再贅述了。

首先看一下官方給的設置圖片后執行時序圖:

SDWebImage執行時序圖

整個執行流程非常清晰明了,本篇文章的重點在第四步、第五步和第八步,關于網絡下載,以后會在講解NSURLSession后進行相關的源碼分析。查看SDWebImage的源碼,與緩存有關的一共有四個文件SDImageCacheConfigSDImageCache,首先看一下SDImageCacheConfig的頭文件:

@interface SDImageCacheConfig : NSObject

//是否壓縮圖片,默認為YES,壓縮圖片可以提高性能,但是會消耗內存
@property (assign, nonatomic) BOOL shouldDecompressImages;

//是否關閉iCloud備份,默認為YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;

//是否使用內存做緩存,默認為YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;

/** 緩存圖片的最長時間,單位是秒,默認是緩存一周
 * 這個緩存圖片最長時間是使用磁盤緩存才有意義
 * 使用內存緩存在前文中講解的幾種情況下會自動刪除緩存對象
 * 超過最長時間后,會將磁盤中存儲的圖片自動刪除
 */
@property (assign, nonatomic) NSInteger maxCacheAge;

//緩存占用最大的空間,單位是字節
@property (assign, nonatomic) NSUInteger maxCacheSize;

@end

NSCacheConfig類可以看得出來就是一個配置類,保存一些緩存策略的信息,沒有太多可以講解的地方,看懂就好,看一下NSCacheConfig.m文件的源碼:

static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

@implementation SDImageCacheConfig

- (instancetype)init {
    if (self = [super init]) {
        _shouldDecompressImages = YES;
        _shouldDisableiCloud = YES;
        _shouldCacheImagesInMemory = YES;
        _maxCacheAge = kDefaultCacheMaxCacheAge;
        _maxCacheSize = 0;
    }
    return self;
}

@end

從上面源碼可以看出相關屬性的默認值,以及maxCacheAge的默認值為一周時間。

接下來,看一下真正執行緩存操作的SDImageCache類的頭文件,接下來的源碼分析都是按照源碼的順序來的,只是分為了幾個小塊,讀者也可以按順序對照源碼一起查看:

//獲取圖片的方式類別枚舉
typedef NS_ENUM(NSInteger, SDImageCacheType) {
    //不是從緩存中拿到的,從網上下載的
    SDImageCacheTypeNone,
    //從磁盤中獲取的
    SDImageCacheTypeDisk,
    //從內存中獲取的
    SDImageCacheTypeMemory
};

//查找緩存完成后的回調塊
typedef void(^SDCacheQueryCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType);
//在緩存中根據指定key查找圖片的回調塊
typedef void(^SDWebImageCheckCacheCompletionBlock)(BOOL isInCache);
//計算磁盤緩存圖片個數和占用內存大小的回調塊
typedef void(^SDWebImageCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize);

上面是一些輔助用的定義,獲取圖片方式的枚舉以及各種情況下的回調塊。

/*
SDWebImage真正執行緩存的類
SDImageCache支持內存緩存,默認也可以進行磁盤存儲,也可以選擇不進行磁盤存儲
*/
@interface SDImageCache : NSObject

#pragma mark - Properties

//SDImageCacheConfig對象,緩存策略的配置
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;

//內存緩存的最大cost,以像素為單位,后面有具體計算方法
@property (assign, nonatomic) NSUInteger maxMemoryCost;

//內存緩存,緩存對象的最大個數
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;

上面這一部分是屬性的聲明,屬性很少,但我們在NSCache中都見過了,首先是SDImageCacheConfig,即前面講解的緩存策略配置,maxMemoryCost其實就是NSCachetotalCostLimit,這里它使用像素為單位進行計算,maxMemoryCountLimit其實就是NSCachecountLimit,需要注意的是SDImageCache繼承自NSObject沒有繼承NSCache,所以它需要保存這些屬性。

#pragma mark - Singleton and initialization

//單例方法用來獲取一個SDImageCache對象
+ (nonnull instancetype)sharedImageCache;

/*
初始化方法,根據指定的namespace創建一個SDImageCache類的對象
這個namespace默認值是default
主要用于磁盤緩存時創建文件夾時作為其名稱使用
*/
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns;

//初始化方法,根據指定namespace以及磁盤緩存的文件夾路徑來創建一個SDImageCache的對象
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;

上面幾個方法就是其初始化方法,提供了類方法用于獲取一個單例對象,使用單例對象就會使用所有的默認配置,下面兩個初始化構造函數提供了兩個接口但真正進行初始化的是最后一個,通過這樣的設計盡可能的抽象出所有共同的部分,簡化代碼,而且思路更清晰。

#pragma mark - Cache paths
//根據fullNamespace構造一個磁盤緩存的文件夾路徑
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace;

/*
添加一個只讀的緩存路徑,以后在查找磁盤緩存時也會從這個路徑中查找
主要用于查找提前添加的圖片
*/
- (void)addReadOnlyCachePath:(nonnull NSString *)path;

上面兩個方法主要用于構造磁盤緩存的文件夾路徑以及添加一個指定路徑到緩存中,以后搜索緩存時也會從這個路徑中查找,這樣設計就提供了可擴展性,如果以后需要修改緩存路徑,只需把之前的路徑添加進來即可。

#pragma mark - Store Ops

/*
根據給定的key異步存儲圖片
image 要存儲的圖片
key 一張圖片的唯一ID,一般使用圖片的URL
completionBlock 完成異步存儲后的回調塊
該方法并不執行任何實際的操作,而是直接調用下面的下面的那個方法
*/
- (void)storeImage:(nullable UIImage *)image
            forKey:(nullable NSString *)key
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;

/*
同上,該方法并不是真正的執行者,而是需要調用下面的那個方法
根據給定的key異步存儲圖片
image 要存儲的圖片
key 唯一ID,一般使用URL
toDisk 是否緩存到磁盤中
completionBlock 緩存完成后的回調塊
*/
- (void)storeImage:(nullable UIImage *)image
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;

/*
根據給定的key異步存儲圖片,真正的緩存執行者
image 要存儲的圖片
imageData 要存儲的圖片的二進制數據即NSData數據
key 唯一ID,一般使用URL
toDisk 是否緩存到磁盤中
completionBlock
*/
- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;
        
/*
根據指定key同步存儲NSData類型的圖片的數據到磁盤中
這是一個同步的方法,需要放在指定的ioQueue中執行,指定的ioQueue在下面會講
imageData 圖片的二進制數據即NSData類型的對象
key 圖片的唯一ID,一般使用URL
*/
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key;

上面幾個方法是用來執行存儲操作的,提供了內存緩存和磁盤緩存的不同存儲方式方法,提供了不同的接口,但真正執行的方法只有一個,這樣的設計方式值得我們學習。

#pragma mark - Query and Retrieve Ops

/*
異步方式根據指定的key查詢磁盤中是否緩存了這個圖片
key 圖片的唯一ID,一般使用URL
completionBlock 查詢完成后的回調塊,這個回調塊默認會在主線程中執行
*/
- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

/**
 * Operation that queries the cache asynchronously and call the completion when done.
 *
 * @param key       The unique key used to store the wanted image
 * @param doneBlock The completion block. Will not get called if the operation is cancelled
 *
 * @return a NSOperation instance containing the cache op
 */
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;

/*
同步查詢內存緩存中是否有ID為key的圖片
key 圖片的唯一ID,一般使用URL
*/
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key;

/*
同步查詢磁盤緩存中是否有ID為key的圖片
key 圖片的唯一ID,一般使用URL
*/
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key;

/*
同步查詢內存緩存和磁盤緩存中是否有ID為key的圖片
key 圖片的唯一ID,一般使用URL
*/
- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key;

上面幾個方法是查詢的方法,提供了豐富的根據圖片key查找的功能。

#pragma mark - Remove Ops

/*
根據給定key異步方式刪除緩存
key 圖片的唯一ID,一般使用URL
completion 操作完成后的回調塊
*/
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;

/*
根據給定key異步方式刪除內存中的緩存
key 圖片的唯一ID,一般使用URL
fromDisk 是否刪除磁盤中的緩存,如果為YES那也會刪除磁盤中的緩存
completion 操作完成后的回調塊
*/
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;

#pragma mark - Cache clean Ops

//刪除所有的內存緩存,即NSCache中的removeAllObjects
- (void)clearMemory;

/*
異步方式清空磁盤中的所有緩存
completion 刪除完成后的回調塊
*/
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;

/*
異步刪除磁盤緩存中所有超過緩存最大時間的圖片,即前面屬性中的maxCacheAge
completionBlock 刪除完成后的回調塊
*/
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;

上面幾個方法是用來刪除緩存中圖片的方法,以及清空內存緩存的方法。

#pragma mark - Cache Info

//獲取磁盤緩存占用的存儲空間大小,單位是字節
- (NSUInteger)getSize;

//獲取磁盤緩存了多少張圖片
- (NSUInteger)getDiskCount;

/*
異步方式計算磁盤緩存占用的存儲空間大小,單位是字節
completionBlock 計算完成后的回調塊
*/
- (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock;

上面幾個方法提供了查詢磁盤緩存占用內存大小以及緩存圖片個數的功能。

#pragma mark - Cache Paths

/*
根據圖片的key以及一個存儲文件夾路徑,構造一個在本地的圖片的路徑
key 圖片的唯一ID,一般使用URL
inPath 本地存儲圖片的文件夾的路徑
比如:圖片URL是http:www.baidu.com/test.png inPath是/usr/local/,那么圖片存儲到本地后的路徑為:/usr/local/test.png
*/
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path;

/*
根據圖片的key獲取一個默認的緩存在本地的路徑
key 圖片的唯一ID,一般使用URL
*/
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key;

@end

上面幾個方法是用來構造圖片保存到磁盤中的路徑的功能。

完整閱讀完上述代碼后,可以發現SDImageCache提供了緩存圖片的增刪查功能,并提供了磁盤緩存路徑相關的一系列功能函數。上面的代碼中的注釋已經詳細解釋了每個函數的功能,這里不再贅述了,接下來看一下具體的實現代碼:

@interface AutoPurgeCache : NSCache
@end

@implementation AutoPurgeCache

- (nonnull instancetype)init {
    self = [super init];
    if (self) {
#if SD_UIKIT
        //收到系統內存警告后直接調用 removeAllObjects 刪除所有緩存對象
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
    }
    return self;
}

- (void)dealloc {
#if SD_UIKIT
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}

@end

首先它定義了一個AutoPurgeCache的類,這個類繼承自NSCache,它只重寫了initdealloc方法,在這兩個方法中進行了添加和刪除系統內存警告通知的監聽操作,目的就是為了在收到系統內存警告后進行內存的清理工作。

繼續看下面的代碼:

FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
#if SD_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
}

上面定義了內聯函數,用于計算每個圖片的cost值,由于不同平臺圖片編碼的方式不同,在iOSwatchOS上一張圖片的實際大小與一個scale縮放值相關,所以需要使用image.size.height * image.scale來計算真正的圖片高度,同理也需要計算真正的寬度,這樣就可以計算出圖片的實際大小,而在macOS下圖片的大小就是實際的寬高乘積。所以,這里使用的cost就是圖片的大小,也即圖片的像素個數。

繼續看下面的代碼:

@interface SDImageCache ()

#pragma mark - Properties
//緩存對象
@property (strong, nonatomic, nonnull) NSCache *memCache;
//磁盤緩存的路徑
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
//自定義緩存查詢路徑,即前面add*方法添加的路徑,都添加到這個數組中
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
/*
專門用來執行IO操作的隊列,這是一個串行隊列
使用串行隊列就解決了很多問題,串行隊列依次執行就不需要加鎖釋放鎖操作來防止多線程下的異常問題
*/
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;

@end

@implementation SDImageCache {
    //文件操作的類
    NSFileManager *_fileManager;
}

這里使用extension又添加了幾個屬性,最重要的就是memCache了,一個NSCache類或子類的對象,真正進行內存緩存的對象。

繼續看下面的代碼:

#pragma mark - Singleton, init, dealloc
//類方法,返回一個單例對象
+ (nonnull instancetype)sharedImageCache {
    //使用GCD構造單例對象
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        //調用默認構造函數
        instance = [self new];
    });
    return instance;
}

/*
默認構造函數,調用initWithNamespace:執行初始化操作
默認的namespace為default,這個屬性就是用來創建一個磁盤緩存存儲文件夾
*/
- (instancetype)init {
    return [self initWithNamespace:@"default"];
}

//根據指定的namespace構造一個磁盤緩存的存儲路徑后調用initWithNamespace:diskCacheDirectory方法完成后續的初始化操作
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns {
    /*
    makeDiskCachePath的目的是為了創建一個磁盤緩存存儲圖片的文件夾
    獲取一個系統沙盒的cache目錄下名稱為ns的文件夾的路徑
    比如:/usr/local/cache/default
    所以namespace的作用就是為了在沙盒的cache目錄下創建一個文件夾時作為它的名稱,以后去磁盤中查找時就有路徑了
    */
    NSString *path = [self makeDiskCachePath:ns];
    return [self initWithNamespace:ns diskCacheDirectory:path];
}

/*
真正執行初始化操作的構造函數
ns 即namespace
directory 即磁盤緩存存儲圖片的文件夾路徑
*/
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory {
    if ((self = [super init])) {
        //構造一個全限定名的namespace
        NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
        
        // Create IO serial queue
        //創建一個串行的專門執行IO操作的隊列
        _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
        
        //構造一個SDImageCacheConfig對象
        _config = [[SDImageCacheConfig alloc] init];
        
        // Init the memory cache
        //創建一個AutoPurgeCache對象,即NSCache的子類
        _memCache = [[AutoPurgeCache alloc] init];
        //指定這個緩存對象的名稱為前面的全限定名
        _memCache.name = fullNamespace;

        // Init the disk cache
        //如果傳入的磁盤緩存的文件夾路徑不為空
        if (directory != nil) {
            //在文件夾路徑后面再創建一個文件夾,名稱為全限定名名稱
            _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
        } else {
            //如果傳入的磁盤緩存文件夾路徑是空的就根據傳入的ns獲取一個沙盒cache目錄下名稱為ns的文件夾路徑
            NSString *path = [self makeDiskCachePath:ns];
            _diskCachePath = path;
        }
        //同步方法在這個IO隊列上進行fileManager的創建工作
        dispatch_sync(_ioQueue, ^{
            _fileManager = [NSFileManager new];
        });

#if SD_UIKIT
        // Subscribe to app events
        //監聽收到系統內存警告的通知,收到后執行clearMemory方法
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(clearMemory)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];
         
         //監聽程序即將終止的通知,收到后執行deleteOldFiles方法

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(deleteOldFiles)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];
        
        //監聽程序進入后臺的通知,收到后執行backgroundDeleteOldFiles方法

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundDeleteOldFiles)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];
#endif
    }

    return self;
}

//析構函數
- (void)dealloc {
    //移除所有通知的監聽器
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    //釋放ioQueue
    SDDispatchQueueRelease(_ioQueue);
}

//檢查當前執行隊列是否為ioQueue
- (void)checkIfQueueIsIOQueue {
    //GCD都是C API所以需要使用C字符串
    const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
    const char *ioQueueLabel = dispatch_queue_get_label(self.ioQueue);
    if (strcmp(currentQueueLabel, ioQueueLabel) != 0) {
        NSLog(@"This method should be called from the ioQueue");
    }
}

上面的構造函數和析構函數都很好理解,主要功能就是創建了NSCache對象以及構造了一個磁盤緩存存儲圖片的文件夾路徑并監聽了一些通知用于清除緩存的操作。

接下里繼續看代碼:

//添加只讀的用戶自行添加的緩存搜索路徑
- (void)addReadOnlyCachePath:(nonnull NSString *)path {
    //如果這個路徑集合為空就創建一個
    if (!self.customPaths) {
        self.customPaths = [NSMutableArray new];
    }
    //如果路徑集合中不包含這個新的路徑就添加
    if (![self.customPaths containsObject:path]) {
        [self.customPaths addObject:path];
    }
}

/*
根據指定的圖片的key和指定文件夾路徑獲取圖片存儲的絕對路徑
首先通過cachedFileNameForKey:方法根據URL獲取一個MD5值作為這個圖片的名稱
接著在這個指定路徑path后面添加這個MD5名稱作為這個圖片在磁盤中的絕對路徑
*/
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
    NSString *filename = [self cachedFileNameForKey:key];
    return [path stringByAppendingPathComponent:filename];
}

/*
該方法與上面的方法一樣,內部調用上面的方法
不過它使用默認的磁盤緩存路徑diskCachePath,就是在構造函數中獲取的沙盒cache下的一個文件夾的路徑
*/
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key {
    return [self cachePathForKey:key inPath:self.diskCachePath];
}

/*
根據圖片的key,即URL構造一個MD5串,添加原來的后綴后作為這個圖片在磁盤中存儲時的名稱
MD5算法保證了不同URL散列出的值不同,也就保證了不同URL圖片的名稱不同
具體算法不在本篇博客的講述范圍,有興趣的讀者自行查閱
*/
- (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);
    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], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]];

    return filename;
}

/*
根據給定的fullNamespace構造一個磁盤緩存存儲圖片的路徑
首先獲取了沙盒下的cache目錄
然后將fullNamespace添加進這個路徑作為cache下的一個文件夾名稱
*/
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace {
    NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    return [paths[0] stringByAppendingPathComponent:fullNamespace];
}

上面的一系列方法提供了構造圖片存儲在磁盤中的絕對路徑的功能,主要就是使用MD5算法散列圖片的URL來創建圖片存儲在磁盤的文件名,并且根據namespace構造一個沙盒cache目錄下的一個路徑。上述函數很簡單,就不再贅述了。

接下來繼續看代碼:

#pragma mark - Store Ops
//存儲圖片到緩存,直接調用下面的下面的方法
- (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 {
    //如果image為nil或image的URL為空直接返回即不執行保存操作
    if (!image || !key) {
        //如果回調塊存在就執行完成回調塊
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    // if memory cache is enabled
    //如果緩存策略指明要進行內存緩存
    if (self.config.shouldCacheImagesInMemory) {
        //根據前面的內聯函數計算圖片的大小作為cost
        NSUInteger cost = SDCacheCostForImage(image);
        //向memCache中添加圖片對象,key即圖片的URL,cost為上面計算的
        [self.memCache setObject:image forKey:key cost:cost];
    }
    
    //如果要保存到磁盤中
    if (toDisk) {
        //異步提交任務到串行的ioQueue中執行
        dispatch_async(self.ioQueue, ^{
            //進行磁盤存儲的具體的操作,使用@autoreleasepool包圍,執行完成后自動釋放相關對象
            //我猜測這么做是為了盡快釋放產生的局部變量,釋放內存
            @autoreleasepool {
                NSData *data = imageData;
                //如果傳入的imageData為空,圖片不為空
                if (!data && image) {
                    // If we do not have any data to detect image format, use PNG format
                    //調用編碼方法,獲取NSData對象
                    //圖片編碼為NSData不在本文的講述范圍,可自行查閱
                    data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:SDImageFormatPNG];
                }
                //調用下面的方法用于磁盤存儲操作
                [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 {
    //判斷圖片NSData數據以及圖片key是否為空,如果為空直接返回
    if (!imageData || !key) {
        return;
    }
    //檢查當前執行隊列是否為ioQueue,如果不是會提示開發者
    [self checkIfQueueIsIOQueue];
    
    //如果構造函數中構造的磁盤緩存存儲圖片路徑的文件夾不存在
    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
        //那就根據這個路徑創建需要的文件夾
        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    
    // get cache Path for image key
    // 根據key獲取默認磁盤緩存存儲路徑下的MD5文件名的文件的絕對路徑
    // 感覺有點繞口。。就是獲取圖片二進制文件在磁盤中的絕對路徑,名稱就是前面使用MD5散列的,路徑就是構造函數默認構造的那個路徑
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    // transform to NSUrl
    // 根據這個絕對路徑創建一個NSURL對象
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    //使用NSFileManager創建一個文件,文件存儲的數據就是imageData
    //到此,圖片二進制數據就存儲在了磁盤中了
    [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
    
    // disable iCloud backup
    if (self.config.shouldDisableiCloud) {
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

上面就是圖片緩存存儲的核心方法了,其實看下來感覺也蠻簡單的,如果要進行內存緩存就直接添加到memCache對象中,如果要進行磁盤緩存,就構造一個路徑,構造一個文件名,然后存儲起來就好了。這里面有幾個重要的點,首先就是@autoreleasepool的使用,其實這里不添加這個autoreleasepool同樣會自動釋放內存,但添加后在這個代碼塊結束后就會立即釋放,不會占用太多內存。其次,對于磁盤寫入的操作是通過一個指定的串行隊列實現的,這樣不管執行多少個磁盤存儲的操作,都必須一個一個的存儲,這樣就可以不用編寫加鎖的操作,可能有讀者會疑惑為什么要進行加鎖,因為并發情況下這些存儲操作都不是線程安全的,很有可能會把路徑修改掉或者產生其他異常行為,但使用了串行隊列就完全不需要考慮加鎖釋放鎖,一張圖片存儲完成才可以進行下一張圖片存儲的操作,這一點值得學習。

接下來繼續看其他源碼:

#pragma mark - Query and Retrieve Ops

//異步方式根據key判斷磁盤緩存中是否存儲了這個圖片,查詢完成后執行回調塊
- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {
    //查詢操作是異步,也放在指定的串行ioQueue中查詢
    dispatch_async(_ioQueue, ^{
        /*
        調用defualtCachePathForKey:方法獲取圖片如果在本地存儲時的絕對路徑
        使用NSFileManager查詢這個絕對路徑的文件是否存在
        */
        BOOL exists = [_fileManager fileExistsAtPath:[self defaultCachePathForKey:key]];

        // 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
        //如果不存在
        if (!exists) {
            //再次去掉后綴名查詢,這個問題可以自行查看上面git的問題
            exists = [_fileManager fileExistsAtPath:[self defaultCachePathForKey:key].stringByDeletingPathExtension];
        }
        
        //查詢完成后,如果存在回調塊,就在主線程執行回調塊并傳入exists
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock(exists);
            });
        }
    });
}

//查詢內存緩存中是否有指定key的緩存數據
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
    //直接調用NSCache的objectForKey:方法查詢
    return [self.memCache objectForKey:key];
}

//根據指定的key獲取磁盤緩存的圖片構造并返回UIImage對象
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key {
    //調用diskImageForKey:方法查詢,這個方法下面會講
    UIImage *diskImage = [self diskImageForKey:key];
    //如果找到了,并且緩存策略使用了內存緩存
    if (diskImage && self.config.shouldCacheImagesInMemory) {
        //計算cost并且將磁盤中獲取的圖片放入到內存緩存中
        NSUInteger cost = SDCacheCostForImage(diskImage);
        //調用NSCache的setObject:forKey:cost方法設置要緩存的對象
        //之所以要設置是因為如果是第一次從磁盤中拿出此時內存緩存中還沒有
        //還有可能是內存緩存中的對象被刪除了,然后在磁盤中找到了,此時也需要設置一下
        //setObject:forKey:cost方法的時間復雜度是常量的,所以哪怕內存中有也無所謂
        [self.memCache setObject:diskImage forKey:key cost:cost];
    }

    return diskImage;
}

//查找內存緩存和磁盤緩存中是否有指定key的圖片
- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key {
    // First check the in-memory cache...
    //首先檢查內存緩存中是否有,有就返回,調用了上面的那個方法
    //實際就是執行了NSCache的 objectForKey:方法
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        return image;
    }
    
    // Second check the disk cache...
    //如果內存緩存中沒有再去磁盤中查找
    image = [self imageFromDiskCacheForKey:key];
    return image;
}

//在磁盤中所有的保存路徑,包括用戶添加的路徑中搜索key對應的圖片數據
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
    //首先在默認存儲路徑中查找,如果有就直接返回
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath];
    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];
    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];
        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];
        if (imageData) {
            return imageData;
        }
    }
    //沒找到返回nil
    return nil;
}

//在磁盤中查找指定key的圖片數據,然后轉換為UIImage對象返回
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
    //調用上面的方法查找所有路徑下是否存在對應key的圖片數據
    NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
    //如果有就解碼解壓縮后返回UIImage對象
    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;
    }
}

//在iOS watchOS下圖片的真實大小與scale有關,這里做一下縮放處理
- (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
    return SDScaledImageForKey(key, image);
}

/*
在緩存中查找指定key的圖片是否存在,完成后執行回調塊
返回一個NSOperation,調用者可以隨時取消查詢
提供這個功能主要是因為在磁盤中查找真的很耗時,調用者可能在一段時間后就不查詢了
這個NSOperation更像是一個標記對象,標記調用者是否取消了查詢操作,完美的利用了NSOperation的cancel方法
*/
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    //如果key為空執行回調塊返回nil
    if (!key) {
        if (doneBlock) {
            //SDImageCacheTypeNone表示沒有緩存數據
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

    // First check the in-memory cache...
    //查找內存緩存中是否存在,調用了前面的方法
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    //如果存在,就在磁盤中查找對應的二進制數據,然后執行回調塊
    if (image) {
        NSData *diskData = nil;
        if (image.images) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        if (doneBlock) {
            //SDImageCacheTypeMemory表示圖片在內存緩存中查找到
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
        //NSOperation為nil
        return nil;
    }
    
    //接下來就需要在磁盤中查找了,由于耗時構造一個NSOperation對象
    //下面是異步方式在ioQueue上進行查詢操作,所以直接就返回了NSOperation對象
    NSOperation *operation = [NSOperation new];
    //異步在ioQueue上查詢
    dispatch_async(self.ioQueue, ^{
        //ioQueue是串行的,而且磁盤操作很慢,有可能還沒開始查詢調用者就取消查詢
        //如果在開始查詢后調用者再取消就沒有用了,只有在查詢前取消才有用
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            //如果是調用者取消查詢不執行回調塊
            return;
        }
        //同理創建一個自動釋放池,
        @autoreleasepool {
            //在磁盤中查找圖片二進制數據,和UIImage對象
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];
            //找到并且需要內存緩存就設置一下
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            //在主線程中執行回調塊
            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    //SDImageCacheTypeDisk表示在磁盤中找到
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });

    return operation;
}

上面的方法提供了內存緩存和磁盤緩存中查找的功能,比較精明的設計就是返回NSOperation對象,這個對象并不代表一個任務,僅僅利用了它的cancel方法和isCancelled屬性,來取消磁盤查詢。上面的代碼也很簡單,就不再贅述啦!

接下來繼續看:

#pragma mark - Remove Ops
//刪除緩存總指定key的圖片,刪除完成后的回調塊completion
//該方法也直接調用了下面的方法,默認也刪除磁盤的數據
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion {
    [self removeImageForKey:key fromDisk:YES withCompletion:completion];
}

//根據指定key刪除圖片數據
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion {
    //圖片key為nil直接返回
    if (key == nil) {
        return;
    }
    //先判斷緩存策略是否有內存緩存,有就刪除內存緩存
    if (self.config.shouldCacheImagesInMemory) {
        //調用NSCache的removeObjectForKey方法
        [self.memCache removeObjectForKey:key];
    }
    //如果要刪除磁盤數據
    if (fromDisk) {
        //異步方式在ioQueue上執行刪除操作
        dispatch_async(self.ioQueue, ^{
            //使用key構造一個默認路徑下的文件存儲的絕對路徑
            //調用NSFileManager刪除該路徑的文件
            [_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
            //有回調塊就在主線程中執行
            if (completion) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completion();
                });
            }
        });
     //不需要刪除磁盤數據并且有回調塊就直接執行
    } else if (completion){
        completion();
    }
    
}

上面的刪除操作也很好理解,內存緩存就直接刪除NSCache對象的數據,磁盤緩存就直接獲取文件的絕對路徑后刪除即可。

繼續看代碼:

# pragma mark - Mem Cache settings

- (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;
}

memCache就是AutoPurgeCache對象,即NSCache的子類,相關設置值的方法直接就設置了NSCache的相關屬性。

繼續看:

#pragma mark - Cache clean Ops
//清除緩存的操作,在收到系統內存警告通知時執行
- (void)clearMemory {
    //調用NSCache方法刪除所有緩存對象
    [self.memCache removeAllObjects];
}

//清空磁盤的緩存,完成后的回調塊completion
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion {
    //使用異步提交在ioQueue中執行
    dispatch_async(self.ioQueue, ^{
        //獲取默認的圖片存儲路徑然后使用NSFileManager刪除這個路徑的所有文件及文件夾
        [_fileManager removeItemAtPath:self.diskCachePath error:nil];
        //刪除以后再創建一個空的文件夾
        [_fileManager createDirectoryAtPath:self.diskCachePath
                withIntermediateDirectories:YES
                                 attributes:nil
                                      error:NULL];
        //完成后有回調塊就在主線程中執行
        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion();
            });
        }
    });
}

//刪除磁盤中老的即超過緩存最長時限maxCacheAge的圖片,直接調用下面的方法
- (void)deleteOldFiles {
    [self deleteOldFilesWithCompletionBlock:nil];
}

//刪除磁盤中老的即超過緩存最長時限maxCacheAge的圖片,完成后回調塊completionBlock
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
    //異步方式在ioQueue上執行
    dispatch_async(self.ioQueue, ^{
        //獲取磁盤緩存存儲圖片的路徑構造為NSURL對象
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        //后面會用到,查詢文件的屬性
        NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

        // This enumerator prefetches useful properties for our cache files.
        //構造一個存儲圖片目錄的迭代器,使用了上面的文件屬性
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];
        //構造過期日期,即當前時間往前maxCacheAge秒的日期
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
        //緩存的文件的字典
        NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
        //當前緩存大小
        NSUInteger currentCacheSize = 0;

        // Enumerate all of the files in the cache directory.  This loop has two purposes:
        //
        //  1. Removing files that are older than the expiration date.
        //  2. Storing file attributes for the size-based cleanup pass.
        //需要刪除的圖片的文件URL
        NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
        //遍歷上面創建的那個目錄迭代器
        for (NSURL *fileURL in fileEnumerator) {
            NSError *error;
            //根據resourcesKeys獲取文件的相關屬性
            NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];

            // Skip directories and errors.
            //有錯誤,然后屬性為nil或者路徑是個目錄就continue
            if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            // Remove files that are older than the expiration date;
            //獲取文件的上次修改日期,即創建日期
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            //如果過期就加進要刪除的集合中
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }

            // Store a reference to this file and account for its total size.
            //獲取文件的占用磁盤的大小
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            //累加總緩存大小
            currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
            cacheFiles[fileURL] = resourceValues;
        }
        //遍歷要刪除的過期的圖片文件URL集合,并刪除文件
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }

        // If our remaining disk cache exceeds a configured maximum size, perform a second
        // size-based cleanup pass.  We delete the oldest files first.
        //如果緩存策略配置了最大緩存大小,并且當前緩存的大小大于這個值則需要清理
        if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
            // Target half of our maximum cache size for this cleanup pass.
            //清理到只占用最大緩存大小的一半
            const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;

            // Sort the remaining cache files by their last modification time (oldest first).
            //根據文件創建的日期排序
            NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                     usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                         return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                     }];

            // Delete files until we fall below our desired cache size.
            //按創建的先后順序遍歷,然后刪除,直到緩存大小是最大值的一半
            for (NSURL *fileURL in sortedFiles) {
                if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;

                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        //執行完成后在主線程執行回調塊
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

#if SD_UIKIT
//在ios下才會有的函數
//寫不動了,就是在后臺刪除。。。自己看看吧。。。唉
- (void)backgroundDeleteOldFiles {
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
    __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        // Clean up any unfinished task business by marking where you
        // stopped or ending the task outright.
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];

    // Start the long-running task and return immediately.
    [self deleteOldFilesWithCompletionBlock:^{
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}
#endif

上面就是刪除磁盤中過期的圖片,以及當緩存大小大于配置的值時,進行緩存清理。

繼續吧:

#pragma mark - Cache Info
//計算磁盤緩存占用空間大小
- (NSUInteger)getSize {
    __block NSUInteger size = 0;
    dispatch_sync(self.ioQueue, ^{
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
        for (NSString *fileName in fileEnumerator) {
            NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
            NSDictionary<NSString *, id> *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
            size += [attrs fileSize];
        }
    });
    return size;
}

//計算磁盤緩存圖片的個數
- (NSUInteger)getDiskCount {
    __block NSUInteger count = 0;
    dispatch_sync(self.ioQueue, ^{
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
        count = fileEnumerator.allObjects.count;
    });
    return count;
}

//同時計算磁盤緩存圖片占用空間大小和緩存圖片的個數,然后調用回調塊,傳入相關參數
- (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock {
    NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
    //異步在ioQueue中執行
    dispatch_async(self.ioQueue, ^{
        NSUInteger fileCount = 0;
        NSUInteger totalSize = 0;

        NSDirectoryEnumerator *fileEnumerator = [_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;
        }

        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock(fileCount, totalSize);
            });
        }
    });
}

@end

上面的方法就是用來計算磁盤中緩存圖片的數量和占用磁盤空間大小。

整個SDWebImage的緩存模塊到此就結束了,閱讀完后可以發現,整個代碼很好理解,但是設計的也很巧妙,各種情況都考慮的很周全,這些都值得我們學習,尤其是所有IO操作使用一個串行隊列來執行,避免加鎖釋放鎖的復雜,還有就是使用NSOperation作為一個標識用來取消耗時的磁盤查詢任務。整個代碼簡潔易懂,接口設計的很完善,是我們學習的榜樣。

補充

最近還研究了一下YYCache的源碼,YYCache包括了內存緩存和磁盤緩存兩部分。

對于內存緩存可以說作者為了提升性能無所不用其極,使用Core Foundation提供的C字典CFMutableDictionaryRef來存儲封裝的緩存對象,并構造了一個雙向鏈表,維護鏈表并使用LRU淘汰算法來剔除超過限制的緩存對象,使用pthread_mutext互斥鎖來保證線程安全,包括釋放對象使用了一個小技巧使得可以在子線程中釋放,而不需要在主線程中執行,直接訪問ivar而不使用getter/setter,一系列的優化方法使得YYCache的內存緩存效率超過了NSCache及其他第三方庫。

對于磁盤緩存,作者參考了NSURLCache的實現及其他第三方的實現,采用文件系統結合SQLite的實現方式,實驗發現對于20KB以上的數據,文件系統的讀寫速度高于SQLite,所以當數據大于20KB時直接將數據保存在文件系統中,在數據庫中保存元數據,并添加索引,數據小于20KB時直接保存在數據庫中,這樣,就能夠快速統計相關數據來實現淘汰。SDWebImage的磁盤緩存使用的只有文件系統。

讀了YYCache源碼讓我明白了,不能一味的迷信蘋果為我們提供的類,為了追求更極致的性能需要做大量的對比試驗來確定技術方案。

讀者可以參考YYCache作者的博客YYCache設計思路,有興趣的讀者可以研究一下其源碼,值得學習的有很多。

備注

由于作者水平有限,難免出現紕漏,如有問題還請不吝賜教。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容