1.SDWebImage有什么用
SDWebImage為UIImageView、UIImage、UIButton添加webcache分類
異步下載圖片
自動(dòng)的異步緩存,包括內(nèi)存緩存和磁盤緩存
在后臺(tái)解壓圖片
保證相同的url不會(huì)被重復(fù)訪問多次,保證錯(cuò)誤的url不會(huì)被反復(fù)請(qǐng)求
保證不會(huì)阻塞主線程
用block和arc實(shí)現(xiàn)
2.框架結(jié)構(gòu)
管理類:SDWebImageManager
處理緩存類:SDImageCache(基于NSCache類,線程安全的)
處理圖片下載:SDWebImageDownloader->SDWebImageDownloaderOperation(真正處理下載操作的類)
3.關(guān)于內(nèi)存不足時(shí),清除緩存
當(dāng)內(nèi)存不足,發(fā)生內(nèi)存警告到時(shí)候需要清除緩存。不同的警告有不同的清除緩存的方式。
當(dāng)監(jiān)聽到UiApplicationDidReceiveMemoryWarningNotification的時(shí)候,也就是系統(tǒng)級(jí)的內(nèi)存警告,會(huì)調(diào)用clearMemory方法。
//清除內(nèi)存緩存
- (void)clearMemory {
? ? //把所有的內(nèi)存緩存都刪除
? ? [self.memCache removeAllObjects];
}
當(dāng)監(jiān)聽到UIApplicationWillTerminateNotification警告的時(shí)候,程序即將終止的時(shí)候(按home鍵),調(diào)用cleanDisk方法。
當(dāng)監(jiān)聽到UIApplicationDidEnterBackgroundNotification警告的時(shí)候,調(diào)用backgroundCleanDisk方法。
cleanDisk清除過期緩存,清除了過期緩存之后計(jì)算當(dāng)前緩存,和設(shè)置的最大緩存數(shù)做比較,如果當(dāng)前緩存大于最大緩存,要繼續(xù)清除,清除的時(shí)候,按照文件創(chuàng)建的時(shí)間順序,從最舊的開始清除。最大緩存數(shù)量:maxCacheSize,緩存圖像總大小,以字節(jié)為單位,默認(rèn)數(shù)值為0,表示不作限制。
過期時(shí)間:7天maxCacheAge
clearDisk直接把緩存全部刪除,然后重新創(chuàng)建一個(gè)文件夾。
清空過期的磁盤緩存:
//清除過期的磁盤緩存- (void)cleanDisk { [self cleanDiskWithCompletionBlock:nil];}
刪除過期磁盤緩存的具體步驟:
1.計(jì)算過期日期,比這個(gè)日期還早的文件就是過期文件
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
2.遍歷緩存路徑中的所有文件,刪除早于過期日期的所有文件,并保存文件屬性來計(jì)算緩存占用空間大小。
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey]; if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) { [urlsToDelete addObject:fileURL]; continue; }
??:[modificationDate laterDate:expirationDate]返回的是modificationDate和:expirationDate中更晚的一個(gè)日期,如果返回expirationDate就代表這個(gè)文件最早的修改時(shí)間比過期日期還早,這個(gè)時(shí)候就要?jiǎng)h除它。
3.// 刪除過期的文件 for (NSURL *fileURL in urlsToDelete) { [_fileManager removeItemAtURL:fileURL error:nil]; }
4.計(jì)算磁盤緩存,如果剩余磁盤緩存超過最大限額繼續(xù)刪除,從最舊的文件開始刪除。
??:面試的時(shí)候被問到了一個(gè)問題,如何去判斷一個(gè)圖片有沒有超過最大緩存時(shí)間呢?
圖片的修改時(shí)間就是圖片的一個(gè)屬性,用這個(gè)時(shí)間和最大緩存時(shí)間去比較,如果比最大緩存時(shí)間還早就說明是過期圖片。
FOUNDATION_EXPORT NSString * const NSURLContentModificationDateKey NS_AVAILABLE(10_6, 4_0);
4.下載圖片核心代碼
給UIImageView設(shè)置圖片需要用到UIImageView+WebCache這個(gè)類提供的接口,這個(gè)類提供了多個(gè)加載圖片的借口,但是核心都是調(diào)用一個(gè)方法。
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {}
這個(gè)方法的實(shí)現(xiàn)流程:
1. 取消當(dāng)前圖像下載?
[self sd_cancelCurrentImageLoad];
下載圖片之前先取消下載這個(gè)UIImageView之前要下載的圖片。因?yàn)橐呀?jīng)要下載新的一張圖片了,原來要下載什么已經(jīng)不重要了。這個(gè)方法的本質(zhì)是把operationDictionary字典中對(duì)應(yīng)的操作移除了。這個(gè)operationDictionary字典里存儲(chǔ)了所有的下載操作。
??:這個(gè)方法可以用來解決一個(gè)問題,因?yàn)閁ITableView里的cell是重用的,一個(gè)cell上的imageView開啟了圖片下載的方法,這個(gè)時(shí)候這個(gè)cell被重用了,新的cell上的imageView又開啟了一個(gè)下載方法,兩個(gè)下載操作回調(diào)給同一個(gè)imageView,就會(huì)造成數(shù)據(jù)的錯(cuò)亂。所以,在開始下載之前要把之前的下載操作取消。
2.設(shè)置占位圖片placeholder
//判斷,如果傳入的下載策略不是延遲顯示占位圖片,那么在主線程中設(shè)置占位圖片
?if (!(options & SDWebImageDelayPlaceholder)) {
? ? ? ? dispatch_main_async_safe(^{
? ? ? ? ? ? // 設(shè)置占位圖像
? ? ? ? ? ? self.image = placeholder;
? ? ? ? });
? ? }
3.判斷url是否為空,如果為空則生成一個(gè)錯(cuò)誤信息,把錯(cuò)誤信息回傳
4.如果url不為空,創(chuàng)建一個(gè)新的下載操作
id operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {}
創(chuàng)建下載操作時(shí),會(huì)調(diào)用SDWebImageManager中的方法
- (id)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options ?progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {};
這個(gè)方法的邏輯:
4.1.檢查圖片的URL,先判斷URL的數(shù)據(jù)類型是否正確,如果不正確需要賦值為nil,防止因參數(shù)類型錯(cuò)誤導(dǎo)致程序崩潰。
if (![url isKindOfClass:NSURL.class]) { url = nil; }
4.2.檢查URL是否在URL黑名單里,黑名單里存儲(chǔ)曾經(jīng)下載失敗的URL。這也就避免了請(qǐng)求失敗的URL不會(huì)被多次請(qǐng)求。
@synchronized (self.failedURLs) {
? ? ? ? isFailedUrl = [self.failedURLs containsObject:url];
? ? }failedURLs是一個(gè)NSMutableSet,用來存放請(qǐng)求失敗的URL。
4.3.如果URL不正確,或者URL存放在URL黑名單里但是沒有選擇請(qǐng)求失敗重新下載的策略,就直接返回。回調(diào)completedBlock塊,把錯(cuò)誤信息返回。
4.4.URL正確,添加當(dāng)前任務(wù)到正在下載的任務(wù)數(shù)組中。
@synchronized (self.runningOperations) {
? ? ? ? [self.runningOperations addObject:operation];
? ? }
4.5.根據(jù)URL生成一個(gè)key,用來對(duì)應(yīng)圖片的緩存。
NSString *key = [self cacheKeyForURL:url];
4.6.根據(jù)key值檢查圖片緩存是否存在,在SDWebImageManager里調(diào)用
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {}方法。
相當(dāng)于調(diào)用SDImageCache里的
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {}這個(gè)方法。
先檢查緩存對(duì)應(yīng)的key是否為空,如果為空直接返回doneBlock。
先檢查圖片在內(nèi)存緩存中是否存在,
UIImage *image = [self imageFromMemoryCacheForKey:key];
? ? if (image) {
? ? ? ? doneBlock(image, SDImageCacheTypeMemory);
? ? ? ? return nil;}
如果圖片存在,就直接把圖片返回,并且把圖片的緩存方式(內(nèi)存緩存)返回。檢查的時(shí)候其實(shí)就是用kvc在memCache這個(gè)NSCache里檢查有沒有對(duì)應(yīng)的value。稍后我們具體看一下NSCache是怎么用的。
如果內(nèi)存緩存不存在,就去檢查磁盤緩存。
開啟子線程,檢查圖片是否在磁盤緩存里。
UIImage *diskImage = [self diskImageForKey:key];
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key]; ?如果data為空,就代表緩存沒有命中。
根據(jù)key在默認(rèn)路徑和自定義路徑下面找:
NSString *defaultPath = [self defaultCachePathForKey:key];
NSArray *customPaths = [self.customPaths copy];
NSString *filePath = [self cachePathForKey:key inPath:path];//這個(gè)path是存在customPath這個(gè)數(shù)組里的,用戶自定義的路徑。
如果磁盤緩存存在,計(jì)算圖片的大小,然后保存到內(nèi)存緩存里。
if (diskImage && self.shouldCacheImagesInMemory) {
? ? ? ? ? ? ? ? NSUInteger cost = SDCacheCostForImage(diskImage);
? ? ? ? ? ? ? ? [self.memCache setObject:diskImage forKey:key cost:cost]; }
然后把圖片和圖片的緩存方式(磁盤緩存)返回,在主線程返回。
如果內(nèi)存緩存和磁盤緩存都沒有命中,就開始下載圖片。
4.7.下載圖片調(diào)用SDWebImageDownloader的方法。設(shè)置一個(gè)下載超時(shí)時(shí)間,默認(rèn)是15秒。真正的下載操作在SDWebImageDownloaderOperation里。
創(chuàng)建一個(gè)SDWebImageDownloaderOperation對(duì)象,當(dāng)這個(gè)SDWebImageDownloaderOperation對(duì)象被添加到隊(duì)列downloadQueue的時(shí)候,就會(huì)自動(dòng)調(diào)用start方法。在start方法中,創(chuàng)建NSURLConnection請(qǐng)求。
//創(chuàng)建NSURLConnection對(duì)象,并設(shè)置代理(沒有馬上發(fā)送請(qǐng)求)
? ? ? ? self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
//獲得當(dāng)前線程 self.thread = [NSThread currentThread];
發(fā)出開始下載圖片的請(qǐng)求,然后開啟runloop直到請(qǐng)求結(jié)束。
[self.connection start]; //發(fā)送網(wǎng)絡(luò)請(qǐng)求
//開啟Runloop ? CFRunLoopRun();
下載圖片的過程中通過NSURLConnection的代理方法接受數(shù)據(jù),第一個(gè)方法 - connection:didReceiveResponse: 被調(diào)用后,接著會(huì)多次調(diào)用 - connection:didReceiveData: 方法來更新進(jìn)度、拼接圖片數(shù)據(jù),當(dāng)圖片數(shù)據(jù)全部下載完成時(shí),- connectionDidFinishLoading: 方法就會(huì)被調(diào)用。
??:下面具體分析一下這三個(gè)代理方法的實(shí)現(xiàn):
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
1.首先要判斷請(qǐng)求回傳的狀態(tài)碼,如果狀態(tài)碼是請(qǐng)求成功,就繼續(xù)執(zhí)行,如果是304或者是其他請(qǐng)求失敗狀態(tài)碼就取消請(qǐng)求操作。
2.//初始化可變的Data用來接收?qǐng)D片數(shù)據(jù) self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
3. //得到請(qǐng)求的響應(yīng)頭信息 self.response = response;
4.//注冊(cè)通知中心,在主線程中發(fā)送通知SDWebImageDownloadReceiveResponseNotification【接收到服務(wù)器的響應(yīng)】 dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self]; });}
接受到服務(wù)器返回的數(shù)據(jù)以后調(diào)用該方法,并且調(diào)用多次。
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
//不斷拼接接收到的圖片數(shù)據(jù)(二進(jìn)制數(shù)據(jù))
[self.imageData appendData:data];
處理圖片下載的進(jìn)度
}
圖片下載完成以后,調(diào)用該方法:
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
@synchronized(self) {
? ? ? ? //關(guān)停當(dāng)前的runloop
? ? ? ? CFRunLoopStop(CFRunLoopGetCurrent());
? ? ? ? //把線程和連接對(duì)象清空
? ? ? ? self.thread = nil;
? ? ? ? self.connection = nil;
? ? ? ? //在主線程中發(fā)出通知:
? ? ? ? //SDWebImageDownloadStopNotification? ? 任務(wù)停止
? ? ? ? //SDWebImageDownloadFinishNotification? 任務(wù)完成
? ? ? ? dispatch_async(dispatch_get_main_queue(), ^{
? ? ? ? ? ? [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
? ? ? ? ? ? [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
??:如果請(qǐng)求過程中發(fā)生了錯(cuò)誤,就只發(fā)送請(qǐng)求停止的通知,而不要發(fā)送請(qǐng)求結(jié)束的通知。
? ? ? ? });
把得到的圖片的二進(jìn)制數(shù)據(jù)轉(zhuǎn)換成圖片
UIImage *image = [UIImage sd_imageWithData:self.imageData];
根據(jù)url獲得緩存key,對(duì)圖片進(jìn)行縮放和解壓
if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:image]; }
? ? }
}
4.8.下載完成后回到SDWebImageManager,根據(jù)得到的圖片進(jìn)行緩存處理(SDImageCache)。
4.9.把operation返回,給控件設(shè)置圖片。
5.將創(chuàng)建好的操作添加到操作字典里?,實(shí)現(xiàn)多張圖片同時(shí)下載
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
5.如何給圖片命名
將圖片的url進(jìn)行md5加密,得到一個(gè)字符串就是圖片的名稱。
6.如何判斷圖片類型
只判斷圖片的第一個(gè)字節(jié),因?yàn)椴煌愋偷氖M(jìn)制數(shù)據(jù)的第一個(gè)字節(jié)是不一樣的,相同類型圖片的第一個(gè)字節(jié)是一樣的。
7.??
使用SDWebImageManager來下載圖片,自動(dòng)就會(huì)進(jìn)行圖片的內(nèi)存緩存和磁盤緩存。
單獨(dú)使用SDWebImageDownloader下載圖片,不會(huì)進(jìn)行緩存。
單獨(dú)使用SDImageCache異步緩存圖片,緩存圖片時(shí)可以選擇緩存策略,是同時(shí)進(jìn)行內(nèi)存緩存和磁盤緩存,還是單獨(dú)選擇一種緩存方式。
讀取緩存的時(shí)候,根據(jù)圖片url生成的唯一的key,就可以在緩存里讀取到對(duì)應(yīng)的圖片。
8.圖片解碼
使用SDWebImageDecoder進(jìn)行圖片解碼
首先我們要知道為什么要對(duì)圖片進(jìn)行解碼,從網(wǎng)絡(luò)上下載下來的數(shù)據(jù)被緩存在磁盤里是png或者jpeg的格式,這種格式的圖片是不能直接顯示在imageView上面的,需要進(jìn)行解壓,將圖片解壓成位圖,這個(gè)過程是非常消耗CPU的。
??:兩種加載圖片的方式
[UIImageView setImage:XXX];
UIImageView的setImage的時(shí)候,這時(shí)候內(nèi)存才會(huì)增加.并且這時(shí)候你將imageView移除,內(nèi)存也不會(huì)有減少。
[UIImage ImageNamed: @"xxx.jpg"]
不適合加載大的不常用的圖片.因?yàn)樗鼤?huì)默認(rèn)在程序里保存這張圖片數(shù)據(jù)(不會(huì)隨ImageView的移除而移除).只有經(jīng)常使用圖片適合這種方式加載.
因?yàn)榻獯a的過程是在調(diào)用setImage方法的時(shí)候才進(jìn)行的,這個(gè)過程默認(rèn)是在主線程執(zhí)行的,為了防止主線程的卡頓,應(yīng)該將其優(yōu)化到在子線程執(zhí)行。
在子線程執(zhí)行圖片的解壓過程,然后對(duì)解壓后的圖片進(jìn)行緩存,在主線程顯示圖片的時(shí)候直接就可以用了。
9.URL如何管理?
就是我們請(qǐng)求加載的圖片地址,是以什么方式保存管理的呢?答案是Runtime中的objc_setAssociatedObject及objc_getAssociatedObject方法,在運(yùn)行時(shí)動(dòng)態(tài)的將url值綁定到具體的對(duì)象(例如ImageView)中,以imageURLKey全局變量作為綁定值的key。
10.SDWebImage如何做到相同的url不去請(qǐng)求多次?
我們可以認(rèn)為url和圖片是一一對(duì)應(yīng)的,也就是說我們請(qǐng)求了這個(gè)url以后對(duì)應(yīng)的圖片就被保存到了緩存里,這個(gè)時(shí)候也就不需要重新發(fā)送url去請(qǐng)求了。而我們?yōu)榱烁鶕?jù)url找到緩存里的圖片,圖片的名稱就是對(duì)url進(jìn)行md5加密以后的字符串。
??:還有一個(gè)延伸的問題,就是如何保證在同一時(shí)間請(qǐng)求相同的url,只請(qǐng)求一次。因?yàn)橥粫r(shí)間發(fā)請(qǐng)求,那么請(qǐng)求到的結(jié)果還沒有緩存起來,就沒辦法根據(jù)緩存避免重復(fù)的請(qǐng)求。
解決方案:讀SDWebImage庫系列(1)-如何保證同一時(shí)間請(qǐng)求相同URL時(shí),只進(jìn)行一次網(wǎng)絡(luò)請(qǐng)求
有一個(gè)字典self.URLCallbacks,這個(gè)字典保存著url為key,self.URLCallbacks中的value是一個(gè)數(shù)組callbacksForURL,這是一個(gè)以字典為元素的數(shù)組,保存一些不同類型的回調(diào)如completeblock,progress block等,我們根據(jù)url找到callbacksForURL數(shù)組,就能找到個(gè)鐘類型的回調(diào),可以知道這個(gè)url請(qǐng)求的執(zhí)行狀態(tài)。
11.如果url相同但是url里圖片的內(nèi)容改變了怎么辦?
為什么會(huì)發(fā)生這種情況呢,就是我們默認(rèn)的url不改變的話那么圖片也不變,我們用這個(gè)url去請(qǐng)求的時(shí)候系統(tǒng)認(rèn)為他已經(jīng)在緩存里了,就不會(huì)重新去下載,這個(gè)時(shí)候要怎么解決呢?
將加載圖片的策略設(shè)置成SDWebImageRefreshCached。
NSURL *imgURL = [NSURL URLWithString:@"http://handy-img-storage.b0.upaiyun.com/3.jpg"];
[[self imageView] sd_setImageWithURL: imgURL ? placeholderImage:nil ?options:SDWebImageRefreshCached];?
然后修改SDWebImageDownloader的headersFilter,讓開發(fā)者對(duì)所有的圖片請(qǐng)求設(shè)置一些額外的header。
給請(qǐng)求添加一個(gè)If-Modified-Since屬性。
思路就是:
與服務(wù)器返回的Last-Modified相對(duì)應(yīng)的request header里可以加一個(gè)名為If-Modified-Since的key,value即是服務(wù)器回傳的服務(wù)端圖片最后被修改的時(shí)間,第一次圖片請(qǐng)求時(shí)If-Modified-Since的值為空,第二次及以后的客戶端請(qǐng)求會(huì)把服務(wù)器回傳的Last-Modified值作為If-Modified-Since的值傳給服務(wù)器,這樣服務(wù)器每次接收到圖片請(qǐng)求時(shí)就將If-Modified-Since與Last-Modified進(jìn)行比較。如果不同就代表圖片被更新了,返回200,如果返回304就代表這兩個(gè)值相同。