IOS源碼解析:SDWeblmage (下)

原創(chuàng):知識(shí)點(diǎn)總結(jié)性文章
創(chuàng)作不易,請(qǐng)珍惜,之后會(huì)持續(xù)更新,不斷完善
個(gè)人比較喜歡做筆記和寫(xiě)總結(jié),畢竟好記性不如爛筆頭哈哈,這些文章記錄了我的IOS成長(zhǎng)歷程,希望能與大家一起進(jìn)步
溫馨提示:由于簡(jiǎn)書(shū)不支持目錄跳轉(zhuǎn),大家可通過(guò)command + F 輸入目錄標(biāo)題后迅速尋找到你所需要的內(nèi)容

續(xù)文見(jiàn)上篇:IOS源碼解析:SDWeblmage(上)

目錄

  • 四、SDImageCache 緩存
    • 1、NSCache 與 NSURLCache
    • 2、SDImageCacheConfig:緩存策略配置對(duì)象
    • 3、SDMemoryCache:內(nèi)存緩存
    • 4、SDDiskCache:磁盤(pán)緩存
    • 5、SDImageCache中提供的屬性
    • 6、單例的創(chuàng)建
    • 7、存入緩存圖片
    • 8、查詢(xún)并獲取圖片
    • 9、清除緩存圖片
  • 五、SDWebImageManager 管理者
    • 1、SDWebImageCombinedOperation:管理讀取緩存和下載圖片
    • 2、獲取url對(duì)應(yīng)的緩存key
    • 3、核心方法:loadImageWithURL
    • 4、查詢(xún)圖片緩存流程
  • 六、UIImageView+WebCache 封裝成分類(lèi)方法
    • 1、調(diào)用真正設(shè)置圖片的方法
    • 2、如果 url 存在則從 Manager 查詢(xún)緩存或者下載圖片
    • 3、如果 url 為 nil 則返回錯(cuò)誤信息
  • Demo
  • 參考文獻(xiàn)

四、SDImageCache 緩存

作用
  • SDImageCache 管理著一個(gè)內(nèi)存緩存和磁盤(pán)緩存(可選),同時(shí)在寫(xiě)入磁盤(pán)緩存時(shí)采取異步執(zhí)行,所以不會(huì)阻塞主線(xiàn)程,影響用戶(hù)體驗(yàn)
  • 以空間換時(shí)間,提升用戶(hù)體驗(yàn):加載同一張圖片,讀取緩存是肯定比遠(yuǎn)程下載的速度要快得多的
  • 減少不必要的網(wǎng)絡(luò)請(qǐng)求,提升性能,節(jié)省流量。一般來(lái)講,同一張圖片的 URL 是不會(huì)經(jīng)常變化的,所以沒(méi)有必要重復(fù)下載。另外,現(xiàn)在的手機(jī)存儲(chǔ)空間都比較大,相對(duì)于流量來(lái),緩存占的那點(diǎn)空間算不了什么
問(wèn)題
  • 從讀取速度和保存時(shí)間上來(lái)考慮,緩存該怎么存?key 怎么定?
  • 內(nèi)存緩存怎么存?
  • 磁盤(pán)緩存怎么存?路徑、文件名怎么定?
  • 使用時(shí)怎么讀取緩存?
  • 什么時(shí)候需要移除緩存?怎么移除?
  • 如何判斷一個(gè)圖片的格式是PNG還是 JPG

1、NSCache 與 NSURLCache

NSCache 的優(yōu)點(diǎn)
  • 類(lèi)似字典,使用方便
  • 線(xiàn)程安全
  • 當(dāng)內(nèi)存不足時(shí)會(huì)自動(dòng)釋放存儲(chǔ)對(duì)象
  • NSCachekey 不會(huì)被拷貝,不需要實(shí)現(xiàn) Copying 協(xié)議
緩存對(duì)象的釋放時(shí)機(jī)
  • totalCostLimit / countLimit 的限制
  • 手動(dòng)調(diào)用 remove
  • APP進(jìn)入到后臺(tái)
  • 收到內(nèi)存警告

a、NSCache 提供的屬性和方法
// 當(dāng)超過(guò)了規(guī)定的總的消耗大小時(shí)候會(huì)自動(dòng)對(duì)內(nèi)存進(jìn)行削減操作
@property NSUInteger totalCostLimit;    
// 同上,但是無(wú)法確定丟棄的對(duì)象順序
@property NSUInteger countLimit;     

// 當(dāng)不確定cost的時(shí)候,調(diào)用該方法,系統(tǒng)會(huì)自動(dòng)將cost設(shè)置為0
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; 
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;

b、實(shí)現(xiàn) NSCacheDelegate 協(xié)議:通知對(duì)象將要被移除了
// 遵循 NSCacheDelegate 協(xié)議
@interface CacheIOP : NSObject<NSCacheDelegate>

@end

@implementation CacheIOP

// 實(shí)現(xiàn)協(xié)議方法
- (void)cache:(NSCache *)cache willEvictObject:(id)obj
{
    NSLog(@"對(duì)象:%@ 將要從緩存中移除了:%@",obj,cache);
}

@end

c、初始化緩存對(duì)象
- (void)initCache
{
    _cacheIOP = [CacheIOP new];
    _cache = [[NSCache alloc] init];
    _cache.countLimit = 5;// 設(shè)置當(dāng)前最大緩存數(shù)量為5
    _cache.delegate = _cacheIOP;// 設(shè)置緩存的委托對(duì)象
}

d、添加緩存對(duì)象
- (void)addCacheObject
{
    // 由于設(shè)置了最大緩存數(shù)量為5,所以在添加第6個(gè)對(duì)象的時(shí)候會(huì)先釋放緩存再添加
    for (int i = 0; i < 10; I++)
    {
        [_cache setObject:[NSString stringWithFormat:@"過(guò)年收到的紅包:%d",i] forKey:[NSString stringWithFormat:@"day%d",I]];
    }
}

輸出結(jié)果

2021-02-04 11:16:14.081684+0800 SDWebImageSourceCodeAnalysis[14481:12313005] 對(duì)象:過(guò)年收到的紅包:0 將要從緩存中移除了:<NSCache: 0x600003a72640>
2021-02-04 11:16:14.081794+0800 SDWebImageSourceCodeAnalysis[14481:12313005] 對(duì)象:過(guò)年收到的紅包:1 將要從緩存中移除了:<NSCache: 0x600003a72640>
2021-02-04 11:16:14.081885+0800 SDWebImageSourceCodeAnalysis[14481:12313005] 對(duì)象:過(guò)年收到的紅包:2 將要從緩存中移除了:<NSCache: 0x600003a72640>
2021-02-04 11:16:14.081975+0800 SDWebImageSourceCodeAnalysis[14481:12313005] 對(duì)象:過(guò)年收到的紅包:3 將要從緩存中移除了:<NSCache: 0x600003a72640>
2021-02-04 11:16:14.082045+0800 SDWebImageSourceCodeAnalysis[14481:12313005] 對(duì)象:過(guò)年收到的紅包:4 將要從緩存中移除了:<NSCache: 0x600003a72640>

