原創(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ì)象
-
NSCache
的key
不會(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ì)象
[[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:禁止使用緩存
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
繼承自NSCache
。NSCache
可以設(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