要寫點關於SDWebImage的文章了,這段時間看的不少,總體的感受是SDWebImage的代碼不如AFN那么規整、有條理,並沒有吐槽的意思,稍微細細看一下就會有這樣的感受。本篇文章不會用大量的篇幅來介紹SDWebImage如何使用,而是更多地介紹SDWebImage的整體思路和一些實現細節,還有介紹一些不是特別常用的一些功能(因為有不少iOS開發人員還只是會使用sd_setImageWithURL)。首先我們要看一下SDWebImage的整體結構:
這里我要說明的一點是我當前使用SD的git提交版本是e41af47e2f5de9317d55083e23168e076b550e34(Sat Jan 30 02:54:23 2016 +0100)。讓我們看一下這張圖的內容。
可以將SDWebImage的框架分為三個部分:
1.適配
SDWebImage
iOS版本、編譯指令的適配、線程切換的宏、還有一個導出的內聯函數,用於根據image的命名key 將image轉換成相應的scale的UIImage類型以完成對Image的縮放,如文件名帶@2x,將按照2倍縮放。
2.Util工具
核心的類就是SDWebImageManager它負責創建和管理下載任務、對緩存操作進行管理,我們通常使用的UIImageView的WebCache分類下的sd_setImageWithURL方法的實現就依賴於這個類,其他View分類的設置圖片的方法也實現也類似。
SDWebImageManager實現下載依賴於下載器:SDWebImageDownloader,下載器負責管理下載任務,而執行下載任務是由SDWebImageDownloaderOperation操作完成。
SDWebImageManager實現緩存依賴於緩存管理:SDImageCache,能夠完成圖片的內存緩存和磁盤緩存,還可以查詢指定url的圖片是否進行了緩存、取出緩存等操作。
下載和緩存的過程中會調用適配模塊進行將圖片轉為合適的尺寸,使用解壓模塊將被壓縮的圖片解壓后完成緩存。
3.分類
包括兩部分:①.視圖分類、②.用於圖片格式處理和格式轉換的模塊。
①.視圖分類
視圖分類中有一個基本的分類:
UIView+WebCacheOperation這個分類用於完成將組合操作(SD定義了能夠實現下載和緩存的組合操作類SDWebImageCombinedOperation)與View綁定、取消綁定和移除綁定等功能。其他視圖分類的實現都依賴於這個分類。
MKAnnotationView+WebCache、UIImageView+WebCache、UIImageView+HighlightedWebCache對view中的圖片的加載過程的實現比較相似(后面會介紹),UIButton+WebCache分類中針對UIButton的不同的State可以設置不同的image。
②.用於圖片格式處理和格式轉換的模塊
NSData+ImageContentType這個分類只有一個方法sd_contentTypeForImageData:,是根據圖片的二進制data的第一個字節的數據,得到圖片相應的MIME類型。
UIImage+MultiFormat也只有一個方法sd_imageWithData:,根據傳入的NSData,讀取到MIME類型然后轉換成對應的UIImage。
UIImage+GIF根據傳入的值如文件名或者NSData,得到對應的GIF圖的UIImage對象,實際上是一個animatedImage。
UIImage+WebP根據傳入的NSData,得到對應的WebP圖的UIImage對象,這個方法的實現依賴於WebP庫,需要到google下載libwebp。
以上是從代碼的角度分析了SD可以完成的工作,而在github上SD的主頁可以看到,它的自我介紹中的主打功能:
提供UIImageView的一個分類,以支持網絡圖片的加載與緩存管理
一個異步的圖片加載器
一個異步的內存+磁盤圖片緩存
支持GIF圖片
支持WebP圖片
后台圖片解壓縮處理
確保同一個URL的圖片不被下載多次
確保虛假的URL不會被反復加載
確保下載及緩存時,主線程不被阻塞
本篇文章的內容主要涉及到4個類:SDWebImageDownloaderOptions
、SDWebImageDownloader
、SDImageCache
、SDWebImageManager
,詳細介紹如何實現下載和緩存的以及如何在這個過程中做到上面提到的‘三個確保’。至於其他內容(如GIF和WebP圖片的加載)以后會一一介紹。
每一個NSOperation都是為了完成一項任務而誕生的,而SDWebImageDownloaderOperation
的任務就是負責依照指定的下載選項,使用將指定的urlRequest創建NSURLConnection對象進行網絡連接(NSURLConnection對象的代理就是SDWebImageDownloaderOperation自己),進行對圖片的下載。在下載的過程中對圖片數據進行拼接,可以實現對進度progress的跟蹤,在下載之后可以將接收到的圖片數據轉換、解壓等,並完成一個下載完成的回調。如果網路訪問過程中接收到質詢,則使用服務端憑據或者本地存儲的憑據處理質詢;如果下載失敗了,則發送錯誤通知,執行完成回調,並結束下載任務。
SDWebImageDownloaderOperation
類主要有以下幾個屬性:
1.NSURLRequest *request
:下載時進行網絡請求的request,由構造方法傳入。
2.BOOL shouldDecompressImages
:下載后是否需要解壓圖片。
3.BOOL shouldUseCredentialStorage
:URLConnection是否需要咨詢憑據倉庫來對連接進行授權,默認是YES。
這是NSURLConnectionDelegate的-connectionShouldUseCredentialStorage:方法的返回值
4.NSURLCredential *credential
:在-connection:didReceiveAuthenticationChallenge:
方法中驗證質詢時使用的憑據
已經存在的request,URL的用戶名或密碼構成的憑據會覆蓋這個值,具體解釋參見SDWebImageDownloader部分。
5.SDWebImageDownloaderOptions options
:readonly下載選項,由構造方法傳入。
6.NSInteger expectedSize
:預期的文件長度,使用NSInteger完全夠用。
7.NSURLResponse *response
:connection對象進行網絡訪問,接收到的的response
要注意的是:下載選項是在SDWebImageDownloader
中定義的,SDWebImageDownloader
是下載器負責管理下載隊列和控制下載過程(通過調用SDWebImageDownloaderOperation
的方法)。下載選項SDWebImageDownloaderOptions
的定義如下:
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
SDWebImageDownloaderLowPriority = 1 << 0,
/// 漸進式下載,如果設置了這個選項,會在下載過程中,每次接收到一段chunk數據就調用一次完成回調(注意是完成回調)回調中的image參數為未下載完成的部分圖像
SDWebImageDownloaderProgressiveDownload = 1 << 1,
/// 通常情況下request阻止使用NSURLCache. 這個選項會用默認策略使用NSURLCache
SDWebImageDownloaderUseNSURLCache = 1 << 2,
/// 如果從NSURLCache中讀取圖片,會在調用完成block時,傳遞空的image或imageData \
* (to be combined with `SDWebImageDownloaderUseNSURLCache`).
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
/// 系統為iOS 4+時,如果應用進入后台,繼續下載。這個選項是為了實現在后台申請額外的時間來完成請求。如果后台任務到期,操作會被取消。
SDWebImageDownloaderContinueInBackground = 1 << 4,
/// 通過設置NSMutableURLRequest.HTTPShouldHandleCookies = YES的方式來處理存儲在NSHTTPCookieStore的cookies
SDWebImageDownloaderHandleCookies = 1 << 5,
/// 允許不受信任的SSL證書,在測試環境中很有用,在生產環境中要謹慎使用
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
/// 將圖片下載放到高優先級隊列中
SDWebImageDownloaderHighPriority = 1 << 7,
};
這些選項主要涉及到下載的優先級、緩存、后台任務執行、cookie處理以及證書認證幾個方面,在創建下載操作的時候可以使用組合的選項以完成一些特殊的需求。
SDWebImageDownloaderOperation
只對外提供了一個對象方法- initWithRequest: options: progress: completed: cancelled:
,它使用默認的屬性值初始化一個SDWebImageDownloaderOperation
對象。
下面我們看一下SDWebImageDownloaderOperation
對NSOperation的-start
方法的重寫,畢竟這是完成下載任務的核心代碼。以下是將-start提取出來的部分代碼
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset]; // 將各個屬性置空。包括取消回調、完成回調、進度回調,用於網絡連接的connection,用於拼接數據的imageData、記錄當前線程的屬性thread。
return;
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
// 使用UIApplication的beginBackgroundTaskWithExpirationHandler方法向系統借用一點時間,繼續執行下面的代碼來完成connection的創建和進行下載任務。
// 在后台任務執行時間超過最大時間時,也就是后台任務過期執行過期回調。在回調主動將這個后台任務結束。
/*
^{
__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]; // 創建用於下載的connection
self.thread = [NSThread currentThread]; // 記錄當前線程
}
[self.connection start];
if (self.connection) {
if (self.progressBlock) { // 任務開始立刻執行一次進度回調
self.progressBlock(0, NSURLResponseUnknownLength);
}
dispatch_async(dispatch_get_main_queue(), ^{ // 發送開始下載的通知,object為operation本身
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
}
else {
CFRunLoopRun();
}
// 當runloop開啟之后,線程切換到runloop中的任務,開始下載圖片,所以下面的代碼是經過一段時間的延遲執行的,也就是當connection的網絡訪問進行之后,才會執行下面的代碼。
// 這個時候可以進行一些判斷,如圖片是否被正確地下載完成。
if (!self.isFinished) {
[self.connection cancel];
// NSURLConnectionDelegate代理方法
// 主動調用 並制造一個錯誤,這樣做的目的是因為這個方法一旦調用,代理就不會再接收connection的消息,也就是不在調用其他的任何代理方法了,connection徹底結束。
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
} else { // connectin 創建失敗,這里直接執行完成回調,並傳遞一個connection沒有初始化的錯誤
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
// 運行到這里說明下載操作已經完成(無論成功還是失敗),因此沒有必要在后台運行。使用endBackgroundTask:
#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
這些就是一次下載操作要執行的任務,但是數據處理是下載任務的關鍵,SDWebImageDownloaderOperation
通過NSURLConnection的代理方法完成對下載的圖片的數據處理,主要用到以下幾個方法:
// NSURLConnectionDataDelegate中聲明
connection: didReceiveResponse: // 接收到服務端的response時,執行一次
connection: didReceiveData: // 每次接收到chunk數據都會調用
connectionDidFinishLoading: // 當連接結束的時候調用一次
connection: willCacheResponse: // 要進行緩存之前調用
// NSURLConnectionDelegate中聲明
connection: didFailWithError: // 連接失敗,或者沒有成功下載完成調用
connectionShouldUseCredentialStorage: // 指定是否需要使用本地憑據進行驗證
connection: willSendRequestForAuthenticationChallenge: // 處理服務端過來的質詢
下面我們看一下除了上面標注的一些基本的功能外,SDWebImageDownloaderOperation
在每個方法內部還有哪些細節性的工作。
1.connection: didReceiveResponse:
主要完成以下工作:
if (statusCode<400並且不等於304) {
// 設置預期文件長度屬性的值
// 立刻完成一次進度回調,傳遞的參數為0,
// 初始化用於拼接圖片二進制數據的屬性imageData
// 設置response屬性為服務端返回的response值
// 向主隊列同步發送一個接收到response的通知
} else {
// 如果statusCode為304,也就是服務端Not Modified並且拿到了本地的HTTP緩存,取消操作,發送操作停止的通知,執行完成回調,停止當前的runloop,設置下載完成標記為YES,正在執行標記為NO,將屬性置空。
}
2.connection: didReceiveData:
使用自身屬性imageData拼接接收的數據
if (下載選項設置了SDWebImageDownloaderProgressiveDownload) {
取得已經拼接完的imageData,創建一個CGImageSourceRef類型的imageSouce,使用imageSouce創建CGImageRef類型的對象partialImageRef,代表着要下載的圖片的一部分,調整方向並將使用`UIImage imageWithCGImage:partialImageRef`將其導出為UIImage,釋放掉partialImageRef,並在主線程同步執行一次完成回調,指定第一個參數為剛才到處的UIImage,最后釋放掉imageSource占用的空間。
}
執行一次進度回調progressBlock,第一個參數傳遞已經拼接的imageData的長度
3.connectionDidFinishLoading:
執行完這個方法之后,代理不會再接收任何connection發送的消息,意味着下載完成。通常情況下,下載任務正常結束之后,就會執行一次這個方法。
@synchronized(self) {
停止當前的RunLoop,將connection屬性和thread屬性置空,發送下載停止的通知。
}
檢查sharedURLCache是否緩存了這次下載response,如果沒有就將responseFromCached設置為NO
執行完成回調completionBlock,並根據是否讀取了緩存、圖片尺寸是否為(0,0)等條件向完成回調傳遞不同的值。
將完成狀態、執行狀態的標記復位、將屬性置空
4.connection: didFailWithError:
執行完這個方法之后,代理不會再接收任何connection發送的消息,意味着下載失敗。通常情況下,下載任務非正常結束,就會執行一次這個方法。
@synchronized(self) {
停止當前的RunLoop,將connection屬性和thread屬性置空,發送下載停止的通知。
}
if (self.completedBlock) { // 只使用這一種參數傳遞的方式完成回調
self.completedBlock(nil, nil, error, YES);
}
將完成狀態、執行狀態的標記復位、將屬性置空
5.connection: willCacheResponse:
緩存response之前調用一次這個方法,給connection的代理一次機會改變它。可以返回一個修改之后的response,或者返回nil不存儲緩存。
SDWebImageDownloaderOperation
在這個方法內部完成了以下工作:
responseFromCached = NO; // 標記這次下載的圖片不是從緩存中讀取出來的
if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
return nil; // 如果request的緩存策略(實際上Downloader在使用操作進行下載的時候,會根據下載選項修改request的緩存策略)是忽略本地緩存,不進行不進行緩存
} else {
return cachedResponse; // 其他情況,正常進行緩存
}
6.connectionShouldUseCredentialStorage:
這個代理方法的返回值決定URL加載器是否需要使用存儲的憑據對網絡進行授權驗證。
SDWebImageDownloaderOperation
中這樣實現:
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection __unused *)connection {
return self.shouldUseCredentialStorage; // shouldUseCredentialStorage屬性的init方法中賦初值YES,提供了對外的setter,可以在外部修改這個值。
}
7.connection: willSendRequestForAuthenticationChallenge:
服務端發起一個質詢,需要在這個方法中解決。SDWebImageDownloaderOperation
對這個方法的實現比較復雜:
if (服務端要求的認證方式是信任認證) {
如果下載選項沒有設置允許無效的SSL證書這個下載選項,那么按照默認的方式處理質詢
其他情況,就直接使用服務端發過來的憑據繼續訪問
} else {
如果這個質詢之前沒有授權失敗過且self.credential存在(也就是想操作賦值了一個本地的憑據),使用self.credential作為憑據處理質詢
其他情況直接使用沒有憑據的方式處理質詢。
}
以上就是所有的SDWebImageDownloaderOperation
內部實現的NSURLConnection的代理方法,這些方法已經能夠很好地完成網絡訪問、圖片下載和數據處理。
SDWebImageDownloaderOperation
中還定義了一些取消操作的方法,用於暫停下載任務,這些方法比較簡單,這里不再一一贅述。
在上面的數據處理全部的過程中,我們發現時刻都在使用者下載選項
,可見熟悉每個下載選項和使用時機的重要性。接下來看一下負責管理下載操作的SDWebImageDownloader
類,同時下載選項枚舉也是在這個類的頭文件中聲明的。
// 所有的下載操作以棧類型 (后進先出)執行.
SDWebImageDownloaderLIFOExecutionOrder
};
默認是`SDWebImageDownloaderFIFOExecutionOrder`,是在init方法中設置的。如果設置了后進先出,在下載操作添加到下載隊列中時,會依據這個值添加依賴關系,使得最后添加操作出在依賴關系鏈條中的第一項,因而會優先下載最后添加的操作任務。
`SDWebImageDownloader`還提供了其他幾個重要的對外接口(包括屬性和方法):
1.`BOOL shouldDecompressImages`
是否需要解壓,在init中設置默認值為YES,在下載操作創建之后將值傳遞給操作的同名屬性。
解壓下載或緩存的圖片可以提升性能,但是會消耗很多內存
默認是YES,如果你會遇到因為過高的內存消耗引起的崩潰將它設置為NO。
2.`NSInteger maxConcurrentDownloads`
放到下載隊列中的下載操作的總數,是一個瞬間值,因為下載操作一旦執行完成,就會從隊列中移除。
3.`NSUInteger currentDownloadCount`
下載操作的超時時長默認是15.0,即request的超時時長,若設置為0,在創建request的時候依然使用15.0。
只讀。
4.`NSURLCredential *urlCredential`
為request操作設置默認的URL憑據,具體實施為:在將操作添加到隊列之前,將操作的credential屬性值設置為urlCredential
5.`NSString *username`和`NSString *passwords`
如果設置了用戶名和密碼:在將操作添加到隊列之前,會將操作的credential屬性值設置為`[NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession]`,而忽略了屬性值urlCredential。
6.`- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;`
為HTTP header設置value,用來追加到每個下載對應的HTTP request, 若傳遞的value為nil,則將對應的field移除。
擴展里面定義了一個HTTPHeaders屬性(NSMutableDictionary類型)用來存儲所有設置好的header和對應value。
在創建request之后緊接着會將HTTPHeaders賦給request,request.allHTTPHeaderFields = self.HTTPHeaders;
7.`- (NSString *)valueForHTTPHeaderField:(NSString *)field;`
返回指定的HTTP header field對應的value
8.`SDWebImageDownloaderHeadersFilterBlock headersFilter`
設置一個過濾器,為下載圖片的HTTP request選取header.意味着最終使用的headers是經過這個block過濾之后的返回值。
9.`- (void)setOperationClass:(Class)operationClass;`
設置一個`SDWebImageDownloaderOperation`的子類 ,在每次 SDWebImage 構建一個下載圖片的請求操作的時候作為默認的`NSOperation`使用.
參數operationClass為要設置的默認下載操作的`SDWebImageDownloaderOperation`的子類。 傳遞 `nil` 會恢復為 `SDWebImageDownloaderOperation`。
以下兩個方法是下載控制方法了
`- (id <SDWebImageOperation>)downloadImageWithURL: options: progress: completed:`
這個方法用指定的URL創建一個異步下載實例。
有關completedBlock回調的一些解釋:下載完成的時候block會調用一次.
沒有使用SDWebImageDownloaderProgressiveDownload選項的情況下,如果下載成功會設置image參數,如果出錯,會根據錯誤設置error參數. 最后一個參數總是YES. 如果使用了SDWebImageDownloaderProgressiveDownload選項,這個block會使用部分image的對象有間隔地重復調用,同時finished參數設置為NO,直到使用完整的image對象和值為YES的finished參數進行最后一次調用.如果出錯,finished參數總是YES.
`- (void)setSuspended:(BOOL)suspended;`
設置下載隊列的掛起(暫停)狀態。若為YES,隊列不再開啟新的下載操作,再向隊列里面添加的操作也不會被開啟,但是正在執行的操作依然繼續執行。
下面我們就來看一下下載方法的實現細節:
```objectivec
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self) wself = self;
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
// 創建下載的回調
}];
return operation;
}
重點就是addProgressCallback: completedBlock: forURL: createCallback:
的執行了,SDWebImageDownloader
將外部傳來的進度回調、完成回調、url直接傳遞給這個方法,並實現創建下載操作的代碼塊作為這個方法的createCallback參數值。下面就看一下這個方法的實現細節:
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
// 對URL判空,如果為空,直接執行完成回調。
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
/*
對dispatch_barrier_sync函數的解釋:
向分配隊列提交一個同步執行的barrier block。與dispatch_barrier_async不同,這個函數直到barrier block執行完畢才會返回,在當前隊列調用這個函數會導致死鎖。當barrier block被放進一個私有的並行隊列后,它不會被立刻執行。實際為,隊列會等待直到當前正在執行的blocks執行完畢。到那個時刻,隊列才會自己執行barrier block。而任何放到 barrier block之后的block直到 barrier block執行完畢才會執行。
傳遞的隊列參數應該是你自己用dispatch_queue_create函數創建的一個並行隊列。如果你傳遞一個串行隊列或者全局並行隊列,這個函數的行為和 dispatch_sync相同。
與dispatch_barrier_async不同,它不會對目標隊列進行強引用(retain操作)。因為調用這個方法是同步的,它“借用”了調用者的引用。而且,沒有對block進行Block_copy操作。
作為對其優化,這個函數會在可能的情況下在當前線程喚起barrier block。
*/
// 為確保不會死鎖,當前隊列是另一個隊列,而不能是self.barrierQueue。
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
/*
URLCallbacks字典類型key為NSURL類型,value為NSMutableArray類型,value只包含着一個元素,這個元素是一個NSMutableDictionary類型,它的key為NSString代表着回調類型,value為block,是對應的回調
*/
// 同一時刻對相同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();
/* 解釋
若url第一次綁定它的回調,也就是第一次使用這個url創建下載任務,則執行一次創建回調。
在創建回調中創建下載操作,dispatch_barrier_sync執行確保同一時間只有一個線程操作URLCallbacks屬性,也就是確保了下面創建過程中在給operation傳遞回調的時候能取到正確的self.URLCallbacks[url]值。同時保證后面有相同的url再次創建時,if (!self.URLCallbacks[url])分支不再進入,first==NO,也就不再繼續調用創建回調。這樣就確保了同一個url對應的圖片不會被重復下載。
而下載器的完成回調中會將url從self.URLCallbacks中remove,雖然remove掉了,但是再次使用這個url進行下載圖片的時候,Manager會向緩存中讀取下載成功的圖片了,而不是無腦地直接添加下載任務;即使之前的下載是失敗的(也就是說沒有緩存),這樣繼續添加下載任務也是合情合理的。
// 因此准確地說,將這個block放到並行隊列dispatch_barrier_sync執行確保了,同一個url的圖片不會同一時刻進行多次下載.
// 這樣做還使得下載操作的創建同步進行,因為一個新的下載操作還沒有創建完成,self.barrierQueue會繼續等待它完成,然后才能執行下一個添加下載任務的block。所以說SD添加下載任務是同步的,而且都是在self.barrierQueue這個並行隊列中,同步添加任務。這樣也保證了根據executionOrder設置依賴關是正確的。換句話說如果創建下載任務不是使用dispatch_barrier_sync完成的,而是使用異步方法 ,雖然依次添加創建下載操作A、B、C的任務,但實際創建順序可能為A、C、B,這樣當executionOrder的值是SDWebImageDownloaderLIFOExecutionOrder,設置的操作依賴關系就變成了A依賴C,C依賴B
// 但是添加之后的下載依然是在下載隊列downloadQueue中異步執行,絲毫不會影響到下載效率。
// 以上就是說了SD下載的關鍵點:創建下載任務在barrierQueue隊列中,執行下載在downloadQueue隊列中。
*/
}
});
}
說完這些,我們再看一下SD如何給addProgressCallback: completedBlock: forURL: createCallback:
方法設置創建回調的,畢竟這個才是創建下載操作並放入隊列的一些細節:
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 創建請求對象,並根據options參數設置其屬性
// 為了避免潛在的重復緩存(NSURLCache + SDImageCache),如果沒有明確告知需要緩存,則禁用圖片請求的緩存操作, 這樣就只有SDImageCache進行了緩存
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (wself.headersFilter) {
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
// 創建SDWebImageDownloaderOperation操作對象,傳入進度回調、完成回調、取消回調
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// 從callbacksForURL中取出進度回調
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
dispatch_async(dispatch_get_main_queue(), ^{ // 切換到主隊列完成異步回調
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
});
}
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
// 從callbacksForURL中取出完成回調。
// 將刪除所有回調的block放到隊列barrierQueue中使用barrier_sync方式執行,確保了在進行調用完成回調之前所有的使用url對應的回調的地方都是正確的數據。
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
[sself.URLCallbacks removeObjectForKey:url];
}
});
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
cancelled:^{
// 將url對應的所有回調移除
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
// 設置是否需要解壓
operation.shouldDecompressImages = wself.shouldDecompressImages;
// 設置進行網絡訪問驗證的憑據
if (wself.urlCredential) {
operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
// 根據下載選項SDWebImageDownloaderHighPriority設置優先級
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 將操作添加到隊列中
[wself.downloadQueue addOperation:operation];
// 根據executionOrder設置操作的依賴關系
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
有關NSOperation的優先級還有一個小細節:
queuePriority
這個屬性包含着操作的相對優先級。這個值會影響到操作出隊列和執行的順序,這個值總是符合一個系統預定義的常量。如果沒有明確設置優先級,則使用默認值NSOperationQueuePriorityNormal。
當且僅當需要對沒有依賴關系的操作之間設置優先級時候使用它。優先級值不應該用來實現對不同的操作對象之間的依賴管理。如果你想在操作之間建立依賴關系,應該使用addDependency:方法。
如果你嘗試指定一個和定義好的常量不同的優先級值,操作對象自動調整你指定的值以適應NSOperationQueuePriorityNormal優先級,直到找到有效的常量值。例如,如果你指定了這個值為-10,操作會調整這個值來匹配NSOperationQueuePriorityVeryLow;相似的,如果你指定了這個值為+10,操作會調整這個值來匹配NSOperationQueuePriorityVeryHigh常量。
另外,我們可以觀察到如果沒有給operationClass傳遞值的情況下,SDWebImageDownloader的- (id <SDWebImageOperation>)downloadImageWithURL: options: progress: completed:
方法實際上返回的是SDWebImageDownloaderOperation類型實例,並且是已經經過各種下載選項設置之后的放入到下載隊列中的操作實例。
我們還需要關注一下方法- (void)setSuspended:(BOOL)suspended;
,這個方法的實現只有一句:
- (void)setSuspended:(BOOL)suspended {
[self.downloadQueue setSuspended:suspended]; // 實際上是對下載隊列調用了setSuspended方法
}
有關NSOperationQueue對象的setSuspended,不得不看一下文檔的一些解釋:
當這個屬性值是NO,隊列主動開啟在隊列中的操作,並准備執行。將這個屬性設置為YES,阻止隊列開啟任何隊列式的操作,但已經開始且正在執行的操作會繼續執行。你可以繼續向暫停的隊列添加操作,但是如果不改變這個屬性為NO,這些操作不會計划執行。
Operation當且僅當執行完成之后才從隊列中移除。但是,為了結束執行,操作必須得先開啟執行。因為一個暫停的隊里不能開啟任何一個新的操作,它不會移除任何一個在當前隊列中且不是正在執行的操作(包括已經取消的操作)。
你可以通過KVO監控這個屬性值的改變。配置一個觀察者來監控操作隊列的suspended key path。
這個屬性的默認值是NO。
可見setSuspended方法傳遞YES,並不能暫停隊列中的所有操作,而是讓隊列不再開啟新的任務。
以上就是關於SD下載圖片的全部內容。
### 緩存SDImageCache `SDImageCache`類是一個功能無比強大的緩存管理器。它可以實現內存和磁盤緩存功能的實現和管理,主要包括以下幾個方面: 1.對內存或磁盤緩存進行單個圖片增、刪、查等操作 2.還提供使用命名空間的方式對圖片分類管理,管理應用啟動前的放入app中的預緩存圖 3.同時還可以對所有的緩存整體操作,如查詢總緩存文件個數,查詢總緩存大小,一次性清理內存緩存,一次性清理磁盤緩存。 而且剛才所說的所有功能實現之后可以添加完成回調,以便在主線程更新UI或者給出提示信息。SDImageCache
的主要屬性有以下幾個:
1.BOOL shouldDecompressImages
是否進行解壓
2.BOOL shouldDisableiCloud
不啟用iCloud備份 默認是YES
3.BOOL shouldCacheImagesInMemory
使用內存緩存 默認是YES
4.NSUInteger maxMemoryCost
內存緩存NSCache能夠承受的最大總開銷,超過這個值NSCache會剔除對象。是內存緩存(NSCache類型)的屬性值。
5.NSUInteger maxMemoryCountLimit
內存緩存NSCache能承受的最多對象個數
6.NSInteger maxCacheAge
最大緩存時長 以秒為單位, 默認值為kDefaultCacheMaxCacheAge,一周時間
7.NSUInteger maxCacheSize
最大緩存大小 以字節為單位。默認沒有設置,也就是為0,而清理磁盤緩存的先決條件為self.maxCacheSize > 0,所以0表示無限制。
在看看它的主要的方法,這里將它們分為幾個組分別說明:
有關命名空間,SD會根據命名空間,對內存緩存創建不同的NSCache對象並對name屬性賦值,創建不同的磁盤緩存寫文件隊列等等。
/**
* 用指定的命名空間初始化一個新的緩存倉庫
*/
- (id)initWithNamespace:(NSString *)ns;
/**
* 用指定的命名空間和目錄初始化一個新的緩存倉庫
*/
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;
- (NSString *)makeDiskCachePath:(NSString*)fullNamespace; // 獲取指定ns的完整路徑,這里傳遞的是完整ns
/**
通過SDImageCache添加一個只讀的緩存文件夾路徑,用來搜索預緩存的圖片
如果你想在你的app中捆綁預加載圖片,就非常有用。
*/
- (void)addReadOnlyCachePath:(NSString *)path;
什么是完整的ns路徑,按照SD的規則(可以在initWithNamespace: diskCacheDirectory:
方法中查看),fullNameSpace是在ns前面添加了前綴com.hackemist.SDWebImageCache.
內存緩存的memCache.name直接設置為fullNamespace,若傳入的ns為@"xyz"磁盤緩存的的路徑則變為Library/Caches/xyz/com.hackemist.SDWebImageCache.xyz。
這里還有兩個用於查詢指定key圖片在磁盤緩存中的路徑的方法
// 獲取指定key的緩存路徑,需要傳入root文件夾
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;
// 獲取指定key的默認文件路徑,也就是root文件夾使用self.diskCachePath
- (NSString *)defaultCachePathForKey:(NSString *)key;
現在介紹一些對單個圖片的緩存操作的方法:
增:
// 將指定的image緩存起來,key一般傳入urlString,默認進行磁盤緩存,實際實現為下面方法toDisk傳入YES
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
// toDisk指定為是否進行磁盤緩存,實際實現為調用了下面的方法
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
/**
* 將image做內存緩存,磁盤緩存為可選
* @param recalculate BOOL 代表着是否imageData可以使用或者一個新的data會根據UIImage構建
* @param imageData imageData作為服務器返回的數據, 這個值會用來做磁盤存儲來代替將給定的image轉換成可存儲的/壓縮的圖片格式的方案,以便節約性能和CPU。(實際上是節約了計算能力,而多使用了一點磁盤的存儲能力)
內存緩存都是使用image
image和imageData都非空 若recalculate為YES會忽略imageData,而使用image進行磁盤緩存
兩者有一個為空的,使用非空的進行磁盤緩存
兩者都為空,則沒有磁盤緩存。
*/
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
查:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock; // 異步查詢磁盤緩存,其實內部實現先查詢了內存緩存,然后查詢磁盤緩存,返回的是一個空的操作(稍后會解釋為什么是空的操作)
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key; // 異步查詢內存緩存
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key; // 查詢完內存緩存之后再異步查詢磁盤緩存
刪:
// 異步地移除內存和磁盤緩存,實際是下面方法的withCompletion傳入nil
- (void)removeImageForKey:(NSString *)key;
// 異步地移除內存和磁盤緩存,帶完成回調
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
// 異步地移除內存緩存,可以選擇是否移除磁盤緩存,實際是下面方法的withCompletion傳入nil
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
// 異步地移除內存緩存,可以選擇是否移除磁盤緩存,完成之后執行回調
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
對於所有的緩存內容的整體操作,有如下一些方法:
刪:
// 清除所有的內存緩存
- (void)clearMemory;
// 清除所有的磁盤緩存,無阻塞的方法,立刻返回.
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
// 上面的方法回調傳入nil
- (void)clearDisk;
// 移除磁盤中所有的過期緩存。無阻塞的方法
// clean和clear的區別是:clear是全部移除,clean只清除過期的緩存
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
// 上面的方法回調傳入nil
- (void)cleanDisk;
查:
// 獲取磁盤緩存的大小
- (NSUInteger)getSize; // 是在當前線程的串行隊列中同步執行的,思路是遍歷目錄中的所有文件,累加大小
// 獲取磁盤緩存文件的個數
- (NSUInteger)getDiskCount;
// 異步計算磁盤緩存的大小,然后執行回調,回調參數為文件個數和總大小
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;
// 異步檢查圖片是否在磁盤緩存中存在(沒有加載圖片)
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
// 上面方法回調參數傳遞nil
- (BOOL)diskImageExistsWithKey:(NSString *)key;
SDImageCache
中的方法實現都比較簡單:
內存緩存
添加都是對memCache屬性添加元素,key為urlString
刪除都是對memCache屬性移除元素
查詢都是按key取元素。
磁盤緩存
添加都是將UIImage的二進制寫入文件,並以url的MD5為文件名(下面會具體分析)
刪除都是將緩存文件刪除
查詢都是讀取文件的二進制轉為UIImage
對磁盤緩存整體的操作則是遍歷文件夾進行對單個文件操作來實現,在執行清理操作的時候,會一一對比緩存文件的上次修改(存儲)的時間到當前時間是否超過了過期時長,進行刪除操作(下面會具體分析)。而對於readonly的預先緩存好的圖片所在的路徑會存儲在私有屬性customPaths中,查詢圖片的時候也會遍歷這個屬性中所有的文件夾。
另外內存緩存的memCache是自定義的NSCache子類AutoPurgeCache,會接收內存警告的通知,當收到通知,會調用removeAllObjects方法清除所有的內存緩存。
對於實現一張圖片緩存的具體實現:
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
return;
}
// 內存緩存 前提是設置了需要進行
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
// 磁盤緩存
if (toDisk) {
// 將緩存操作作為一個任務放入ioQueue中異步執行
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// 需要確定圖片是PNG還是JPEG。PNG圖片容易檢測,因為有一個唯一簽名。PNG圖像的前8個字節總是包含以下值:137 80 78 71 13 10 26 10
// 在imageData為nil的情況下假定圖像為PNG。我們將其當作PNG以避免丟失透明度。
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;
// 而當有圖片數據時,我們檢測其前綴,確定圖片的類型
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
if (data) {
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// 根據image的key獲取緩存路徑
NSString *cachePathForKey = [self defaultCachePathForKey:key];
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];
// 不適用iCloud備份
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
});
}
}
對於清理方法cleanDiskWithCompletionBlock:
,有兩個指標:文件的緩存有效期及最大緩存空間大小。文件的緩存有效期可以通過maxCacheAge屬性來設置,默認是1周的時間。如果文件的緩存時間超過這個時間值,則將其移除。而最大緩存空間大小是通過maxCacheSize屬性來設置的,如果所有緩存文件的總大小超過這一大小,則會按照文件最后修改時間的逆序,以每次一半的遞歸來移除那些過早的文件,直到緩存的實際大小小於我們設置的最大使用空間。清理的操作在-cleanDiskWithCompletionBlock:方法中,其實現如下:
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// 枚舉器預先獲取緩存文件的有用的屬性
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
// 枚舉緩存文件夾中所有文件,該迭代有兩個目的:移除比過期日期更老的文件;存儲文件屬性以備后面執行基於緩存大小的清理操作
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// 移除早於有效期的老文件
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// 存儲文件的引用並計算所有文件的總大小
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
// 如果磁盤緩存的大小超過我們配置的最大大小,則執行基於文件大小的清理,我們首先刪除最老的文件
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
// 以設置的最大緩存大小的一半值作為清理目標
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
// 按照最后修改時間來排序剩下的緩存文件
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// 刪除文件,直到緩存總大小降到我們期望的大小
for (NSURL *fileURL in sortedFiles) {
if ([_fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
我們看一下剛才遺留的一個問題,為什么使用- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock
方法查詢指定key的緩存時,返回的是一個空的NSOperation,我們先看一下這個方法的實現:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
// 對doneBlock、key判空 查找內存緩存
// ...
// 查找內存緩存
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) { // isCancelled初始默認值為NO
return;
}
@autoreleasepool {
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
通過代碼可以看到operation雖然沒有具體的內容,但是我們可以在外部調用operation的cancel方法來改變isCancelled的值。這樣做對從內存緩存中查找到圖片的本次操作查詢過程沒有影響,但是如果本次查詢過程是在磁盤緩存中進行的,就會受到影響,autoreleasepool{}代碼塊不再執行。而在這段代碼塊完成了這樣的工作:將磁盤緩存取出進行內存緩存,在線程執行完成回調。因此可以看到這個返回的NSOpeation值可以幫助我們在外部控制不再進行磁盤緩存查詢和內存緩存備份的操作,歸根結底就是向外部暴漏了取消操作的接口。
### SDWebImageManager:按需下載->完成緩存->緩存管理等一系列完整的流程線 在實際的運用中,我們並不直接使用SDWebImageDownloader類及SDImageCache類來執行圖片的下載及緩存。為了方便用戶的使用,SDWebImage提供了SDWebImageManager對象來管理圖片的下載與緩存。而且我們經常用到的諸如UIImageView+WebCache等控件的分類都是基於SDWebImageManager對象的。該對象將一個下載器和一個圖片緩存綁定在一起,並對外提供兩個只讀屬性來獲取它們,如下代碼所示: ```objectivec @interface SDWebImageManager : NSObject@property (weak, nonatomic) id
@property (strong, nonatomic, readonly) SDImageCache *imageCache;
@property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader;
@property (nonatomic, copy) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
// ...
@end
從上面的代碼中我們還可以看到有一個delegate屬性,其是一個`id<SDWebImageManagerDelegate>`對象。`SDWebImageManagerDelegate`聲明了兩個可選實現的方法,如下所示:
```objectivec
// 控制當圖片在緩存中沒有找到時,應該下載哪個圖片
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
// 允許在圖片已經被下載完成且被緩存到磁盤或內存前立即轉換
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;
這兩個代理方法會在SDWebImageManager
的-downloadImageWithURL:options:progress:completed:
方法中調用,而這個方法是SDWebImageManager類的核心所在。我們來看看它的具體實現:
為了能夠更好地理解這個方法的實現,再次必須強調一個SDWebImageOptions選項值SDWebImageRefreshCached
,如果設置了這個值:
即使SD對圖片緩存了,也期望HTTP響應cache control,並在需要的情況下從遠程刷新圖片。也就是說如果在磁盤中找到了這張圖片,但設置了這個選項,仍然需要進行網絡請求,查看服務器端的這張圖片有沒有被改變,並決定進行下載,然后使用新的圖片,同時完成新的緩存。
但是這個下載並不是自己決定要不要進行的,還需要如果代理通過方法[self.delegate imageManager:self shouldDownloadImageForURL:url]
返回NO,那就是代理要求這個url對應的圖片不需要下載。這種情況下就不再下載,而是使用在緩存中查找到的圖片
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// 判斷URL合法性
// ...
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; // 創建一個組合操作,主要用於將查詢緩存、下載操作、進行緩存等工作聯系在一起
__weak SDWebImageCombinedOperation *weakOperation = operation;
// 檢查這個url是否在失敗列表中,也就是是否曾經下載失敗過。
// ...
// 如果沒有設置失敗重試選項(SDWebImageRetryFailed),並且是一個失敗過的url,則直接執行完成回調。
// ...
@synchronized (self.runningOperations) { // (self.runningOperations是一個數組,元素為正在進行的組合操作)
[self.runningOperations addObject:operation];
}
NSString *key = [self cacheKeyForURL:url];
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
// 如果操作被取消了,從正在進行的操作列表中將它移出.
// ...
// 條件A:在緩存中沒有找到圖片 或者 options選項包含SDWebImageRefreshCached (這兩種情況都需要進行請求網絡圖片的)
// 且
// 條件B:代理允許下載
/*
條件B的實現為:代理不能響應imageManager:shouldDownloadImageForURL:方法 或者 能響應且方法返回值為YES。也就是說沒有實現這個方法就是允許的,而如果實現了的話,返回為YES才是允許的。
*/
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
// 分支一:緩存中找到了圖片 且 options選項包含SDWebImageRefreshCached, 先在主線程完成一次回調,使用的是緩存中找到的圖片
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
// 如果在緩存中找到了image但是設置了SDWebImageRefreshCached選項,傳遞緩存的image,同時嘗試重新下載它來讓NSURLCache有機會接收服務器端的更新
completedBlock(image, nil, cacheType, YES, url);
});
}
// 如果沒在緩存中找到image 或者 設置了需要請求服務器刷新的選項,則仍需要下載.
SDWebImageDownloaderOptions downloaderOptions = 0;
// ...
if (image && options & SDWebImageRefreshCached) {
// 如果image已經被緩存但是設置了需要請求服務器刷新的選項,強制關閉漸進式選項
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// 如果image已經被緩存但是設置了需要請求服務器刷新的選項,忽略從NSURLCache讀取的image
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
// 創建下載操作,先使用self.imageDownloader下載
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { // 使用self.imageDownloader進行下載
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
// 如果操作取消了,不做任何事情
//如果調用completedBlock, 這個block會和另一個completedBlock爭奪同一個對象。因此,如果這個block后被調用,會覆蓋新的數據。
}
else if (error) {
// 進行完成回調
// 將url添加到失敗列表中
// ...
}
else {
// 如果設置了失敗重試,將url從失敗列表中去掉
// ...
// 設置了SDWebImageRefreshCached選項 且 緩存中找到了image 且 沒有下載成功
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
// 這個分支的進入的條件:既沒有error、downloadedImage又是nil,這種回調在SDWebImageDownloaderOperation進行下載的時候只有讀取了URL的緩存才會發生,即下載正常完成,但是沒有數據。
// 圖片刷新遇到了NSSURLCache中有緩存的狀況,不調用完成回調。
// Image refresh hit the NSURLCache cache, do not call the completion block
}
// 下載成功 且 設置了需要變形Image的選項 且變形的代理方法已經實現
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
/*
全局隊列異步執行:
1.調用代理方法完成形變
2.進行緩存
3.主線程執行完成回調
*/
// ...
}
else {
/*
1.進行緩存
2.主線程執行完成回調
*/
// ...
}
}
if (finished) {
// 從正在進行的操作列表中移除這個組合操作
// ...
}
}];
// 設置組合操作的取消回調
// ...
}
// 處理其他情況
// 情況一:在緩存中找到圖片(代理不允許下載 或者 沒有設置SDWebImageRefreshCached選項 滿足至少一項)
else if (image) {
// 使用image執行完成回調
// 從正在進行的操作列表中移除組合操作
// ...
}
// 情況二:在緩存中沒找到圖片 且 代理不允許下載
else {
// 執行完成回調
// 從正在進行的操作列表中移除組合操作
// ...
}
}];
return operation;
}
這個方法主要完成了這些工作:
1.創建一個組合Operation,是一個SDWebImageCombinedOperation對象,這個對象負責對下載operation創建和管理,同時有緩存功能,是對下載和緩存兩個過程的組合。
2.先去尋找這張圖片 內存緩存和磁盤緩存,這兩個功能在self.imageCache的queryDiskCacheForKey: done:方法中完成,這個方法的返回值既是一個緩存operation,最終被賦給上面的Operation的cacheOperation屬性。
在查找緩存的完成回調中的代碼是重點:它會根據是否設置了SDWebImageRefreshCached
選項和代理是否支持下載決定是否要進行下載,並對下載過程中遇到NSURLCache的情況做處理,還有下載失敗的處理以及下載之后進行緩存,然后查看是否設置了形變選項並調用代理的形變方法進行對圖片形變處理。
3.將上面的下載方法返回的操作命名為subOperation,並在組合操作operation的cancelBlock代碼塊中添加對subOperation的cancel方法的調用。這樣就完成了下面的工作1和2:
// 1.使能通過組合操作的屬性cacheOperation控制緩存操作的取消
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
// ...
// 需要下載的話,進行下面的過程
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
}
operation.cancelBlock = ^{ // 2.使能通過組合操作的cancelBlock控制下載的取消
[subOperation cancel];
// ...
};
// 不需要下載的其他情況
// ...
}
4.處理請他的情況:代理不允許下載但是找到緩存的情況,沒有找到緩存且代理不允許下載的情況
5.這個方法最終返回的是operation也就是一個SDWebImageCombinedOperation對象,而不是下載操作。
注意以下區分:
本方法,也就是SDWebImageManager對象的- (id <SDWebImageOperation>)downloadImageWithURL:options:progress:completed:返回的是SDWebImageCombinedOperation對象
SDImageCache對象的- (NSOperation *)queryDiskCacheForKey: done:返回的是一個空的NSOperation對象(用於取消磁盤緩存查詢和內存緩存備份)
SDWebImageDownloader對象的- (id <SDWebImageOperation>)downloadImageWithURL:options: progress:completed:返回的是一個已經放到隊列中執行的下載操作,默認是SDWebImageDownloaderOperation對象
介於幾個方法的語義不明 我強烈建議SD做一下修改:
將SDWebImageOperation協議改名為SDCancellableOperation
將SDWebImageManager對象的- (id <SDWebImageOperation>)downloadImageWithURL:options:progress:completed:方法改名為- (id <SDWebImageCombinedOperation>)downloadAndCacheImageWithURL:options:progress:completed:
介於沒有對外暴漏SDWebImageCombinedOperation類,改名為- (id <SDCancellableOperation>)downloadAndCacheImageWithURL:options:progress:completed:即可
將SDImageCache對象的- (NSOperation *)queryDiskCacheForKey: done:改名為- (id <SDCancellableOperation>)queryDiskCacheForKey: done:,當然這個不是必須的
將SDWebImageDownloader對象的- (id <SDWebImageOperation>)downloadImageWithURL:options: progress:completed:改名為
- (id <SDCancellableOperation>)downloadImageWithURL:options: progress:completed:
說了那么半天還沒有介紹一項重要內容:上面這個下載方法中的操作選項參數是由枚舉SDWebImageOptions來定義的,這個操作中的一些選項是與SDWebImageDownloaderOptions中的選項對應的。我們來看看這個SDWebImageOptions選項都有哪些:
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
// 默認情況下,當URL下載失敗時,URL會被列入黑名單,導致庫不會再去重試,該標記用於禁用黑名單
SDWebImageRetryFailed = 1 << 0,
// 默認情況下,圖片下載開始於UI交互,該標記禁用這一特性,這樣下載延遲到UIScrollView減速時
SDWebImageLowPriority = 1 << 1,
// 該標記禁用磁盤緩存
SDWebImageCacheMemoryOnly = 1 << 2,
// 該標記啟用漸進式下載,圖片在下載過程中是漸漸顯示的,如同瀏覽器一下。
// 默認情況下,圖像在下載完成后一次性顯示
SDWebImageProgressiveDownload = 1 << 3,
// 即使圖片緩存了,也期望HTTP響應cache control,並在需要的情況下從遠程刷新圖片。
// 磁盤緩存將被NSURLCache處理而不是SDWebImage,因為SDWebImage會導致輕微的性能下載。
// 該標記幫助處理在相同請求URL后面改變的圖片。如果緩存圖片被刷新,則完成block會使用緩存圖片調用一次
// 然后再用最終圖片調用一次
SDWebImageRefreshCached = 1 << 4,
// 在iOS 4+系統中,當程序進入后台后繼續下載圖片。這將要求系統給予額外的時間讓請求完成
// 如果后台任務超時,則操作被取消
SDWebImageContinueInBackground = 1 << 5,
// 通過設置NSMutableURLRequest.HTTPShouldHandleCookies = YES;來處理存儲在NSHTTPCookieStore中的cookie
SDWebImageHandleCookies = 1 << 6,
// 允許不受信任的SSL認證
SDWebImageAllowInvalidSSLCertificates = 1 << 7,
// 默認情況下,圖片下載按入隊的順序來執行。該標記將其移到隊列的前面,
// 以便圖片能立即下載而不是等到當前隊列被加載
SDWebImageHighPriority = 1 << 8,
// 默認情況下,占位圖片在加載圖片的同時被加載。該標記延遲占位圖片的加載直到圖片已以被加載完成
SDWebImageDelayPlaceholder = 1 << 9,
// 通常我們不調用動畫圖片的transformDownloadedImage代理方法,因為大多數轉換代碼可以管理它。
// 使用這個票房則不任何情況下都進行轉換。
SDWebImageTransformAnimatedImage = 1 << 10,
};
可以看到兩個SDWebImageOptions與SDWebImageDownloaderOptions中的選項有一定的對應關系,實際上我們在使用SD時,使用SDWebImageManager的-downloadImageWithURL:options:progress:completed:
方法較多,而幾乎很少單獨使用下載和緩存的功能,這個方法的組合功能中會使用設置的SDWebImageOptions值改變相應的SDWebImageDownloaderOptions值,同時也會對緩存方案有一定的一項。
SDWebImageManager
中還有一個重要的屬性:
決定緩存的key的使用方案的屬性@property (nonatomic, copy) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
這是一個block類型的值,會按照它定義的內容對url進行過濾,得到url對應的緩存key。還有一個根據url得到緩存key的方法,其內部就是調用了這個block。
- (NSString *)cacheKeyForURL:(NSURL *)url; // 如果外部沒有傳入self.cacheFilter 那么返回的是[url absoluteString]
SDWebImageManager
中還有一些控制和查看執行狀態的方法:
// 取消runningOperations中所有的操作,並全部刪除
- (void)cancelAll;
// 檢查是否有操作在運行,這里的操作指的是下載和緩存組成的組合操作,其實就是檢查self.runningOperations中的組合操作個數是否大於0
- (BOOL)isRunning;
另外要說明SDWebImageManager
中還定義了與緩存操作相關的方法,其實都是調用了self.imageCache(SDImageCache類型)的相關緩存方法實現的,如:
// 使用self.imageCache的store..方法進行內存和磁盤緩存
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
// 指定url的圖片是否進行了緩存,優先查看內存緩存,再查看磁盤緩存,只要有就返回YES,兩者都沒有則返回NO
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
// 指定url的圖片是否進行了磁盤緩存
- (BOOL)diskImageExistsForURL:(NSURL *)url;
- (void)cachedImageExistsForURL:(NSURL *)url
completion:(SDWebImageCheckCacheCompletionBlock)completionBlock; // 獲取指定url的緩存傳遞給回調,如果是內存緩存,在主隊列異步執行回調;如果是磁盤緩存,在當前線程執行回調
- (void)diskImageExistsForURL:(NSURL *)url
completion:(SDWebImageCheckCacheCompletionBlock)completionBlock; // 獲取指定url的磁盤緩存,只是磁盤緩存,和上面的實現相同,在當前線程執行回調
這些方法的具體實現可以查看本文第三部分:緩存SDImageCache