e、獲取緩存中的對(duì)象
- (void)getCacheObject
{
    // 前面5個(gè)緩存對(duì)象被釋放掉了,只剩下后面5個(gè)
    for (int i = 0; i < 10; I++)
    {
        NSLog(@"緩存對(duì)象:%@, 索引位置:%d",[_cache objectForKey:[NSString stringWithFormat:@"day%d",i]],i);
    }
}

輸出結(jié)果

2021-02-04 11:20:56.000841+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 緩存對(duì)象:(null), 索引位置:0
2021-02-04 11:20:56.000918+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 緩存對(duì)象:(null), 索引位置:1
2021-02-04 11:20:56.000988+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 緩存對(duì)象:(null), 索引位置:2
2021-02-04 11:20:56.001057+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 緩存對(duì)象:(null), 索引位置:3
2021-02-04 11:20:56.001115+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 緩存對(duì)象:(null), 索引位置:4
2021-02-04 11:20:56.001190+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 緩存對(duì)象:過(guò)年收到的紅包:5, 索引位置:5
2021-02-04 11:20:56.001266+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 緩存對(duì)象:過(guò)年收到的紅包:6, 索引位置:6
2021-02-04 11:20:56.008719+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 緩存對(duì)象:過(guò)年收到的紅包:7, 索引位置:7
2021-02-04 11:20:56.008808+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 緩存對(duì)象:過(guò)年收到的紅包:8, 索引位置:8
2021-02-04 11:20:56.008896+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 緩存對(duì)象:過(guò)年收到的紅包:9, 索引位置:9
進(jìn)入到后臺(tái)后面的緩存對(duì)象也會(huì)全部被自動(dòng)移除
2021-02-04 11:23:25.517456+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 對(duì)象:過(guò)年收到的紅包:5 將要從緩存中移除了:<NSCache: 0x600003b32580>
2021-02-04 11:23:25.517582+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 對(duì)象:過(guò)年收到的紅包:6 將要從緩存中移除了:<NSCache: 0x600003b32580>
2021-02-04 11:23:25.517679+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 對(duì)象:過(guò)年收到的紅包:7 將要從緩存中移除了:<NSCache: 0x600003b32580>
2021-02-04 11:23:25.517773+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 對(duì)象:過(guò)年收到的紅包:8 將要從緩存中移除了:<NSCache: 0x600003b32580>
2021-02-04 11:23:25.517853+0800 SDWebImageSourceCodeAnalysis[14557:12320154] 對(duì)象:過(guò)年收到的紅包:9 將要從緩存中移除了:<NSCache: 0x600003b32580>

f、收到內(nèi)存警告的時(shí)候只會(huì)自動(dòng)移除部分緩存對(duì)象
image.png
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWaring:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];

- (void)didReceiveMemoryWaring:(NSNotification *)notification
{
    NSLog(@"內(nèi)存警告通知:%@",notification);
}

輸出結(jié)果顯示系統(tǒng)只會(huì)自動(dòng)移除部分緩存對(duì)象,并不會(huì)全部移除掉

2021-02-04 11:30:40.021066+0800 SDWebImageSourceCodeAnalysis[14682:12334458] 內(nèi)存警告通知:NSConcreteNotification 0x600001a86f70 {name = UIApplicationDidReceiveMemoryWarningNotification; object = <UIApplication: 0x7fa63ac04fe0>}
2021-02-04 11:30:40.022680+0800 SDWebImageSourceCodeAnalysis[14682:12334936] 對(duì)象:過(guò)年收到的紅包:5 將要從緩存中移除了:<NSCache: 0x6000001f87c0>
2021-02-04 11:30:40.022871+0800 SDWebImageSourceCodeAnalysis[14682:12334936] 對(duì)象:過(guò)年收到的紅包:6 將要從緩存中移除了:<NSCache: 0x6000001f87c0>
2021-02-04 11:30:40.022941+0800 SDWebImageSourceCodeAnalysis[14682:12334936] 對(duì)象:過(guò)年收到的紅包:7 將要從緩存中移除了:<NSCache: 0x6000001f87c0>

g、NSURLCache
? 設(shè)置網(wǎng)絡(luò)緩存策略
// 將緩存策略設(shè)置為不使用緩存直接請(qǐng)求原始數(shù)據(jù)
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];

// 默認(rèn)使用HTTP協(xié)議的緩存策略來(lái)進(jìn)行緩存 NSURLRequestUseProtocolCachePolicy
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
? 和服務(wù)器進(jìn)行比較,判斷資源是否更新
// lastModified:使用日期來(lái)判斷是否服務(wù)端數(shù)據(jù)發(fā)生了改變
if (self.lastModified)
{
    [request setValue:self.lastModified forHTTPHeaderField:@"If-Modified-Since"];
}

// etag: 使用hash值來(lái)判斷是否服務(wù)端數(shù)據(jù)發(fā)生了改變
if (self.etag)
{
    [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 點(diǎn)擊重新請(qǐng)求網(wǎng)絡(luò),并且不使用緩存直接請(qǐng)求原始數(shù)據(jù),以此驗(yàn)證資源是否更新
    [self httpCache];
}
? 從緩存當(dāng)中讀取數(shù)據(jù)
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    if (error)
    {
        NSLog(@"請(qǐng)求網(wǎng)絡(luò)失敗: %@",error);
    }
    else
    {
        self.lastModified = [(NSHTTPURLResponse *)response allHeaderFields][@"Last-Modified"];
        //self.etag = [(NSHTTPURLResponse *)response allHeaderFields][@"Etag"];

        NSData *tempData = data;
        NSString *responseString = [[NSString alloc] initWithData:tempData encoding:NSUTF8StringEncoding];
  
        NSLog(@"lastModified: %@",self.lastModified);
        NSLog(@"response: %@",response);
        NSLog(@"responseString: %@",responseString);
    }
}] resume];
? Cache-Control
  • max-age:緩存時(shí)間
  • public:誰(shuí)都可以緩存
  • no-cache:服務(wù)端進(jìn)行緩存確認(rèn)
  • no-store:禁止使用緩存
