項(xiàng)目中一直都有使用SDWebImage
,對(duì)這個(gè)框架有一定的了解哩牍,但是體系卻未能貫通,因此特地整理下,主要參考:
一. 簡(jiǎn)介
SDWebImage
提供了一個(gè)異步下載圖片并且支持緩存的UIImageView
分類(lèi)疗垛。
主要邏輯為:
- 查看緩存罢洲,如果緩存中存在圖片就返回圖片并且更新
UIImageView
. - 緩存中不存在圖片就異步下載圖片,加入緩存塑崖,更新
UIImageView
.
主要用到的對(duì)象:
1七冲、UIImageView (WebCache)
類(lèi)別,入口封裝规婆,實(shí)現(xiàn)讀取圖片完成后的回調(diào)
2澜躺、SDWebImageManager
,對(duì)圖片進(jìn)行管理的中轉(zhuǎn)站抒蚜,記錄那些圖片正在讀取掘鄙。
向下層讀取Cache
(調(diào)用SDImageCache
),或者向網(wǎng)絡(luò)請(qǐng)求下載對(duì)象(調(diào)用SDWebImageDownloader
) 嗡髓。
實(shí)現(xiàn)SDImageCache
和SDWebImageDownloader
的回調(diào)操漠。
3、SDImageCache
饿这,根據(jù)URL的MD5生成key對(duì)圖片進(jìn)行存儲(chǔ)和讀嚷帷(實(shí)現(xiàn)存在內(nèi)存中或者存在硬盤(pán)上兩種實(shí)現(xiàn))
實(shí)現(xiàn)圖片和內(nèi)存清理工作。
4蛹稍、SDWebImageDownloader
吧黄,根據(jù)URL
向網(wǎng)絡(luò)讀取數(shù)據(jù)(實(shí)現(xiàn)部分讀取和全部讀取后再通知回調(diào)兩種方式)
其他類(lèi):
SDWebImageDecoder,異步對(duì)圖像進(jìn)行了一次解壓唆姐。
具體流程圖:
SDWebImage
加載圖片的流程 :
入口
setImageWithURL:placeholderImage:options:
會(huì)先把placeholderImage
顯示拗慨,然后 SDWebImageManager 根據(jù) URL 開(kāi)始處理圖片。進(jìn)入
SDWebImageManager-downloadWithURL:delegate:options:userInfo:
奉芦,交給SDImageCache
從緩存查找圖片是否已經(jīng)下載queryDiskCacheForKey:delegate:userInfo:
.先從內(nèi)存圖片緩存查找是否有圖片赵抢,如果內(nèi)存中已經(jīng)有圖片緩存,
SDImageCacheDelegate
回調(diào)imageCache:didFindImage:forKey:userInfo:
到SDWebImageManager
声功。
4.SDWebImageManagerDelegate
回調(diào) webImageManager:didFinishWithImage:
到 UIImageView+WebCache
等前端展示圖片烦却。
如果內(nèi)存緩存中沒(méi)有,生成
NSInvocationOperation
添加到隊(duì)列開(kāi)始從硬盤(pán)異步查找圖片是否已經(jīng)緩存先巴。根據(jù)
URLKey
在硬盤(pán)緩存目錄下嘗試讀取圖片文件其爵。這一步是在 NSOperation 進(jìn)行的操作冒冬,所以回主線程進(jìn)行結(jié)果回調(diào)notifyDelegate:
。如果上一操作從硬盤(pán)讀取到了圖片摩渺,將圖片添加到內(nèi)存緩存中(如果空閑內(nèi)存過(guò)小简烤,會(huì)先清空內(nèi)存緩存)。
SDImageCacheDelegate
回調(diào)imageCache:didFindImage:forKey:userInfo:
摇幻。進(jìn)而回調(diào)展示圖片横侦。如果從硬盤(pán)緩存目錄讀取不到圖片,說(shuō)明所有緩存都不存在該圖片绰姻,需要下載圖片枉侧,回調(diào)
imageCache:didNotFindImageForKey:userInfo:
。共享或重新生成一個(gè)下載器
SDWebImageDownloader
開(kāi)始下載圖片狂芋。圖片下載由
NSURLConnection
來(lái)做榨馁,實(shí)現(xiàn)相關(guān)delegate
來(lái)判斷圖片下載中、下載完成和下載失敗银酗。connection:didReceiveData:
中利用ImageIO
做了按圖片下載進(jìn)度加載效果。connectionDidFinishLoading:
數(shù)據(jù)下載完成后交給SDWebImageDecoder
做圖片解碼處理徒像。圖片解碼處理在一個(gè)
NSOperationQueue
完成黍特,不會(huì)拖慢主線程UI
。如果有需要對(duì)下載的圖片進(jìn)行二次處理锯蛀,最好也在這里完成灭衷,效率會(huì)好很多。在主線程
notifyDelegateOnMainThreadWithInfo:
宣告解碼完成旁涤,imageDecoder:didFinishDecodingImage:userInfo:
回調(diào)給SDWebImageDownloader
翔曲。imageDownloader:didFinishWithImage: 回調(diào)給 SDWebImageManager 告知圖片下載完成。
通知所有的 downloadDelegates 下載完成劈愚,回調(diào)給需要的地方展示圖片瞳遍。
將圖片保存到
SDImageCache
中,內(nèi)存緩存和硬盤(pán)緩存同時(shí)保存菌羽。寫(xiě)文件到硬盤(pán)也在以單獨(dú)NSInvocationOperation
完成掠械,避免拖慢主線程。SDImageCache
在初始化的時(shí)候會(huì)注冊(cè)一些消息通知注祖,在內(nèi)存警告或退到后臺(tái)的時(shí)候清理內(nèi)存圖片緩存猾蒂,應(yīng)用結(jié)束的時(shí)候清理過(guò)期圖片。SDWebImage
也提供了UIButton+WebCach
e 和MKAnnotationView+WebCache
是晨,方便使用肚菠。SDWebImagePrefetcher
可以預(yù)先下載圖片,方便后續(xù)使用罩缴。
二. 架構(gòu)簡(jiǎn)介
A.架構(gòu)圖:
UIImageView+WebCaceh
和UIButton+WebCache
直接為UIkit框架提供接口蚊逢,而SDWebImageManger
負(fù)責(zé)處理和協(xié)調(diào)SDWebImageDownloader
和SDWebImageCache
并與UIkit
層進(jìn)行交互层扶。
三. 具體分析
1.UIImageView+WebCache
A.框架常用入口
// 所有設(shè)置圖片最終都會(huì)調(diào)用這個(gè)方法
- (void)sd_setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder {
[self sd_setImageWithURL:url
placeholderImage:placeholder
options:0
progress:nil
completed:nil];
}
該接口調(diào)用下面這個(gè)方法:
[self sd_setImageWithURL:placeholderImage:options:progress:completed:]
該方法作為sd_setImageWithURL接口的最終入口,提供了多種參數(shù)时捌。
url
:遠(yuǎn)程圖片的地址placeholder
: 預(yù)顯示圖片-
options
:SDWebImageOptions
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { //下載失敗了會(huì)再次嘗試下載 SDWebImageRetryFailed = 1 << 0, //當(dāng)UIScrollView等正在滾動(dòng)時(shí)怒医,延遲下載圖片(放置scrollView滾動(dòng)卡) WebImageLowPriority = 1 << 1, //只緩存到內(nèi)存中 SDWebImageCacheMemoryOnly = 1 << 2, // 圖片會(huì)邊下邊顯示 SDWebImageProgressiveDownload = 1 << 3, // 將硬盤(pán)緩存交給系統(tǒng)自帶的NSURLCache去處理 SDWebImageRefreshCached = 1 << 4, //后臺(tái)下載 SDWebImageContinueInBackground = 1 << 5, // 通過(guò)設(shè)置NSMutableURLRequest.HTTPShouldHandleCookies = YES來(lái)處理存儲(chǔ)在NSHTTPCookieStore中的cookie SDWebImageHandleCookies = 1 << 6, // 允許不受信任的SSL證書(shū)。主要用于測(cè)試目的奢讨。 SDWebImageAllowInvalidSSLCertificates = 1 << 7, // 默認(rèn)情況下,image在裝載的時(shí)候是按照他們?cè)陉?duì)列中的順序裝載的(就是先進(jìn)先出).這個(gè)flag會(huì)把他們移動(dòng)到隊(duì)列的前端,并且立刻裝載,而不是等到當(dāng)前隊(duì)列裝載的時(shí)候再裝載 SDWebImageHighPriority = 1 << 8, // 默認(rèn)情況下,占位圖會(huì)在圖片下載的時(shí)候顯示.這個(gè)flag開(kāi)啟會(huì)延遲占位圖顯示的時(shí)間,等到圖片下載完成之后才會(huì)顯示占位圖 SDWebImageDelayPlaceholder = 1 << 9, // 是否transform圖片 SDWebImageTransformAnimatedImage = 1 << 10, };
progress
:下載進(jìn)度
B.代碼分析:
操作的管理:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
// 取消當(dāng)前下載操作
[self sd_cancelCurrentImageLoad];
// 動(dòng)態(tài)添加屬性
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 如果選項(xiàng)非SDWebImageDelayPlaceholder
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
// 設(shè)置占位圖
self.image = placeholder;
});
}
if (url.absoluteString.length > 0) {
// check if activityView is enabled or not
if ([self showActivityIndicatorView]) {
// 顯示 下載轉(zhuǎn)圈
[self addActivityIndicator];
}
__weak __typeof(self)wself = self;
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
// 下載完成回調(diào)
// 移除下載進(jìn)度轉(zhuǎn)圈
[wself removeActivityIndicator];
if (!wself) return;
dispatch_main_sync_safe(^{
if (!wself) return;
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
completedBlock(image, error, cacheType, url);
return;
}
else if (image) {
wself.image = image;
[wself setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else {
dispatch_main_async_safe(^{
[self removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
[self sd_cancelCurrentImageLoad]
;取消當(dāng)前的下載操作稚叹,它表明 SDWebImage
管理操作的方法:
SDWebImage
所有的操作實(shí)際都是通過(guò)一個(gè) operationDictionary
的字典管理,這個(gè)字典是動(dòng)態(tài)添加到 UIView
上的一個(gè)屬性拿诸,因?yàn)檫@個(gè)operationDictionary
需要在UIButton
和 UIImageView
上重用扒袖,所以需要添加到它們的根類(lèi)上。
這行代碼是要保證沒(méi)有當(dāng)前正在進(jìn)行的異步下載操作, 不會(huì)與即將進(jìn)行的操作發(fā)生沖突, 它會(huì)調(diào)用:
// UIImageView+WebCache
// sd_cancelCurrentImageLoad #1
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]
這行代碼會(huì)取消當(dāng)前這個(gè)UIImageView
的所有操作亩码,不會(huì)影響之后進(jìn)行的下載操作季率。
占位圖的實(shí)現(xiàn):
// UIImageView+WebCache
//sd_setImageWithURL:placeholderImage:options:progress:completed: #4
if (!(options & SDWebImageDelayPlaceholder)) { self.image = placeholder;
}
當(dāng)options
中沒(méi)有SDWebImageDelayPlaceholder
,UIImageView添加一個(gè)占位圖image.
獲取圖片:
// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #8
if (url)
檢測(cè)傳入的URL
是否為空描沟,如果非空就調(diào)用全局的SDWebImageManager
來(lái)獲取圖片:
[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]
下載完成后調(diào)用(SDWebImageCompletionWithFinishedBlock)completedBlock 為 UIImageView.image 賦值, 添加上最終所需要的圖片.
// UIImageView+WebCache
//sd_setImageWithURL:placeholderImage:options:progress:completed: #10
dispatch_main_sync_safe(^{
if (!wself) return;
if (image) {
wself.image = image;
[wself setNeedsLayout];
}
else {
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
最后在返回 operation
的同時(shí), 也會(huì)向 operationDictionary
中添加一個(gè)鍵值對(duì), 來(lái)表示操作的正在進(jìn)行:
// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #28
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
它將operation 存儲(chǔ)到operationDictionary 中方便以后的cancel操作飒泻。
2. SDWebImageManager
這個(gè)類(lèi)主要用于處理異步下載和圖片緩存的類(lèi),也可以直接用SDWebImageManager
的downloadImageWithURL:options:progress:completed:
來(lái)直接下載圖片吏廉。
可以看出這個(gè)類(lèi)主要作用就是為了UIImageView+WebCache
和 SDWebImageDownloader
, SDImageCache
之間構(gòu)建一個(gè)橋梁泞遗,使它們能夠更好的協(xié)同工作。
A.核心代碼分析:
a.SDWebImageManager
// SDWebImageManager
//- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
這塊代碼的功能是確定 url是否被正確傳入, 如果傳入?yún)?shù)的是 NSString
類(lèi)型就會(huì)被轉(zhuǎn)換為NSURL
, 如果轉(zhuǎn)換失敗, 那么url
會(huì)被賦值為空, 這個(gè)下載的操作就會(huì)出錯(cuò).
b. SDWebImageCombinedOperation
當(dāng) url
被正確傳入之后, 會(huì)實(shí)例一個(gè)非常奇怪的 operation
, 它其實(shí)是一個(gè)遵循 SDWebImageOperation
協(xié)議的 NSObject
的子類(lèi). 而這個(gè)協(xié)議也非常的簡(jiǎn)單:
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
SDWebImageOperation
只是看著像NSOperation
但是它唯一跟NSOperation
相同就是都可以響應(yīng)cancel
方法席覆。調(diào)用這個(gè)類(lèi)的cancel
方法史辙,會(huì)使得它持有的兩個(gè)operation
都被cancel
。
// SDWebImageCombinedOperation
// cancel #1
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
_cancelBlock = nil;
}
}
既然獲取了url
佩伤,再通過(guò)url
獲取對(duì)應(yīng)的key
.
NSString *key = [self cacheKeyForURL:url];
接著通過(guò)key在緩存中查找一起是否下載過(guò)相同的圖片
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { ... }];
這里調(diào)用SDImageCache
的實(shí)例方法 queryDiskCacheForKey:done:
來(lái)嘗試在緩存中獲取圖片的數(shù)據(jù),而這個(gè)方法獲取的就是貨真價(jià)實(shí)的NSOperation
.
如果我們?cè)诰彺嬷胁檎业綄?duì)應(yīng)的圖片聊倔,那么我們直接調(diào)用completedBlock
回調(diào)塊結(jié)束這一次圖片的下載操作
// SDWebImageManager
// downloadImageWithURL:options:progress:completed: #47
dispatch_main_sync_safe(^{ completedBlock(image, nil, cacheType, YES, url);});
如果沒(méi)有找到就調(diào)用SDWebImageDownLoader的實(shí)例方法去下載該圖片:
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { ... }];
如果這個(gè)方法返回正確的downloadedImage
,那么我們就在全局緩存中存儲(chǔ)這個(gè)圖片的數(shù)據(jù):
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
并調(diào)用completedBlock
對(duì)UIImageView
或者UIButton
添加圖片生巡。
最后我們將這個(gè)subOperation
的 cancel
操作添加到operation.cancelBlock
中耙蔑,方便操作的取消
operation.cancelBlock = ^{ [subOperation cancel]; }
3. SDWebImageCache
維護(hù)了一個(gè)內(nèi)存緩存和一個(gè)可選的磁盤(pán)緩存,首先看下查詢圖片緩存的方法:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
該方法主要功能是異步查詢圖片緩存,先在內(nèi)存中查找
// SDWebImageCache
// queryDiskCacheForKey:done: #9
UIImage *image = [self imageFromMemoryCacheForKey:key];
// 內(nèi)存中查找圖片
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];
}
imageFromMemoryCacheForKey:key
方法會(huì)在SDWebImageCache 維護(hù)的緩存memCache
中查找是否有對(duì)應(yīng)的數(shù)據(jù)孤荣,而 memCache
就是一個(gè) NSCache
.
NSCache
是一個(gè)類(lèi)似于 NSMutableDictionary
存儲(chǔ) key-value
的容器纵潦,主要有以下幾個(gè)特點(diǎn):
自動(dòng)刪除機(jī)制:當(dāng)系統(tǒng)內(nèi)存緊張時(shí),NSCache
會(huì)自動(dòng)刪除一些緩存對(duì)象
線程安全:從不同線程中對(duì)同一個(gè) NSCache
對(duì)象進(jìn)行增刪改查時(shí)垃环,不需要加鎖
不同于 NSMutableDictionary
邀层,NSCache
存儲(chǔ)對(duì)象時(shí)不會(huì)對(duì)key
進(jìn)行 copy
操作
如果在內(nèi)存中沒(méi)有找到圖片的緩存的話,就需要在磁盤(pán)中查找遂庄。
- (UIImage *)diskImageForKey:(NSString *)key {
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
if (data) {
UIImage *image = [UIImage sd_imageWithData:data];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
return image;
}
else {
return nil;
}
}
得到圖片對(duì)應(yīng)的NSData
后還有經(jīng)過(guò):
- 根據(jù)圖片的不同種類(lèi)寥院,生成對(duì)應(yīng)的UIImage,
- 根據(jù)
key
值,調(diào)整image
的Scale
值 - 如果設(shè)置圖片需要解壓縮涛目,需要對(duì)圖片進(jìn)行解碼
對(duì)圖片進(jìn)行存儲(chǔ)需要對(duì)url
進(jìn)行MD5
加密計(jì)算生成相應(yīng)的key
值:
- (NSString *)cachedFileNameForKey:(NSString *)key {
const char *str = [key UTF8String];
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];
return filename;
}
然后用該key
作為圖片文件名存儲(chǔ)在默認(rèn)路徑下:
// 獲取緩存路徑方法(自己寫(xiě)的)
- (NSString*)getCachePath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
if (paths.count > 0) {
NSString *path = [paths[0] stringByAppendingFormat:@"/com.hackemist.SDWebImageCache.default"];
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
[[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
}
return path;
}else{
return nil;
}
}
之前做朋友圈后臺(tái)發(fā)送圖片就是先將小圖命名秸谢,然后根據(jù)獲取到的七牛的domain和token,拼出url凛澎,接著將該url,進(jìn)行md5
加密估蹄,加密后存儲(chǔ)到SDWebImage
的默認(rèn)存儲(chǔ)路徑下塑煎,然后在主界面顯示存儲(chǔ)的小圖,后臺(tái)去進(jìn)行圖片壓縮上傳任務(wù)臭蚁。
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
如果在磁盤(pán)中找到圖片最铁,就將他復(fù)制到內(nèi)存中,以便下次使用垮兑。
4.SDWebImageDownloader
專(zhuān)用的并且優(yōu)化的圖片異步下載器,主要用來(lái)下載圖片,下載放在NSOperationQueue
中進(jìn)行冷尉,默認(rèn)maxConcurrentOperationCount
為6,timeout時(shí)間為15s.
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock
該方法直接調(diào)用了下載進(jìn)度回調(diào)函數(shù):
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
// Handle single download of simultaneous download request for the same URL
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
if (first) {
createCallback();
}
});
}
方法會(huì)先查看這個(gè) url
是否有對(duì)應(yīng)的 callback
, 使用的是 downloader
系枪,持有的一個(gè)字典URLCallbacks
.
如果是第一次添加回調(diào)的話, 就會(huì)執(zhí)行first = YES
, 這個(gè)賦值非常的關(guān)鍵, 因?yàn)?first
不為 YES
那么 HTTP
請(qǐng)求就不會(huì)被初始化, 圖片也無(wú)法被獲取.
然后, 在這個(gè)方法中會(huì)重新修正在URLCallbacks
中存儲(chǔ)的回調(diào)塊.
通過(guò)dispatch_barrier_async
函數(shù)提交的任務(wù)會(huì)等它前面的任務(wù)執(zhí)行完才開(kāi)始雀哨,然后它后面的任務(wù)必須等它執(zhí)行完畢才能開(kāi)始. 必須使用dispatch_queue_create
創(chuàng)建的隊(duì)列才會(huì)達(dá)到上面的效果.通過(guò)該函數(shù)來(lái)保證每張圖片進(jìn)度順序。
如果是第一次添加回調(diào)塊私爷,那么就會(huì)直接運(yùn)行這個(gè)createCallBack
這個(gè)block
雾棺,而這個(gè)block
,就是我們?cè)?code>downloadImageWithURL:options:progress:completed: 中傳入的回調(diào)塊.
接著分析下NSMutableURLRequest
請(qǐng)求:
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
該request
發(fā)送了一個(gè)http
請(qǐng)求衬浑,接著又初始化一個(gè)SDWebImageDownloaderOperation
實(shí)例捌浩,這個(gè)實(shí)例用于請(qǐng)求網(wǎng)絡(luò)資源的操作,是NSOperation
的子類(lèi):
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
初始化之后嚎卫,將該operation
添加到NSOperationQueue
中嘉栓。(備注:NSOperation
實(shí)例只有在調(diào)用start
方法或者加入NSOperationQueue
才會(huì)執(zhí)行)
[wself.downloadQueue addOperation:operation];
5.SDWebImageDownloaderOperation
這個(gè)類(lèi)主要處理HTTP
請(qǐng)求宏榕,URL
連接的類(lèi)拓诸,當(dāng)這個(gè)類(lèi)的實(shí)例被加入到隊(duì)列之后,start
方法被調(diào)用麻昼,start
方法首先產(chǎn)生一個(gè)NSURLConnection
,通過(guò)NSURLConnection
進(jìn)行圖片的下載奠支,為了確保能夠處理下載的數(shù)據(jù),需要在后臺(tái)運(yùn)行runloop
,保證程序不被掛起.
- (void)start {
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
self.executing = YES;
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
self.thread = [NSThread currentThread];
}
[self.connection start];
if (self.connection) {
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
//在主線程發(fā)通知抚芦,這樣也保證在主線程收到通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
// Make sure to run the runloop in our background thread so it can process downloaded data
// Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
// not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466)
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
}
else {
CFRunLoopRun();
}
if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
}
else {
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
接下來(lái)這個(gè) connection
就會(huì)開(kāi)始運(yùn)行:
[self.connection start];
它發(fā)出一個(gè)SDWebImageDownloadStartNotification
通知,開(kāi)啟狀態(tài)欄的請(qǐng)求加載轉(zhuǎn)圈倍谜。同時(shí)調(diào)用NSURLConnectionDataDelegate
代理
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;
前兩個(gè)代理會(huì)不停的回調(diào) pregressBlock
來(lái)提示下載進(jìn)度。
而最后一個(gè)代理方法會(huì)在圖片下載完成之后調(diào)用completionBlock
來(lái)完成最后 UIImageView.image
的更新叉抡,而這里調(diào)用的 progressBlock
尔崔,completionBlock
, cancelBlock
都是在之前存儲(chǔ)在 URLCallbacks
字典中的.
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
@synchronized(self) {
// 停止 該線程 運(yùn)行時(shí)
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
self.connection = nil;
// 通知停止?fàn)顟B(tài)欄轉(zhuǎn)圈請(qǐng)求
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
});
}
if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
responseFromCached = NO;
}
if (completionBlock) {
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) {
completionBlock(nil, nil, nil, YES);
} else if (self.imageData) {
// 進(jìn)行緩存
UIImage *image = [UIImage sd_imageWithData:self.imageData];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
// Do not force decoding animated GIFs
if (!image.images) {
// 進(jìn)行解碼
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
}
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
}
else {
completionBlock(image, self.imageData, nil, YES);
}
} else {
completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES);
}
}
self.completionBlock = nil;
[self done];
}
轉(zhuǎn)換處理圖片和進(jìn)行緩存后褥民,將下載image
賦值給控件跳纳。
四. 面試點(diǎn)
1北专、SDImageCache
是怎么做數(shù)據(jù)管理的?
SDImageCache
分成兩部分,一個(gè)是內(nèi)存層面的侣姆,一個(gè)是磁盤(pán)層面的。內(nèi)存緩存的處理是使用
NSCache
對(duì)象來(lái)實(shí)現(xiàn)的煮嫌。NSCache
是一個(gè)類(lèi)似于集合的容器。它存儲(chǔ)key-value
對(duì),這一點(diǎn)類(lèi)似于NSDictionary
類(lèi)惫叛,用搜索文件系統(tǒng)的方式做管理,文件替換方式是以時(shí)間為單位逞刷。我們通常用使用緩存來(lái)臨時(shí)存儲(chǔ)短時(shí)間使用但創(chuàng)建昂貴的對(duì)象嘉涌。重用這些對(duì)象可以優(yōu)化性能,因?yàn)樗鼈兊闹挡恍枰匦掠?jì)算亲桥。另外一方面洛心,這些對(duì)象對(duì)于程序來(lái)說(shuō)不是緊要的,在內(nèi)存緊張時(shí)會(huì)被丟棄题篷。磁盤(pán)緩存的處理則是使用
NSFileManager
對(duì)象來(lái)實(shí)現(xiàn)的词身。圖片存儲(chǔ)的位置是位于Cache
文件夾,文件替換方式是以時(shí)間為單位番枚,剔除時(shí)間大一一周的圖片文件法严。當(dāng)
SDWebImageManager
向SDImageCache
要資源時(shí), 先搜索內(nèi)存層面的數(shù)據(jù)葫笼,如果有直接返回深啤,沒(méi)有再訪問(wèn)磁盤(pán),如果有將圖片從磁盤(pán)讀取出來(lái)路星,然后做解壓溯街,將圖片對(duì)象放到內(nèi)存層面做備份,再返回調(diào)用層洋丐。
- 為什么圖片要進(jìn)行解壓?
- 因?yàn)?code>UIImage的
imageWithData
函數(shù)是每次畫(huà)圖的時(shí)候才將Data
解壓成ARGB
圖像呈昔,所以在每次畫(huà)圖的時(shí)候,會(huì)有一個(gè)解壓操作友绝,這樣效率很低堤尾,但是只有瞬時(shí)的內(nèi)存需求,為了提高效率迁客,通過(guò)SDWebImageDecoder
將包裝在Data
下的資源解壓郭宝,然后畫(huà)在另外一張圖片上面,這樣這張圖片就不需要重復(fù)解壓了掷漱,這種做法就是典型的空間換取時(shí)間的做法粘室。
3.SDWebImage
在多線程下載圖片時(shí)防止錯(cuò)亂的策略
SDWebImage
會(huì)將ImageView
對(duì)象關(guān)聯(lián)一個(gè)下載列表(列表是給AnimationImages
用的,這個(gè)時(shí)候會(huì)下載多張圖片)卜范,當(dāng)tableView滾動(dòng)時(shí)衔统,imageView會(huì)重設(shè)數(shù)據(jù)源url
,這時(shí)會(huì)cancel掉下載列表中當(dāng)前對(duì)應(yīng)的下載任務(wù),然后開(kāi)啟一個(gè)新的下載任務(wù),這樣就保證只有當(dāng)前可見(jiàn)的cell
對(duì)象的ImageView
對(duì)象關(guān)聯(lián)的下載任務(wù)能夠回調(diào)缰冤,不會(huì)發(fā)生Image
錯(cuò)亂犬缨。同時(shí),
SDWebImage
管理了一個(gè)全局下載隊(duì)列SDWebDownloadManager
棉浸,并發(fā)量設(shè)置為6怀薛,也就表示如果cell
的數(shù)目大于6,就會(huì)有部分下載隊(duì)列處于等待狀態(tài)迷郑,而且枝恋,在添加下載任務(wù)到全局的下載隊(duì)列中去的時(shí)候,SDWebImage
默認(rèn)采取的是LIFO(后進(jìn)先出)策略嗡害,具體是添加新的下載任務(wù)的時(shí)候焚碌,將之前的下載任務(wù)添加依賴為新的下載任務(wù)。
另外解決方案:
將
imageView
對(duì)象和圖片的url
相關(guān)聯(lián)霸妹,在滑動(dòng)時(shí)十电,不取消舊的下載任務(wù),而是在下載任務(wù)完成回調(diào)時(shí)叹螟,進(jìn)行url
匹配鹃骂,只有匹配成功的image
會(huì)刷新imageView
對(duì)象,而其他的image
則只做緩存操作罢绽,而不刷新UI
畏线。同時(shí),仍然管理一個(gè)執(zhí)行隊(duì)列良价,為了避免占用太多的資源寝殴,通常會(huì)對(duì)執(zhí)行隊(duì)列設(shè)置一個(gè)最大的并發(fā)量。此外明垢,為了保證
LIFO
的下載策略蚣常,可以自己維持一個(gè)等待隊(duì)列,每次下載任務(wù)開(kāi)始的時(shí)候袖外,將后進(jìn)入的下載任務(wù)插入到等待隊(duì)列的前面史隆。
-
SDWebImage
的主要任務(wù)就是圖片的下載和緩存魂务。為了支持這些操作曼验,它主要使用了以下知識(shí)點(diǎn):
dispatch_barrier_sync
函數(shù):該方法用于對(duì)操作設(shè)置屏障,確保在執(zhí)行完任務(wù)后才會(huì)執(zhí)行后續(xù)操作粘姜。該方法常用于確保類(lèi)的線程安全性操作鬓照。NSMutableURLRequest
:用于創(chuàng)建一個(gè)網(wǎng)絡(luò)請(qǐng)求對(duì)象,我們可以根據(jù)需要來(lái)配置請(qǐng)求報(bào)頭等信息孤紧。
NSOperation
及NSOperationQueue
:操作隊(duì)列是Objective-C
中一種高級(jí)的并發(fā)處理方法豺裆,現(xiàn)在它是基于GCD
來(lái)實(shí)現(xiàn)的。相對(duì)于GCD
來(lái)說(shuō),操作隊(duì)列的優(yōu)點(diǎn)是可以取消在任務(wù)處理隊(duì)列中的任務(wù)臭猜,另外在管理操作間的依賴關(guān)系方面也容易一些躺酒。對(duì)SDWebImage
中我們就看到了如何使用依賴將下載順序設(shè)置成后進(jìn)先出的順序。
-
NSURLConnection
:用于網(wǎng)絡(luò)請(qǐng)求及響應(yīng)處理蔑歌。在iOS7.0
后羹应,蘋(píng)果推出了一套新的網(wǎng)絡(luò)請(qǐng)求接口,即NSURLSession
類(lèi)次屠。
開(kāi)啟一個(gè)后臺(tái)任務(wù)园匹。
NSCache
類(lèi):一個(gè)類(lèi)似于集合的容器。它存儲(chǔ)key-value對(duì)劫灶,這一點(diǎn)類(lèi)似于NSDictionary類(lèi)裸违。我們通常用使用緩存來(lái)臨時(shí)存儲(chǔ)短時(shí)間使用但創(chuàng)建昂貴的對(duì)象。重用這些對(duì)象可以優(yōu)化性能本昏,因?yàn)樗鼈兊闹挡恍枰匦掠?jì)算供汛。另外一方面,這些對(duì)象對(duì)于程序來(lái)說(shuō)不是緊要的涌穆,在內(nèi)存緊張時(shí)會(huì)被丟棄紊馏。清理緩存圖片的策略:特別是最大緩存空間大小的設(shè)置。如果所有緩存文件的總大小超過(guò)這一大小蒲犬,則會(huì)按照文件最后修改時(shí)間的逆序朱监,以每次一半的遞歸來(lái)移除那些過(guò)早的文件,直到緩存的實(shí)際大小小于我們?cè)O(shè)置的最大使用空間原叮。
對(duì)圖片的解壓縮操作:這一操作可以查看
SDWebImageDecoder.m
中+decodedImageWithImage
方法的實(shí)現(xiàn)赫编。對(duì)
GIF
圖片的處理對(duì)
WebP
圖片的處理
- 系統(tǒng)級(jí)內(nèi)存警告如何處理
- 取消當(dāng)前正在進(jìn)行的所有下載操作
[[SDWebImageManager sharedManager] cancelAll];
- 清除緩存數(shù)據(jù):
內(nèi)存緩存:直接刪除文件,重新創(chuàng)建新的文件
磁盤(pán)緩存:刪除過(guò)期的文件數(shù)據(jù)奋隶,計(jì)算當(dāng)前未過(guò)期的已經(jīng)下載的文件數(shù)據(jù)的大小擂送,如果發(fā)現(xiàn)該數(shù)據(jù)大小大于我們?cè)O(shè)置的最大緩存數(shù)據(jù)大小,那么程序內(nèi)部會(huì)按照按文件數(shù)據(jù)緩存的時(shí)間從遠(yuǎn)到近刪除唯欣,知道小于最大緩存數(shù)據(jù)為止嘹吨。
- 如何播放
gif
圖片
- 把用戶傳入的gif圖片->
NSData
- 根據(jù)該
Data
創(chuàng)建一個(gè)圖片數(shù)據(jù)源(NSData
->CFImageSourceRef
) - 計(jì)算該數(shù)據(jù)源中一共有多少幀,把每一幀數(shù)據(jù)取出來(lái)放到圖片數(shù)組中
- 根據(jù)得到的數(shù)組+計(jì)算的動(dòng)畫(huà)時(shí)間-》可動(dòng)畫(huà)的image
- [UIImage animatedImageWithImages:images duration:duration];
- 如何判斷當(dāng)前圖片類(lèi)型
+ (NSString *)sd_contentTypeForImageData:(NSData *)data;
圖片的十六進(jìn)制數(shù)據(jù), 的前8個(gè)字節(jié)都是一樣的, 所以可以通過(guò)判斷十六進(jìn)制來(lái)判斷圖片的類(lèi)型
五. 最后
送上一張自己喜歡的圖片:
個(gè)人小結(jié)境氢,有興趣的朋友可以看一下蟀拷,如果覺(jué)得不錯(cuò),麻煩給個(gè)喜歡或star,若發(fā)現(xiàn)問(wèn)題請(qǐng)及時(shí)反饋萍聊,謝謝问芬!