簡(jiǎn)介
SDWebImage
是目前使用最廣泛的第三方圖片處理庫(kù)温算,它不僅能夠異步加載網(wǎng)絡(luò)圖片,還提供了一套圖片緩存管理機(jī)制间影,功能非常強(qiáng)大注竿。我們使用只要一行代碼就可以輕松的實(shí)現(xiàn)圖片異步下載和緩存功能。
本文主要針對(duì)v4.0.0
版本進(jìn)行分析魂贬, 從3.0
升級(jí)4.0
的小伙伴可以查看 SDWebImage 4.0遷移指南巩割;
特性
- 提供
UIImageView
、UIButton
的分類, 支持網(wǎng)絡(luò)圖片的加載與緩存管理 - 一個(gè)異步的圖片下載器
- 異步(內(nèi)存+磁盤)緩存和自動(dòng)過期處理緩存
- 后臺(tái)圖片解壓縮
- 同一個(gè) URL 不會(huì)重復(fù)下載
- 自動(dòng)識(shí)別無效 URL付燥,不會(huì)反復(fù)重試
- 不阻塞主線程
- 高性能
- 使用
GCD
和ARC
支持的圖像格式
- 支持
UIImage(JPEG宣谈,PNG,...)
键科,包括GIF
(從4.0版本開始,依靠FLAnimatedImage來處理動(dòng)畫圖像) -
WebP
格式闻丑,包括動(dòng)畫WebP
(使用WebPsubspec
)
使用
UIImageView+WebCache
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];;
這是UIImageView
的一個(gè)擴(kuò)展方法漩怎,通過這個(gè)方法就已經(jīng)實(shí)現(xiàn)了 圖片異步加載
和緩存機(jī)制
功能了, 是不是超簡(jiǎn)單呢
加載流程
首先: 我們通過時(shí)序圖
,來了解一下框架的基本加載流程~
- 當(dāng)我們的
UIImageView
嗦嗡、UIButton
控件調(diào)用sd_setImageWithURL: ()...
方法 來進(jìn)行加載圖片勋锤; - 框架會(huì)直接調(diào)用
UIView+WebCache
中的sd_internalSetImageWithURL:() ...
, 該方法是UIImageView
和UIButton
的共有拓展方法 - 接下來調(diào)用
SDWebImageManager
類中的loadImageWithURL:() ...
方法,會(huì)根據(jù)提供的圖片URL
加載圖片侥祭,SDWebImageManager
主要負(fù)責(zé)管理SDImageCache
緩存和SDWebImageDownloader
下載器 - 首先進(jìn)入
SDImageCache
類叁执,調(diào)用queryCacheOperationForKey...
在內(nèi)存或者磁盤進(jìn)行查詢,如果有圖片緩存則進(jìn)行回調(diào)展示卑硫, 如果沒有查詢到圖片緩存徒恋,則進(jìn)行下一步下載 - 在未查詢到圖片緩存時(shí),
SDWebImageDownloader
類會(huì)進(jìn)行網(wǎng)絡(luò)下載欢伏,下載成功后進(jìn)行回調(diào)展示入挣,并將下載的圖片緩存到內(nèi)存和磁盤
通俗理解:
根據(jù)Url
在內(nèi)存
中查詢圖片,如果有則展示硝拧,沒有則在磁盤
查詢圖片径筏,查詢到展示, 沒有查詢到在會(huì)通過網(wǎng)絡(luò)下載
進(jìn)行展示障陶。下載完后會(huì)存儲(chǔ)到內(nèi)存和磁盤
滋恬,方便下次直接使用,磁盤查詢和網(wǎng)絡(luò)下載都是異步的,不會(huì)影響主線程
.
源碼分析
UIView+WebCache 加載邏輯
我們了解了基本的加載流程后抱究,接下來我們看看他是如何一步一步實(shí)現(xiàn)的,首先我們來看一下
UIView+WebCache.m
文件中的加載圖片源碼:
/**
@param url 圖片URL
@param placeholder 占位圖
@param options 加載選項(xiàng)(SDWebImageOptions枚舉類型)
@param operationKey 要用作操的key鍵恢氯。如果為nil,將使用類名
@param setImageBlock 自定義Block塊
@param progressBlock 下載進(jìn)度Block塊
@param completedBlock 加載完成Block塊
*/
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
// 這個(gè)方法很強(qiáng)大,用于保障當(dāng)前的加載操作,不會(huì)被之前的加載操作污染(會(huì)將之前的加載操作取消)
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 占位圖設(shè)置
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
if (url) {
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
__weak __typeof(self)wself = self;
// 通過 SDWebImageManager類來進(jìn)行圖片加載
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
[sself sd_removeActivityIndicator];
if (!sself) {
return;
}
// 會(huì)判斷是否是否在主線程,如果在則直接回調(diào),不在則執(zhí)行主線程異步回調(diào)
dispatch_main_async_safe(^{
if (!sself) {
return;
}
// 如果選項(xiàng)為 SDWebImageAvoidAutoSetImage(用戶手動(dòng)設(shè)置), 則Block回調(diào).不會(huì)給ImageView賦值,由用戶自己操作
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
completedBlock(image, error, cacheType, url);
return;
}
else if (image)
{
// 判斷是UIimageView還是UIButton給對(duì)應(yīng)的控件設(shè)置圖像
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
// 刷新視圖布局
[sself sd_setNeedsLayout];
} else {
// 在加載網(wǎng)絡(luò)圖片失敗后,展示展位圖
if ((options & SDWebImageDelayPlaceholder)) {
[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
}
}
// 加載完成回調(diào)
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
// 將 operation添加到 operation字典中
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
}
else
{
// url為nil 則直接進(jìn)行錯(cuò)誤回調(diào)提示
dispatch_main_async_safe(^{
[self sd_removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
UIView+WebCache
為UIImageView
提供了方法擴(kuò)展鼓寺,上面方法作為加載圖片的入口勋拟,邏輯也簡(jiǎn)單清晰;
視圖每次再加載圖片之前, 都會(huì)調(diào)用 sd_cancelImageLoadOperationWithKey:(nullable NSString *)key
方法, 用于取消上一個(gè)URl的加載操作, (比如: TalbeViewCell
中的ImageView
一般都是復(fù)用的,如果快速滑動(dòng), 這個(gè)ImageView會(huì)加載很多次不同的URL, 這個(gè)方法可以保證展示和加載的URL地址是最新的妈候,并且會(huì)將之前的操作取消)敢靡;
這個(gè)方法是UIView+WebCacheOperation
類中提供的一個(gè)方法,我們看下具體實(shí)現(xiàn)
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
// SDOperationsDictionary是一個(gè)字典類型, key是當(dāng)前視圖的字符串, value是視圖加載的操作
SDOperationsDictionary *operationDictionary = [self operationDictionary];
// 通過key獲取到加載操作,operations實(shí)際是一個(gè) SDWebImageCombinedOperation類型,并遵守了<SDWebImageOperation>協(xié)議
id operations = operationDictionary[key];
// 將操作取消掉
if (operations) {
if ([operations isKindOfClass:[NSArray class]]) {
for (id <SDWebImageOperation> operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id<SDWebImageOperation>) operations cancel]; //在協(xié)議方法中實(shí)現(xiàn)取消操作
}
// 并從字典中將操作移除
[operationDictionary removeObjectForKey:key];
}
}
每個(gè)視圖內(nèi)部會(huì)有一個(gè)SDOperationsDictionary
字典, key
是當(dāng)前視圖的字符串, value
是視圖加載的操作, 這個(gè)操作類遵守<SDWebImageOperation>
協(xié)議, 在協(xié)議方法中實(shí)現(xiàn)真正的取消操作
dispatch_main_async_safe(^{})
宏定義
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
該宏可以判斷是否是在主線程,如果在主線程則直接異步執(zhí)行block, 如果不在則調(diào)用 dispatch_async(dispatch_get_main_queue(), block);
回主線程異步執(zhí)行;
SDWebImageManager
SDWebImageManager
是一個(gè)單例管理類苦银,主要用于管理SDWebImageDownloader
以及SDImageCache
啸胧;SDImageCache
類負(fù)責(zé)執(zhí)行圖片的緩存,SDWebImageDownloader
類負(fù)責(zé)圖片的下載任務(wù)幔虏。該對(duì)象將一個(gè)下載器和一個(gè)圖片緩存綁定在一起纺念,并對(duì)外提供兩個(gè)只讀屬性來獲取它們,
@property (weak, nonatomic, nullable) id <SDWebImageManagerDelegate> delegate;
@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;
@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;
我們看到他有個(gè)內(nèi)部有一個(gè) id<SDWebImageManagerDelegate>
delegate屬性想括,SDWebImageManagerDelegate
聲明如下:
// 緩存中沒有圖片的話是否下載這張圖片陷谱,如果返回NO的話,則如果緩存中沒有這張圖片主胧,也不會(huì)去重新下載
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
// 允許在圖片已經(jīng)被下載完成且被緩存到磁盤或內(nèi)存前立即轉(zhuǎn)換
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;
如果我們要設(shè)置 只加載緩存叭首,不加載網(wǎng)絡(luò)圖片
,或者在拿到圖片做一些處理
踪栋,可以遵守改協(xié)議焙格,自己實(shí)現(xiàn)加載邏輯;
在SDWebImageManager.m
頁(yè)面同樣還有一個(gè) SDWebImageCombinedOperation
類夷都,并遵守<SDWebImageOperation>
協(xié)議眷唉,主要負(fù)責(zé)取消一些未執(zhí)行的NSOperation操作和正在執(zhí)行的操作
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;
@end
協(xié)議方法實(shí)現(xiàn):
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
_cancelBlock = nil;
}
}
這個(gè)就是文章一開始,每次視圖加載前都需要取消之前的操作所執(zhí)行的方法囤官,通過cancelled
屬性來中斷正在進(jìn)行的操作
下面我們來看下:SDWebImageManager
的loadImageWithURL: options: progress: completed...
加載圖片源碼具體實(shí)現(xiàn):
/*
加載圖片,返回緩存版本冬阳。 如果不在緩存中,則使用給的URL下載圖像
*/
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 如果是NSString類型則進(jìn)行強(qiáng)轉(zhuǎn)NSurl
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 防止應(yīng)用程序在參數(shù)類型錯(cuò)誤上崩潰
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
// 自定義Operation對(duì)象(繼承NSObject) 內(nèi)部有一個(gè) NSOperation對(duì)象
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO;
// 檢查是否為Url是否是失效的
if (url) {
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}
// 如果url為空或者是失效的,則直接block回調(diào),并附加上錯(cuò)誤信息,不再進(jìn)行加載操作
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
return operation;
}
// 將當(dāng)前操作 添加 到正在運(yùn)行的操作 集合中
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
// url轉(zhuǎn)字符串 Key
NSString *key = [self cacheKeyForURL:url];
// 并在緩存中查詢 圖片,如果內(nèi)存中有,直接回調(diào), 內(nèi)存中沒有,則會(huì)創(chuàng)建NSoperation操作,異步在 磁盤查詢
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
// 判斷操作是否被取消了
if (operation.isCancelled) {
// 從正在執(zhí)行的操作中移除,并renturn操作
[self safelyRemoveOperationFromRunning:operation];
return;
}
// 沒有圖片緩存 或者 options 為 SDWebImageRefreshCached 党饮,或者代理設(shè)置需要網(wǎng)絡(luò)加載肝陪,從網(wǎng)絡(luò)刷新
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
// 如果有緩存,但是提供了SDWebImageRefreshCached刑顺,先展示之前的緩存
if (cachedImage && options & SDWebImageRefreshCached) {
[self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
// 根據(jù)SDWebImageOptions設(shè)置下載選項(xiàng)的 SDWebImageDownloaderOptions
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
if (cachedImage && options & SDWebImageRefreshCached) {
//如果圖像已經(jīng)緩存但強(qiáng)制刷新氯窍,則強(qiáng)制關(guān)閉
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
//忽略從NSURLCache讀取的圖像,如果緩存了圖像蹲堂,則強(qiáng)制刷新
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
// 使用給定的URL創(chuàng)建SDWebImageDownloader加載器實(shí)例
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
// 如果Operation被取消
}
else if (error)
{
// 錯(cuò)誤回調(diào)
[self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost) {
// 加入黑名單
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
else
{
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
// 是否磁盤緩存
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
} else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
// 如果圖像被轉(zhuǎn)換狼讨,傳遞nil,這樣我們就可以重新計(jì)算圖像中的數(shù)據(jù)
[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
}
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {
if (downloadedImage && finished) {
// 磁盤緩存
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
if (finished) {
// 移除 Operation操作
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
operation.cancelBlock = ^{
[self.imageDownloader cancel:subOperationToken];
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self safelyRemoveOperationFromRunning:strongOperation];
};
}
else if (cachedImage) // 從內(nèi)存或者磁盤中查詢到圖片信息
{
__strong __typeof(weakOperation) strongOperation = weakOperation;
// 進(jìn)行Block回調(diào)
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// 從正在運(yùn)行的集合中移除 移除 operation操作
[self safelyRemoveOperationFromRunning:operation];
}
else
{
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
}
}];
return operation;
}
以上代碼邏輯還是比較清晰的柒竞,方法內(nèi)部會(huì)創(chuàng)建一個(gè) SDWebImageCombinedOperation
對(duì)象政供,這個(gè)對(duì)象用于貫穿緩存查詢和下載操作,如果operation.isCancelled
為YES
朽基,則回調(diào)操作會(huì)直接 return
布隔,不在繼續(xù)執(zhí)行;
方法內(nèi)部首先會(huì)調(diào)用了[self.imageCache queryCacheOperationForKey:()...]
方法踩晶,用于查詢圖片緩存执泰,如果沒有緩存則會(huì)調(diào)用[self.imageDownloader downloadImageWithURL:()...]
下載方法, 在獲得到圖片后,會(huì)將圖片通過Block回調(diào)傳給視圖。
SDImageCache緩存類
SDImageCache
是一個(gè)全局單例類, 負(fù)責(zé)處理內(nèi)存緩存及磁盤緩存渡蜻。其中磁盤緩存的讀寫操作是異步的术吝,這樣就不會(huì)對(duì)UI操作造成影響。在SDImageCache
內(nèi)部有一個(gè)NSCache
屬性,用于處理內(nèi)存緩存, NSCache
是一個(gè)類似NSDictionary一個(gè)可變的集合,當(dāng)內(nèi)存警告時(shí)內(nèi)部自動(dòng)清理部分緩存數(shù)據(jù)茸苇。磁盤緩存的處理則是使用NSFileManager
對(duì)象來實(shí)現(xiàn)的排苍。圖片存儲(chǔ)的位置是位于Cache
文件夾。另外学密,SDImageCache
還定義了一個(gè)串行隊(duì)列
淘衙,來異步存儲(chǔ)圖片。
內(nèi)存緩存與磁盤緩存相關(guān)變量的聲明及定義如下:
@interface SDImageCache ()
@property (strong, nonatomic, nonnull) NSCache *memCache;
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;
@end
@implementation SDImageCache {
NSFileManager *_fileManager;
}
// 初始化
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
我們看下SDImageCache
查詢緩存源碼:
// 異步查詢緩存并在完成時(shí)調(diào)用完成的操作腻暮。
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
// key是空的則直接回調(diào),并返回nil
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 首先檢查內(nèi)存緩存…
UIImage *image = [self imageFromMemoryCacheForKey:key];
// 如果內(nèi)存中有圖片
if (image) {
NSData *diskData = nil;
// 判斷是否為gif圖
if ([image isGIF]) {
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
// 回調(diào)內(nèi)存圖片信息
if (doneBlock) {
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
return nil;
}
// 內(nèi)存未查到圖片信息 則創(chuàng)建一個(gè) operation
NSOperation *operation = [NSOperation new];
// 在一個(gè)串行里異步執(zhí)行
dispatch_async(self.ioQueue, ^{
// 如果取消了彤守,則進(jìn)行回調(diào)
if (operation.isCancelled) {
return;
}
// 從磁盤進(jìn)行查詢
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage = [self diskImageForKey:key];
// 判斷是否有圖片并需要內(nèi)存緩存
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
// 內(nèi)存緩存一份
[self.memCache setObject:diskImage forKey:key cost:cost];
}
// 進(jìn)行回調(diào)
if (doneBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
});
return operation;
}
通過上面的代碼,我們可以看出, 首先在內(nèi)存中查詢圖片通過imageFromMemoryCacheForKey:() ...
// 從內(nèi)存中查看是否有改圖片
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
return [self.memCache objectForKey:key];
}
key
為圖片的 URL 地址毯侦, Value
則是緩存圖片,如果內(nèi)存沒有緩存,則會(huì)調(diào)用[self diskImageDataBySearchingAllPathsForKey:key]
進(jìn)行磁盤查詢
// 磁盤查詢
- (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;
}
}
return nil;
}
如果內(nèi)存和磁盤都未查詢到圖片,則會(huì)進(jìn)行網(wǎng)絡(luò)請(qǐng)求下載圖片
另外SDImageCache
還提供了存儲(chǔ)圖片
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
if (!image || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
// 內(nèi)存緩存
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
// 如果磁盤緩存
if (toDisk) {
dispatch_async(self.ioQueue, ^{ // 在一個(gè)串行隊(duì)列里面進(jìn)行
NSData *data = imageData;
if (!data && image) {
SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data]; // 獲取圖片格式
data = [image sd_imageDataAsFormat:imageFormatFromData]; //根據(jù)格式返回二進(jìn)制數(shù)據(jù)
}
[self storeImageDataToDisk:data forKey:key]; // 存儲(chǔ)到磁盤
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
}
緩存操作具垫,該操作會(huì)在內(nèi)存中放置一份緩存侈离,而如果確定需要緩存到磁盤,則將磁盤緩存操作作為一個(gè)task
放到串行隊(duì)列中處理筝蚕。在方法中卦碾,會(huì)先檢測(cè)圖片是PNG
還是JPEG
,并將其轉(zhuǎn)換為相應(yīng)的圖片數(shù)據(jù)起宽,最后將數(shù)據(jù)寫入到磁盤中(文件名是對(duì)key值做MD5后的串)洲胖。
SDWebImageDownloader下載管理器
SDWebImageDownloader
下載管理器是一個(gè)單例類,它主要負(fù)責(zé)圖片的下載的管理坯沪。實(shí)質(zhì)的下載操作則是通過SDWebImageDownloaderOperation
類繼承NSOperation
绿映,圖片的下載操作是放在一個(gè)NSOperationQueue
操作隊(duì)列中來完成的。
@property (strong, nonatomic) NSOperationQueue *downloadQueue;
默認(rèn)情況下屏箍,隊(duì)列最大并發(fā)數(shù)是6绘梦。如果需要的話,我們可以通過SDWebImageDownloader
類的maxConcurrentDownloads
屬性來修改赴魁。
我看來看下downloadImageWithURL:()..options:(): progress():completed:()
下載操作內(nèi)部實(shí)現(xiàn)
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
// 默認(rèn)15秒
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 為了防止?jié)撛诘闹貜?fù)緩存(NSURLCache + SDImageCache)卸奉,如果被告知其他信息,我們將禁用圖像請(qǐng)求的緩存
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = sself.HTTPHeaders;
}
// 創(chuàng)建下載操作
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
// 是否解碼
operation.shouldDecompressImages = sself.shouldDecompressImages;
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 操作加入隊(duì)列
[sself.downloadQueue addOperation:operation];
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
該方法的內(nèi)部會(huì)調(diào)用addProgressCallback..
方法颖御,會(huì)將圖片下載的一些回調(diào)信息存儲(chǔ)在SDWebImageDownloaderOperation
類的URLCallbacks
屬性中榄棵,該屬性是一個(gè)字典,key
是圖片的URL地址
潘拱,value
則是一個(gè)數(shù)組疹鳄,包含每個(gè)圖片的多組回調(diào)信息。
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
// URL將用作回調(diào)字典的鍵芦岂,因此它不能為nil瘪弓。如果是nil,立即調(diào)用完整的塊禽最,沒有圖像或數(shù)據(jù)腺怯。
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
__block SDWebImageDownloadToken *token = nil;
// 按順序依次執(zhí)行
dispatch_barrier_sync(self.barrierQueue, ^{
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
// 創(chuàng)建下載 SDWebImageDownloaderOperation操作
operation = createCallback();
// 添加到URLOperations操作緩存中
self.URLOperations[url] = operation;
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
// 下載完移除下載操作
[self.URLOperations removeObjectForKey:url];
};
};
}
// 將一些回到信息放入`SDWebImageDownloaderOperation`類的`callbackBlocks`屬性中,并創(chuàng)建 downloadOperationCancelToken實(shí)例
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
return token;
}
在SDWebImageDownloaderOperation
對(duì)象加入到操作隊(duì)列后川无,就開始調(diào)用該對(duì)象的start
方法呛占,代碼如下
// SDWebImageDownloaderOperation.m
- (void)start {
// 如果操作被取消,就reset設(shè)置
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
...
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
// 創(chuàng)建session的配置
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
// 創(chuàng)建session對(duì)象
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
// 開始下載任務(wù)
[self.dataTask resume];
if (self.dataTask) {
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
} else {
// 創(chuàng)建任務(wù)失敗
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
}
...
}
在下載過程中懦趋,會(huì)涉及鑒權(quán)晾虑、響應(yīng)的statusCode
判斷(404
、304
等等),以及收到數(shù)據(jù)后的進(jìn)度回調(diào)等等帜篇,在最后的URLSession:task:didCompleteWithError
里做最后的處理糙捺,然后回調(diào)完成的block
,下面僅分析一下- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
的方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
...
if (error) {
[self callCompletionBlocksWithError:error];
} else {
if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
/**
* See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash
* Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response
* and images for which responseFromCached is YES (only the ones that cannot be cached).
* Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication
*/
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
// 如果options是忽略緩存笙隙,而圖片又是從緩存中取的继找,就給回調(diào)傳入nil
[self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
} else if (self.imageData) {
UIImage *image = [UIImage sd_imageWithData:self.imageData];
// 緩存圖片
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
// 跳轉(zhuǎn)圖片的大小
image = [self scaledImageForKey:key image:image];
// Do not force decoding animated GIFs
if (!image.images) {
// 不是Gif圖像
if (self.shouldDecompressImages) {
if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
#if SD_UIKIT || SD_WATCH
image = [UIImage decodedAndScaledDownImageWithImage:image];
[self.imageData setData:UIImagePNGRepresentation(image)];
#endif
} else {
image = [UIImage decodedImageWithImage:image];
}
}
}
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
// 下載是圖片大小的0
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
} else {
// 把下載的圖片作為參數(shù)回調(diào)
[self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
}
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
}
}
}
...
}