緩存目錄
使用數(shù)據(jù)庫(kù)打開(kāi)本地緩存
2021-02-04 14:10:52.902660+0800 SDWebImageSourceCodeAnalysis[16236:12479083] response: <NSHTTPURLResponse: 0x600003d81720> { URL: http://via.placeholder.com/50x50.jpg } { Status Code: 200, Headers {
    "Accept-Ranges" =     (
        bytes
    );
    "Cache-Control" =     (
        "max-age=604800"
    );
    Connection =     (
        "keep-alive"
    );
    "Content-Length" =     (
        807
    );
    "Content-Type" =     (
        "image/jpeg"
    );
    Date =     (
        "Thu, 04 Feb 2021 06:10:47 GMT"
    );
    Etag =     (
        "\"5febdf22-327\""
    );
    Expires =     (
        "Thu, 11 Feb 2021 06:10:47 GMT"
    );
    "Last-Modified" =     (
        "Wed, 30 Dec 2020 02:00:02 GMT"
    );
    Server =     (
        "nginx/1.6.2"
    );
    "X-Cache" =     (
        L1
    );
} }
2021-02-04 14:10:52.902762+0800 SDWebImageSourceCodeAnalysis[16236:12479083] lastModified: Wed, 30 Dec 2020 02:00:02 GMT
2021-02-04 14:10:52.902844+0800 SDWebImageSourceCodeAnalysis[16236:12479083] responseString: (null)
// 304表示服務(wù)端資源并沒(méi)有更新,所以從本地緩存中加載數(shù)據(jù)
response: <NSHTTPURLResponse: 0x6000019b6d40> { URL: http://via.placeholder.com/50x50.jpg } { Status Code: 304, Headers {
    "Cache-Control" =     (
        "max-age=604800"
    );

2、SDImageCacheConfig:緩存策略配置對(duì)象

枚舉:以什么方式來(lái)計(jì)算圖片的過(guò)期時(shí)間
typedef NS_ENUM(NSUInteger, SDImageCacheConfigExpireType)
{
    // 圖片最近訪(fǎng)問(wèn)的時(shí)間
    SDImageCacheConfigExpireTypeAccessDate,

    // 默認(rèn):圖片最近修改的時(shí)間
    SDImageCacheConfigExpireTypeModificationDate,

    // 圖片的創(chuàng)建時(shí)間
    SDImageCacheConfigExpireTypeCreationDate,
};
控制開(kāi)關(guān)
// 默認(rèn)緩存策略配置
@property (nonatomic, class, readonly, nonnull) SDImageCacheConfig *defaultCacheConfig;

// 是否應(yīng)該取消iCloud備份,默認(rèn)是YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;

// 是否使用內(nèi)存緩存,默認(rèn)是YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;

// 是否開(kāi)啟SDMemoryCache內(nèi)部維護(hù)的一張圖片弱引用表
// 開(kāi)啟的好處是當(dāng)收到內(nèi)存警告,SDMemoryCache會(huì)移除圖片的緩存
@property (assign, nonatomic) BOOL shouldUseWeakMemoryCache;

// 在進(jìn)入應(yīng)用程序時(shí)是否刪除過(guò)期的磁盤(pán)數(shù)據(jù)
@property (assign, nonatomic) BOOL shouldRemoveExpiredDataWhenEnterBackground;
配置選項(xiàng)
// 硬盤(pán)圖片讀取的配置選項(xiàng)
@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions;

// 把圖片存入硬盤(pán)的配置選項(xiàng),默認(rèn)NSDataWritingAtomic原子操作
@property (assign, nonatomic) NSDataWritingOptions diskCacheWritingOptions;
過(guò)期時(shí)間和限制大小
// 圖片最大的緩存時(shí)間,默認(rèn)1星期
// 在清除緩存的時(shí)候會(huì)先把緩存時(shí)間過(guò)期的圖片清理掉再清除圖片到總緩存大小在最大占用空間一半以下
@property (assign, nonatomic) NSTimeInterval maxDiskAge;

// 能夠占用的最大磁盤(pán)空間
@property (assign, nonatomic) NSUInteger maxDiskSize;

// 能夠占用的最大內(nèi)存空間
@property (assign, nonatomic) NSUInteger maxMemoryCost;

// 緩存能夠保存的key-value個(gè)數(shù)的最大數(shù)量
@property (assign, nonatomic) NSUInteger maxMemoryCount;

// 硬盤(pán)緩存圖片過(guò)期時(shí)間的計(jì)算方式,默認(rèn)是最近修改的時(shí)間
@property (assign, nonatomic) SDImageCacheConfigExpireType diskCacheExpireType;

// 存儲(chǔ)圖片到硬盤(pán)的文件管理者
@property (strong, nonatomic, nullable) NSFileManager *fileManager;

3、SDMemoryCache:內(nèi)存緩存

a、屬性
// 多線(xiàn)程鎖保證多線(xiàn)程環(huán)境下weakCache數(shù)據(jù)安全
SD_LOCK_DECLARE(_weakCacheLock);

// 弱引用表
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache;

b、初始化
? 最簡(jiǎn)單的初始化方法
- (instancetype)init
{
    self = [super init];
    if (self)
    {
        _config = [[SDImageCacheConfig alloc] init];
        [self commonInit];
    }
    return self;
}
? 使用自定義的緩存策略配置進(jìn)行初始化
- (instancetype)initWithConfig:(SDImageCacheConfig *)config
{
    self = [super init];
    if (self)
    {
        _config = config;
        [self commonInit];
    }
    return self;
}
? 真正的初始化方法

當(dāng)收到內(nèi)存警告,內(nèi)存緩存雖然被清理,但是有些圖片已經(jīng)被其他對(duì)象強(qiáng)引用著。這時(shí)weakCache維持這些圖片的弱引用,如果需要獲取這些圖片就不用去硬盤(pán)獲取了。

- (void)commonInit
{
    SDImageCacheConfig *config = self.config;
    self.totalCostLimit = config.maxMemoryCost;
    self.countLimit = config.maxMemoryCount;

    [config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxMemoryCost)) options:0 context:SDMemoryCacheContext];
    [config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxMemoryCount)) options:0 context:SDMemoryCacheContext];

    // 初始化弱引用表
    //NSPointerFunctionsWeakMemory,對(duì)值進(jìn)行弱引用,不會(huì)對(duì)引用計(jì)數(shù)+1
    self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
    SD_LOCK_INIT(_weakCacheLock);

    // 監(jiān)聽(tīng)內(nèi)存警告通知
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(didReceiveMemoryWarning:)
                                                 name:UIApplicationDidReceiveMemoryWarningNotification
                                               object:nil];
}
? 當(dāng)收到內(nèi)存警告通知,移除內(nèi)存中緩存的圖片
- (void)didReceiveMemoryWarning:(NSNotification *)notification
{
    // 僅僅移除內(nèi)存中緩存的圖片,仍然保留weakCache,維持對(duì)被強(qiáng)引用著的圖片的訪(fǎng)問(wèn)
    [super removeAllObjects];
}

c、存入弱引用表

SDMemoryCache繼承自NSCacheNSCache可以設(shè)置totalCostLimit來(lái)限制緩存的總成本消耗,所以我們?cè)谔砑泳彺娴臅r(shí)候需要通過(guò)cost來(lái)指定緩存對(duì)象消耗的成本。SDImageCache用圖片的像素點(diǎn)(寬縮放比例)來(lái)計(jì)算圖片的消耗成本。

- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g
{
    [super setObject:obj forKey:key cost:g];
    
    if (!self.config.shouldUseWeakMemoryCache)
    {
        return;
    }
    
    if (key && obj)
    {
        // 存入弱引用表
        SD_LOCK(_weakCacheLock);
        [self.weakCache setObject:obj forKey:key];
        SD_UNLOCK(_weakCacheLock);
    }
}

d、把通過(guò)弱引用表獲取的圖片添加到內(nèi)存緩存中
- (id)objectForKey:(id)key
{
    id obj = [super objectForKey:key];
    if (!self.config.shouldUseWeakMemoryCache)
    {
        return obj;
    }
    if (key && !obj)
    {
        // 檢查弱引用表
        SD_LOCK(_weakCacheLock);
        obj = [self.weakCache objectForKey:key];
        SD_UNLOCK(_weakCacheLock);
        if (obj)
        {
            // 把通過(guò)弱引用表獲取的圖片添加到內(nèi)存緩存中
            NSUInteger cost = 0;
            if ([obj isKindOfClass:[  1UIImage class]]) {
                cost = [(UIImage *)obj sd_memoryCost];
            }
            [super setObject:obj forKey:key cost:cost];
        }
    }
    return obj;
}

4、SDDiskCache:磁盤(pán)緩存

a、使用MD5處理URL,生成文件名

如果進(jìn)入沙盒查看緩存的圖片,可以發(fā)現(xiàn)文件名是用過(guò)md5的格式來(lái)命名。將圖片數(shù)據(jù)存儲(chǔ)到磁盤(pán)(沙盒)時(shí),需要提供一個(gè)包含文件名的路徑,這個(gè)文件名是一個(gè)對(duì) key 進(jìn)行 MD5 處理后生成的字符串。

static inline NSString * _Nonnull SDDiskCacheFileNameForKey(NSString * _Nullable key)
{
    const char *str = key.UTF8String;

    // 計(jì)算key的md5值
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSURL *keyURL = [NSURL URLWithString:key];
    NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
    
    // md5值拼接文件后綴
    // X 表示以十六進(jìn)制形式輸出
    // 02 表示不足兩位,前面補(bǔ)0輸出;超過(guò)兩位,不影響
    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;
}

b、把圖片資源存入磁盤(pán)
- (void)setData:(NSData *)data forKey:(NSString *)key
{
    // 如果還沒(méi)有緩存目錄,通過(guò)fileManager生成緩存目錄
    if (![self.fileManager fileExistsAtPath:self.diskCachePath])
    {
        [self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    
    // 獲取key對(duì)應(yīng)的完整緩存路徑
    NSString *cachePathForKey = [self cachePathForKey:key];
    // 轉(zhuǎn)換成URL
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    // 把數(shù)據(jù)存入路徑保存到硬盤(pán)
    [data writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
    
    // 禁止iCloud備份
    if (self.config.shouldDisableiCloud)
    {
        // 給文件添加到運(yùn)行存儲(chǔ)到iCloud屬性
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

5、SDImageCache中提供的屬性

枚舉:緩存圖片的方式
typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions)
{
    // 當(dāng)內(nèi)存有圖片,查詢(xún)內(nèi)存緩存
    SDImageCacheQueryMemoryData = 1 << 0,

    // 同步的方式來(lái)獲取內(nèi)存緩存(默認(rèn)異步)
    SDImageCacheQueryMemoryDataSync = 1 << 1,

    // 同步的方式來(lái)獲取硬盤(pán)緩存(默認(rèn)異步)
    SDImageCacheQueryDiskDataSync = 1 << 2,

    // 縮小大圖(>60M)
    SDImageCacheScaleDownLargeImages = 1 << 3,

    // 避免解碼圖片
    SDImageCacheAvoidDecodeImage = 1 << 4,
};
公開(kāi)的屬性
// 緩存策略配置對(duì)象
@property (nonatomic, copy, nonnull, readonly) SDImageCacheConfig *config;

// 使用SDMemoryCache(繼承自NSCache)來(lái)實(shí)現(xiàn)內(nèi)存緩存
@property (nonatomic, strong, readonly, nonnull) id<SDMemoryCache> memoryCache;

// 使用SDDiskCache來(lái)實(shí)現(xiàn)磁盤(pán)緩存
@property (nonatomic, strong, readonly, nonnull) id<SDDiskCache> diskCache;

// 獲取圖片默認(rèn)的磁盤(pán)緩存路徑
@property (nonatomic, copy, nonnull, readonly) NSString *diskCachePath;
單例和初始化方法
// 暴露的單例對(duì)象
@property (nonatomic, class, readonly, nonnull) SDImageCache *sharedImageCache;

// 默認(rèn)的磁盤(pán)緩存目錄
@property (nonatomic, class, readwrite, null_resettable) NSString *defaultDiskCacheDirectory;

// 指定命名空間,圖片存到對(duì)應(yīng)的沙盒目錄中
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns;

// 指定命名空間和沙盒目錄
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nullable NSString *)directory;

// 指定命名空間、沙盒目錄、緩存策略配置
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nullable NSString *)directory
                                   config:(nullable SDImageCacheConfig *)config 
圖片緩存路徑
// 指定key,獲取圖片的緩存路徑
- (nullable NSString *)cachePathForKey:(nullable NSString *)key;
進(jìn)行緩存圖片
// 把圖片二進(jìn)制數(shù)據(jù)存入內(nèi)存
- (void)storeImageToMemory:(nullable UIImage*)image
                    forKey:(nullable NSString *)key;

// 把圖片二進(jìn)制數(shù)據(jù)存入硬盤(pán)
- (void)storeImageDataToDisk:(nullable NSData *)imageData
                      forKey:(nullable NSString *)key;

// 異步緩存圖片到內(nèi)存和磁盤(pán)
- (void)storeImage:(nullable UIImage *)image
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;
檢查是否存在緩存圖片
// 異步的方式查詢(xún)硬盤(pán)中是否有key對(duì)應(yīng)的緩存圖片
- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDImageCacheCheckCompletionBlock)completionBlock;

// 同步的方式查詢(xún)硬盤(pán)中是否有key對(duì)應(yīng)的緩存圖片
- (BOOL)diskImageDataExistsWithKey:(nullable NSString *)key;
查詢(xún)并獲取緩存圖片
// 同步的方式獲取硬盤(pán)緩存的圖片二進(jìn)制數(shù)據(jù)
- (nullable NSData *)diskImageDataForKey:(nullable NSString *)key;

// 異步的方式來(lái)獲取硬盤(pán)緩存的圖片二進(jìn)制數(shù)據(jù)
- (void)diskImageDataQueryForKey:(nullable NSString *)key completion:(nullable SDImageCacheQueryDataCompletionBlock)completionBlock;

// 異步的方式來(lái)獲取硬盤(pán)緩存的圖片
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;

// 異步的方式來(lái)獲取硬盤(pán)緩存的圖片
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;

// 同步的方式來(lái)獲取內(nèi)存緩存的圖片
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key;

// 同步的方式獲取硬盤(pán)緩存的圖片
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key;

// 同步的方式,先查詢(xún)內(nèi)存中有沒(méi)有緩存的圖片,如果沒(méi)有再查詢(xún)硬盤(pán)中有沒(méi)有緩存的圖片
- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key;
移除緩存中的圖片
// 異步的方式移除緩存中的圖片,包括內(nèi)存和硬盤(pán)
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;

// 異步的方式移除緩存中的圖片,包括內(nèi)存和硬盤(pán)(可選,fromDisk為YES移除硬盤(pán)緩存)
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;

// 移除內(nèi)存中的圖片
- (void)removeImageFromMemoryForKey:(nullable NSString *)key;

// 移除磁盤(pán)中的圖片
- (void)removeImageFromDiskForKey:(nullable NSString *)key;
清除緩存
// 清除內(nèi)存緩存
- (void)clearMemory;

// 異步方式清除硬盤(pán)緩存
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;

// 異步方式清除過(guò)期的圖片
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;
獲取緩存信息
// 同步方式計(jì)算緩存目錄的大小
- (NSUInteger)totalDiskSize;

// 同步方式計(jì)算緩存的圖片數(shù)量
- (NSUInteger)totalDiskCount;

// 異步的方式獲取緩存圖片數(shù)量和大小
- (void)calculateSizeWithCompletionBlock:(nullable SDImageCacheCalculateSizeBlock)completionBlock;
私有屬性
// 內(nèi)存緩存
@property (nonatomic, strong, readwrite, nonnull) id<SDMemoryCache> memoryCache;

// 磁盤(pán)緩存
@property (nonatomic, strong, readwrite, nonnull) id<SDDiskCache> diskCache;

// 緩存策略配置
@property (nonatomic, copy, readwrite, nonnull) SDImageCacheConfig *config;

// 磁盤(pán)緩存路徑
@property (nonatomic, copy, readwrite, nonnull) NSString *diskCachePath;

// 訪(fǎng)問(wèn)操作硬盤(pán)緩存時(shí)用到的串行隊(duì)列
@property (nonatomic, strong, nullable) dispatch_queue_t ioQueue;

6、單例的創(chuàng)建

單例的創(chuàng)建最終會(huì)調(diào)用的方法
// 
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nullable NSString *)directory
                                   config:(nullable SDImageCacheConfig *)config
{
    if ((self = [super init]))
    {
        ...
    }
}
? 初始化一個(gè)串行的dispatch_queue_t
_ioQueue = dispatch_queue_create("com.hackemist.SDImageCache", DISPATCH_QUEUE_SERIAL);
? 初始化緩存策略配置對(duì)象
if (!config)
{
    config = SDImageCacheConfig.defaultCacheConfig;
}
_config = [config copy];
? 初始化內(nèi)存緩存對(duì)象
_memoryCache = [[config.memoryCacheClass alloc] initWithConfig:_config];
? 初始化磁盤(pán)
// 初始化磁盤(pán)緩存路徑
if (!directory)
{
    directory = [self.class defaultDiskCacheDirectory];
}
_diskCachePath = [directory stringByAppendingPathComponent:ns];

// 初始化磁盤(pán)緩存對(duì)象
_diskCache = [[config.diskCacheClass alloc] initWithCachePath:_diskCachePath config:_config];
? 監(jiān)聽(tīng)通知來(lái)清除過(guò)期的圖片緩存數(shù)據(jù)
// 當(dāng)應(yīng)用終止的時(shí)候,清除老數(shù)據(jù)
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil];

// 當(dāng)應(yīng)用進(jìn)入后臺(tái)的時(shí)候,在后臺(tái)刪除老數(shù)據(jù)
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];

7、存入緩存圖片

在存儲(chǔ)緩存數(shù)據(jù)時(shí),先計(jì)算圖片像素大小,并存儲(chǔ)到內(nèi)存緩存中去,然后如果需要存到磁盤(pán)(沙盒)中,就開(kāi)啟異步線(xiàn)程將圖片的二進(jìn)制數(shù)據(jù)存儲(chǔ)到磁盤(pán)(沙盒)中。

a、把一張圖片存入緩存的具體實(shí)現(xiàn)
  • image:緩存的圖片對(duì)象
  • imageData:緩存的圖片數(shù)據(jù)
  • key:緩存對(duì)應(yīng)的key
  • toDisk:是否緩存到瓷片
  • completionBlock:緩存完成回調(diào)
- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
          toMemory:(BOOL)toMemory
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock
{
    ...
}
? 如果允許內(nèi)存緩存,先把圖片緩存到內(nèi)存
if (toMemory && self.config.shouldCacheImagesInMemory)
{
    // 計(jì)算緩存數(shù)據(jù)的大小(圖片像素大小)
    NSUInteger cost = image.sd_memoryCost;
    // 并存儲(chǔ)到內(nèi)存緩存中去
    [self.memoryCache setObject:image forKey:key cost:cost];
}
? 在一個(gè)線(xiàn)性隊(duì)列中做磁盤(pán)緩存操作

一般圖片的大小都不會(huì)很小,對(duì)圖片進(jìn)行編碼過(guò)程中也會(huì)產(chǎn)出一些開(kāi)銷(xiāo)不小的臨時(shí)對(duì)象。在子線(xiàn)程中添加自動(dòng)釋放池,可以提前釋放這些對(duì)象,緩解內(nèi)存壓力。

// 如果需要存儲(chǔ)到沙盒的話(huà),就異步執(zhí)行磁盤(pán)緩存操作
dispatch_async(self.ioQueue, ^{
    @autoreleasepool
    {
        ...
    }
}
? 獲取圖片的類(lèi)型GIF/PNG等

如果需要在存儲(chǔ)之前將傳進(jìn)來(lái)的 image 轉(zhuǎn)成 NSData,而不是直接使用傳入的imageData,那么就要針對(duì) iOS 系統(tǒng)下,按不同的圖片格式來(lái)轉(zhuǎn)成對(duì)應(yīng)的 NSData 對(duì)象。那么圖片格式是怎么判斷的呢?這里是根據(jù)是否有 alpha 通道以及圖片數(shù)據(jù)的前 8 位字節(jié)來(lái)判斷是不是 PNG 圖片,不是 PNG 的話(huà)就按照 JPG 來(lái)處理。

SDImageFormat format = image.sd_imageFormat;
if (format == SDImageFormatUndefined)
{
    if (image.sd_isAnimated)
    {
        format = SDImageFormatGIF;
    }
    else
    {
        // 如果 imageData 為 nil,就根據(jù) image 是否有 alpha 通道來(lái)判斷圖片是否是 PNG 格式的
        // 如果 imageData 不為 nil,就根據(jù) imageData 的前 8 位字節(jié)來(lái)判斷是不是 PNG 格式的
        // 因?yàn)?PNG 圖片有一個(gè)唯一簽名,前 8 位字節(jié)是(十進(jìn)制): 137 80 78 71 13 10 26 10
        format = [SDImageCoderHelper CGImageContainsAlpha:image.CGImage] ? SDImageFormatPNG : SDImageFormatJPEG;
    }
}
? 根據(jù)指定的SDImageFormat把圖片進(jìn)行編碼,得到可以存儲(chǔ)的二進(jìn)制數(shù)據(jù)
data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
? 把處理好了的數(shù)據(jù)存入磁盤(pán)
[self _storeImageDataToDisk:data forKey:key];
? 在主線(xiàn)程調(diào)用回調(diào)閉包
if (completionBlock)
{
    dispatch_async(dispatch_get_main_queue(), ^{
        completionBlock();
    });
}

b、把圖片資源存入磁盤(pán)
- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key
{
    if (!imageData || !key)
    {
        return;
    }
    
    [self.diskCache setData:imageData forKey:key];
}

8、查詢(xún)并獲取圖片

memoryCache 中去找,如果找到了對(duì)應(yīng)的圖片(一個(gè) UIImage 對(duì)象),就直接回調(diào) doneBlock,并直接返回。 如果內(nèi)存緩存中沒(méi)有找到對(duì)應(yīng)的圖片,就開(kāi)啟異步隊(duì)列,調(diào)用 diskImageDataBySearchingAllPathsForKey 讀取磁盤(pán)緩存,讀取成功之后,再保存到內(nèi)存緩存,最后再回到主隊(duì)列,回調(diào) doneBlock

a、在緩存中查詢(xún)對(duì)應(yīng)key的數(shù)據(jù)
  • key:要查詢(xún)的key
  • doneBlock:查詢(xún)結(jié)束以后的Block
  • return:返回做查詢(xún)操作的Block
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock
{
    ...
}
? 先檢查內(nèi)存緩存,如果找到了對(duì)應(yīng)的圖片就回調(diào) doneBlock,并直接返回
UIImage *image;
if (queryCacheType != SDImageCacheTypeDisk)
{
    image = [self imageFromMemoryCacheForKey:key];
}
? 如果內(nèi)存緩存中沒(méi)有找到對(duì)應(yīng)的圖片,開(kāi)啟異步隊(duì)列,讀取硬盤(pán)緩存
NSOperation *operation = [NSOperation new];
? 從磁盤(pán)獲取圖片,這一步包含了圖片解碼
// 在一個(gè)自動(dòng)釋放池中處理圖片從磁盤(pán)加載
@autoreleasepool
{
    NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
    UIImage *diskImage;
    if (image)
    {
        diskImage = image;
    }
}
? 把從磁盤(pán)取出的緩存圖片加入內(nèi)存緩存中
if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory)
{
    NSUInteger cost = diskImage.sd_memoryCost;
    [self.memoryCache setObject:diskImage forKey:key cost:cost];
}
? 圖片處理完成以后回到主隊(duì)列回調(diào)Block
if (doneBlock)
{
    if (shouldQueryDiskSync)
    {
        doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
    }
    else
    {
        dispatch_async(dispatch_get_main_queue(), ^{
            doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
        });
    }
}

b、根據(jù)key獲取緩存在內(nèi)存中的圖片
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key
{
    return [self.memoryCache objectForKey:key];
}

c、根據(jù)指定的key獲取存儲(chǔ)在磁盤(pán)上的數(shù)據(jù)
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context
{
    ...
}
? 從磁盤(pán)中獲取到緩存圖片
NSData *data = [self diskImageDataForKey:key];
UIImage *diskImage = [self diskImageForKey:key data:data options:options context:context];
? 將圖片保存到內(nèi)存
if (diskImage && self.config.shouldCacheImagesInMemory && shouldCacheToMomery)
{
    NSUInteger cost = diskImage.sd_memoryCost;
    [self.memoryCache setObject:diskImage forKey:key cost:cost];
}

9、清除緩存圖片

每新加載一張圖片,就會(huì)新增一份緩存,時(shí)間一長(zhǎng),磁盤(pán)上的緩存只會(huì)越來(lái)越多,所以我們需要定期清除部分緩存。清掃磁盤(pán)緩存有兩個(gè)指標(biāo):一是緩存有效期,二是緩存體積最大限制。值得注意的是,清掃磁盤(pán)緩存和清空磁盤(pán)緩存是兩個(gè)不同的概念,清空是刪除整個(gè)緩存目錄,清掃只是刪除部分緩存文件。

a、清空緩存
? 清空內(nèi)存緩存
- (void)clearMemory
{
    [self.memoryCache removeAllObjects];
}
? 清空磁盤(pán)緩存
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion
{
    dispatch_async(self.ioQueue, ^{
        [self.diskCache removeAllData];
        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion();
            });
        }
    });
}

b、應(yīng)用進(jìn)入后臺(tái)的時(shí)候,調(diào)用這個(gè)方法清除緩存圖片
- (void)applicationDidEnterBackground:(NSNotification *)notification
{
    ...
}
? 如果backgroundTask對(duì)應(yīng)的時(shí)間結(jié)束了,任務(wù)還沒(méi)有處理完成則直接終止任務(wù)
UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
    
    // 當(dāng)任務(wù)非正常終止的時(shí)候,做清理工作
    [application endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}];
? 圖片清理結(jié)束以后,處理完成終止任務(wù)
[self deleteOldFilesWithCompletionBlock:^{
    [application endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}];

c、當(dāng)應(yīng)用終止的時(shí)候,清除老數(shù)據(jù)
- (void)applicationWillTerminate:(NSNotification *)notification
{
    [self deleteOldFilesWithCompletionBlock:nil];
}

d、當(dāng)應(yīng)用終止或者進(jìn)入后臺(tái)都回調(diào)用這個(gè)方法來(lái)清除緩存圖片
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock
{
    dispatch_async(self.ioQueue, ^{
        // 移除過(guò)期數(shù)據(jù)
        [self.diskCache removeExpiredData];
        
        // 執(zhí)行完畢,主線(xiàn)程回調(diào)
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

e、這里會(huì)根據(jù)圖片存儲(chǔ)時(shí)間來(lái)清理圖片。默認(rèn)是一周,從最老的圖片開(kāi)始清理

清掃磁盤(pán)緩存的邏輯是,先遍歷所有緩存文件,并根據(jù)文件的修改時(shí)間來(lái)刪除過(guò)期的文件,同時(shí)記錄剩下的文件的屬性和總體積大小,如果設(shè)置了 maxCacheAge 屬性的話(huà),接下來(lái)就把剩下的文件按修改時(shí)間從小到大排序(最早的排最前面),最后再遍歷這個(gè)文件數(shù)組,一個(gè)一個(gè)刪,直到總體積小于 desiredCacheSize 為止,也就是 maxCacheSize 的一半。

- (void)removeExpiredData
{
    ...
}
? 獲取磁盤(pán)緩存的默認(rèn)根目錄
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
? 獲取文件迭代器和過(guò)期時(shí)間
// 創(chuàng)建文件迭代器
// 第二個(gè)參數(shù)制定了需要獲取的屬性集合
// 第三個(gè)參數(shù)表示不迭代隱藏文件
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                           includingPropertiesForKeys:resourceKeys
                                                              options:NSDirectoryEnumerationSkipsHiddenFiles
                                                         errorHandler:NULL];

// 根據(jù)文件的修改時(shí)間來(lái)刪除過(guò)期的文件
NSDate *expirationDate = (self.config.maxDiskAge < 0) ? nil: [NSDate dateWithTimeIntervalSinceNow:-self.config.maxDiskAge];
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
// 同時(shí)記錄剩下的文件的屬性和總體積大小
NSUInteger currentCacheSize = 0;
? 獲取指定url對(duì)應(yīng)文件
// 刪除比指定日期更老的圖片
// 記錄文件的大小,以提供給后面刪除使用
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
// 遍歷所有緩存文件
for (NSURL *fileURL in fileEnumerator)
{
    NSError *error;
    // 獲取指定url對(duì)應(yīng)文件
    NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
    
    // 如果是文件夾則返回
    if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue])
    {
        continue;
    }
    ...
}
? 如果修改日期大于指定日期,則加入要移除的數(shù)組里
// 獲取指定url文件對(duì)應(yīng)的修改日期
NSDate *modifiedDate = resourceValues[cacheContentDateKey];
// 如果修改日期大于指定日期,則加入要移除的數(shù)組里
if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate])
{
    [urlsToDelete addObject:fileURL];
    continue;
}
? 獲取指定的url對(duì)應(yīng)的文件的大小,并且把url與對(duì)應(yīng)大小存入一個(gè)字典中
// 同時(shí)記錄剩下的文件的屬性和總體積大小
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
cacheFiles[fileURL] = resourceValues;
? 刪除所有最后修改日期大于指定日期的所有文件
for (NSURL *fileURL in urlsToDelete)
{
    [self.fileManager removeItemAtURL:fileURL error:nil];
}
? 如果我們當(dāng)前緩存的大小超過(guò)了默認(rèn)大小,則按照日期刪除,直到緩存大小<默認(rèn)大小的一半
NSUInteger maxDiskSize = self.config.maxDiskSize;
if (maxDiskSize > 0 && currentCacheSize > maxDiskSize)
{
    const NSUInteger desiredCacheSize = maxDiskSize / 2;
    
    // 接下來(lái)就把剩下的文件按修改時(shí)間從小到大排序(最早的排最前面)
    NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                             usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                 return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
                                                             }];
    
    // 迭代刪除緩存,直到緩存大小是默認(rèn)緩存大小的一半
    for (NSURL *fileURL in sortedFiles)
    {
        if ([self.fileManager removeItemAtURL:fileURL error:nil])
        {
            NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            
            // 總的緩存大小減去當(dāng)前要?jiǎng)h除文件的大小
            currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
            
            if (currentCacheSize < desiredCacheSize)
            {
                break;
            }
        }
    }
}

五、SDWebImageManager 管理者

初始化SDImageCache和SDWebImageDownloader對(duì)象
- (nonnull instancetype)initWithCache:(nonnull id<SDImageCache>)cache loader:(nonnull id<SDImageLoader>)loader
{
    if ((self = [super init]))
    {
        _imageCache = cache;
        _imageLoader = loader;
        
        // 用于保存加載失敗的url集合
        _failedURLs = [NSMutableSet new];
        SD_LOCK_INIT(_failedURLsLock);
        
        // 用于保存當(dāng)前正在加載的Operation
        _runningOperations = [NSMutableSet new];
        SD_LOCK_INIT(_runningOperationsLock);
    }
    return self;
}

1、SDWebImageCombinedOperation:管理讀取緩存和下載圖片

a、屬性
// 用來(lái)取消當(dāng)前加載任務(wù)的
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;

// 用來(lái)取消讀取緩存操作
@property (strong, nonatomic, readwrite, nullable) id<SDWebImageOperation> cacheOperation;

// 管理者
@property (weak, nonatomic, nullable) SDWebImageManager *manager;

b、cancel 方法:取消緩存任務(wù)或者加載任務(wù)
- (void)cancel
{
    @synchronized(self)
    {
        if (self.isCancelled)
        {
            return;
        }

        self.cancelled = YES;
        if (self.cacheOperation)
        {
            [self.cacheOperation cancel];
            self.cacheOperation = nil;
        }
        if (self.loaderOperation)
        {
            [self.loaderOperation cancel];
            self.loaderOperation = nil;
        }
        [self.manager safelyRemoveOperationFromRunning:self];
    }
}

2、獲取url對(duì)應(yīng)的緩存key

- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url
{
    if (!url)
    {
        return @"";
    }
    
    NSString *key;
    // 如果有實(shí)現(xiàn)根據(jù)指定的url獲取key的Block,則用這個(gè)方式獲取key
    id<SDWebImageCacheKeyFilter> cacheKeyFilter = self.cacheKeyFilter;
    if (cacheKeyFilter)
    {
        key = [cacheKeyFilter cacheKeyForURL:url];
    }
    // 否則直接用url的絕對(duì)值為key
    else
    {
        key = url.absoluteString;
    }
    
    return key;
}

3、核心方法:loadImageWithURL

a、UIImageView等這種分類(lèi)都默認(rèn)通過(guò)調(diào)用這個(gè)方法來(lái)獲取數(shù)據(jù)
  • url:圖片的url地址
  • options:獲取圖片的屬性
  • progressBlock:加載進(jìn)度回調(diào)
  • completedBlock:加載完成回調(diào)
  • return:返回一個(gè)加載的載體對(duì)象以便提供給后面取消刪除等
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(SDWebImageOptions)options
                                          context:(nullable SDWebImageContext *)context
                                         progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                        completed:(nonnull SDInternalCompletionBlock)completedBlock
{
    ...
    return operation;
}

b、判斷傳入的url類(lèi)型
// 如果傳入的url是NSString格式的則轉(zhuǎn)換為NSURL類(lèi)型再處理
if ([url isKindOfClass:NSString.class])
{
    url = [NSURL URLWithString:(NSString *)url];
}

// 如果url不是NSURL類(lèi)型的對(duì)象則置為nil
if (![url isKindOfClass:NSURL.class])
{
    url = nil;
}

c、綁定一個(gè)CombinedOperation對(duì)象
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;

d、判斷是否是曾經(jīng)下載失敗過(guò)的url
BOOL isFailedUrl = NO;
if (url)
{
    SD_LOCK(_failedURLsLock);
    isFailedUrl = [self.failedURLs containsObject:url];
    SD_UNLOCK(_failedURLsLock);
}

e、如果這個(gè) url 曾經(jīng)下載失敗過(guò)
// 如果這個(gè) url 曾經(jīng)下載失敗過(guò),并且沒(méi)有設(shè)置 SDWebImageRetryFailed,就回調(diào) completedBlock 直接返回
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl))
{
    NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
    NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
    [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url];
    return operation;
}

f、把加載圖片操作添加到runningOperations中
// 里面是所有正在做圖片加載過(guò)程的operation的集合
SD_LOCK(_runningOperationsLock);
[self.runningOperations addObject:operation];
SD_UNLOCK(_runningOperationsLock);

g、獲取圖片配置結(jié)果并開(kāi)始從緩存中加載圖片
// 獲取圖片配置結(jié)果
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];

// 開(kāi)始從緩存中加載圖片
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];

4、查詢(xún)圖片緩存流程

- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                 url:(nonnull NSURL *)url
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context
                            progress:(nullable SDImageLoaderProgressBlock)progressBlock
                           completed:(nullable SDInternalCompletionBlock)completedBlock
{
    ...
}
a、創(chuàng)建緩存任務(wù)
// 檢查是否應(yīng)該查詢(xún)緩存
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
if (shouldQueryCache)
{
    // 根據(jù)url獲取key
    NSString *key = [self cacheKeyForURL:url context:context];

    // 給 SDWebImageCombinedOperation 的緩存任務(wù)賦值
    // imageCache 的類(lèi)型為SDImageCache,調(diào)用其根據(jù)key查詢(xún)緩存圖片的方法
    operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType)
    {
        ...
    }
}

六、UIImageView+WebCache 封裝成分類(lèi)方法

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                   context:(nullable SDWebImageContext *)context
                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock
{
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                             context:context
                       setImageBlock:nil
                            progress:progressBlock
                           completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
                               if (completedBlock) {
                                   completedBlock(image, error, cacheType, imageURL);
                               }
                           }];
}

1、調(diào)用真正設(shè)置圖片的方法

sd_internalSetImageWithURL...
a、判斷當(dāng)前控件上是否有其他任務(wù),如果有就取消掉
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
self.sd_latestOperationKey = validOperationKey;
// 調(diào)用了[Operation cancel]
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
self.sd_imageURL = url;

b、如果當(dāng)前options不是延遲設(shè)置Placeholder就在主線(xiàn)程中設(shè)置占位圖
if (!(options & SDWebImageDelayPlaceholder))
{
    dispatch_main_async_safe(^{
        [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
    });
}

2、如果 url 存在則從 Manager 查詢(xún)緩存或者下載圖片

a、重置下載進(jìn)度
NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
if (imageProgress)
{
    imageProgress.totalUnitCount = 0;
    imageProgress.completedUnitCount = 0;
}
b、檢查并啟動(dòng)加載指示器(加載轉(zhuǎn)圈動(dòng)畫(huà))
[self sd_startImageIndicator];
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
c、開(kāi)始計(jì)算下載進(jìn)度
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
    if (imageProgress)
    {
        imageProgress.totalUnitCount = expectedSize;
        imageProgress.completedUnitCount = receivedSize;
    }
    
    if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)])
    {
        double progress = 0;
        if (expectedSize != 0)
        {
            progress = (double)receivedSize / expectedSize;
        }
        progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
        dispatch_async(dispatch_get_main_queue(), ^{
            [imageIndicator updateIndicatorProgress:progress];
        });
    }
    
    if (progressBlock)
    {
        progressBlock(receivedSize, expectedSize, targetURL);
    }
};
d、調(diào)用 SDWebImageManager 的 loadImageWithURL 方法開(kāi)始加載圖片
// 獲取Manager(可由用戶(hù)自定義)
SDWebImageManager *manager = context[SDWebImageContextCustomManager];

id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {

    // 下載完成停止指示器
    if (finished)
    {
        [self sd_stopImageIndicator];
    }

    if (image)
    {
        // 圖片下載成功,設(shè)置image
        targetImage = image;
        targetData = data;
    }
    else if (options & SDWebImageDelayPlaceholder)
    {
        // 圖片下載失敗,設(shè)置 placeholder
        targetImage = placeholder;
        targetData = nil;
    }
    
    dispatch_main_async_safe(^{
        [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
    });
}];
e、將當(dāng)前的 operation 和 key 設(shè)置到NSMapTable中
[self sd_setImageLoadOperation:operation forKey:validOperationKey];

typedef NSMapTable<NSString *, id<SDWebImageOperation>> SDOperationsDictionary;

- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key
{
    if (key)
    {
        [self sd_cancelImageLoadOperationWithKey:key];
        if (operation)
        {
            SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
            @synchronized (self)
            {
                [operationDictionary setObject:operation forKey:key];
            }
        }
    }
}

3、如果 url 為 nil 則返回錯(cuò)誤信息

// 停止指示器
[self sd_stopImageIndicator];

dispatch_main_async_safe(^{
    if (completedBlock)
    {
        // 直接回調(diào) completedBlock,返回錯(cuò)誤信息
        NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
        completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
    }
});

Demo

Demo在我的Github上,歡迎下載。
SourceCodeAnalysisDemo

參考文獻(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

推薦閱讀更多精彩內(nèi)容