- UIButton+WebCache
- UIImageView+WebCache
- MKAnnotationView+WebCache
等Category 都是直接調(diào)用 SDWebImageManager 的方法
SDWebImageManager 內(nèi)部使用 Downloader 和 Cache 來協(xié)調(diào)下載和緩存的任務.
SDWebImageManager分析
毫無疑問 SDWebImageManager 有這2個屬性
**@property (strong, nonatomic, readonly) SDImageCache imageCache;
@property (strong, nonatomic, readonly) SDWebImageDownloader imageDownloader;
SDWebImageManager 這些顧名思義的方法也都是直接調(diào)用 SDImageCache的方法來實現(xiàn)的.
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
- (void)diskImageExistsForURL:(NSURL *)url
completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
... ...
具體實現(xiàn)
大部分的緩存和下載工作都在這一個方法中完成
代碼太長 ~ 無關代碼有省略
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
1.如果你將 url 參數(shù)傳成了字符串,我們幫你轉(zhuǎn)成 NSURL
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
2.防止你亂傳參數(shù),console 輸出奇怪的錯誤信息
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
3.如果url 不正確,或者此 url 已經(jīng)被下載過而且失敗了, 也不需要重試,那么就取消下載,直接回調(diào)下載完成 block, 返回錯誤信息,
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
dispatch_main_sync_safe(^{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
});
return operation;
}
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
NSString *key = [self cacheKeyForURL:url];
4.先查詢緩存中是否存在
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
5.關于 cancel 下載操作,沒有辦法強制中斷正在執(zhí)行的 Operation, 當然你可以主動殺死 App..
一般取消的操作都是設置一個標記 flag, 在執(zhí)行重要,耗時的操作之前檢查這個標記,如果已經(jīng)被取消了,就不繼續(xù)執(zhí)行.
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
6.詢問代理是否應該下載這個 url 對應的圖片
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
7.如果磁盤緩存中存在,直接回調(diào)下載完成 block 但是不中斷下載任務,嘗試,重新下載圖片,為了讓 NSURLCache 刷新緩存狀態(tài)
因為一個 url 對應的圖片可能會變化,比如 url 對應一個用戶的頭像,而這個頭像用戶隨時可能更改
completedBlock(image, nil, cacheType, YES, url);
});
}
7.終于開始準備下載圖片了, 將SDWebImageOptions 和 SDWebImageDownloaderOptions 做一些協(xié)調(diào)轉(zhuǎn)換的工作.
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 (image && options & SDWebImageRefreshCached) {
8.如果磁盤緩存中有圖片,就關閉 progressive 下載方式(圖片會從上到下,下載一部分,顯示一部分)
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
9.如果磁盤緩存中有圖片,讓 NSURLCache 刷新緩存狀態(tài)
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
10.準備完成..正式調(diào)用下載工作
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
//什么都沒有, Github issues 的 bugfix
}
else if (error) {
11.出錯了或者被取消了就回調(diào)下載完成的 block
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
}
});
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost) {
12.如果因為如上原因下載失敗,就加入self.failedURLs 黑名單,如果沒設置下載失敗重試,下次就不下載了
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
else {
if ((options & SDWebImageRetryFailed)) {
13.如果設置了下載失敗重試,就不加入黑名單,每次都重新下載圖片,不管上次是否下載失敗
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
14.如果這次下載是為了讓 NSURLCache 刷新緩存狀態(tài) 就不調(diào)用回調(diào)block
}
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
15.詢問代理是否要在image 存儲到緩存之前做一些最后的操作,(縮放,裁剪,圓角等)
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];
16.存入內(nèi)存和磁盤緩存
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
}
17.回調(diào)主線程,圖片終于下載完了,而且不是從緩存中取出來的...
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
});
}
else {
18.如果沒有實現(xiàn)代理,直接把圖片存入緩存
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
19,然后回調(diào)主線程,和17一樣...
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
}
}
19.將下載完成的 Operation 從runningOperations數(shù)組中移除
SDWebImageManager的 isRunning 方法的實現(xiàn)是判斷 self.runningOperations
if (finished) {
@synchronized (self.runningOperations) {
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
}
}
}
}];
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
}
}
};
}
else if (image) {
20.這怎么還有個完成的回調(diào)..其實些 block 回調(diào)嵌套的有點惡心..
這個回調(diào)是在磁盤或者內(nèi)存緩存中查詢到圖片時的回調(diào),此時 image 為緩存中的數(shù)據(jù), cacheType為 SDImageCacheTypeDisk或 SDImageCacheTypeMemory
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
else {
21.這里的 else 是,如果緩存中沒有,并且代理不允許下載這個 url 對應的圖片,會執(zhí)行下面的回調(diào)
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !weakOperation.isCancelled) {
completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
}
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
}];
return operation;
}
這個雖然長但是并不難理解的下載緩存過程終于分析完了... SDWebImage 的核心我們也就理解了
SDWebImageOptions
這個 Options 類似第二篇中的 SDWebImageDownloaderOptions
在 UIImageView 等的 Category,或者直接調(diào)用SDWebImageManager 下載都能自定義SDWebImageOptions設置來完成更多自定義的操作
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
//1.每次都重新嘗試下載圖片
//下載失敗后不將 圖片的 url 加入黑名單,每次都重新下載圖片
//如果加入了黑名單,下次再請求這個 url 時直接返回下載失敗,不嘗試下載,節(jié)省資源
SDWebImageRetryFailed = 1 << 0,
//2.下載圖片優(yōu)先級低
//默認情況下,有 UI 事件發(fā)生時,比如點擊按鈕, tableview 滾動,下載任務也會同時在其他線程異步執(zhí)行,并不會阻塞主線程,但下載會消耗 cpu, 可能會造成卡頓.
//設置這個LowPriority 后,只有 tableview 不滾動時才會下載.
SDWebImageLowPriority = 1 << 1,
//3.只啟用內(nèi)存緩存,可以用它實現(xiàn)隱私瀏覽?
SDWebImageCacheMemoryOnly = 1 << 2,
//4.圖片會從上到下,下載一些顯示一些,網(wǎng)速慢的時候,優(yōu)化體驗,默認不開啟
SDWebImageProgressiveDownload = 1 << 3,
//5.即使存在圖片緩存,也嘗試下載操作, 因為同一個 url 對應的圖片可能會變化
//例如用戶的頭像,用戶可以隨時上傳更新頭像,那我們就必須嘗試下載更新這個圖片,如果更新操作成功,會調(diào)用 下載完成的 completion Block
SDWebImageRefreshCached = 1 << 4,
//5.如果App進入后臺,啟用這個參數(shù)會在向系統(tǒng)要求額外的時間來將下載圖片隊列中的下載請求執(zhí)行完畢
//如果額外的下載時間過長可能會被系統(tǒng)主動取消下載操作
SDWebImageContinueInBackground = 1 << 5,
//6.設置 NSMutableURLRequest.HTTPShouldHandleCookies = YES; 處理Cookie的存儲
SDWebImageHandleCookies = 1 << 6,
//7.允許不安全的SSL傳輸,如果后臺配置了https,測試階段可以加這個參數(shù),Release時取消這參數(shù)
SDWebImageAllowInvalidSSLCertificates = 1 << 7,
//8.直接將這個圖片下載任務放到下載隊列的頭,讓這個下載任務先被執(zhí)行
SDWebImageHighPriority = 1 << 8,
//9.延遲設置 PlaceHolder 圖片,當圖片下載完時才會設置 PlaceHolder, 那么默認情況下,ImageView 不會顯示任何內(nèi)容,只會顯示其背景色.
SDWebImageDelayPlaceholder = 1 << 9,
//10.默認情況下,如果圖片是 Gif ,不會調(diào)用代理方法 transformDownloadedImage 執(zhí)行對圖片的自定義操作,(關于代理方法下面一點點就會提到),設置這個 flag 對 Gif 也調(diào)用代理方法
SDWebImageTransformAnimatedImage = 1 << 10,
//11.默認情況下,圖片下載完成后就通過 imageView.image=image 被設置給 imageView,我們可以阻止這一行為,然后在下載完成回調(diào)方法中先處理圖片,加圓角,加濾鏡等,之后再手動設置給
imageView
SDWebImageAvoidAutoSetImage = 1 << 11
};
我們可以發(fā)現(xiàn)這有很多 Option和 SDWebImageDownloaderOptions 類似,因為在上面的具體實現(xiàn)中,就是將SDWebImageOptions和SDWebImageDownloaderOptions 做了一個轉(zhuǎn)換或者說傳遞的工作
比如SDWebImageProgressiveDownload, SDWebImageContinueInBackground 等都是傳遞給它的下載模塊來執(zhí)行的.
P.S. 因為它是 NS_OPTIONS 所以我們可以同時設置多個 Option
類似 : SDWebImageRetryFailed | SDWebImageContinueInBackground
還有個 SDWebImageManagerDelegate
@protocol SDWebImageManagerDelegate <NSObject>
@optional
可以控制當緩存中沒有這個 url 對應的圖片時,是否應該下載它,默認Yes 會下載
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
允許下載,完成圖片之后,放入緩存之前,做最后的操作,裁剪,圓角等
注意,這個方法是在 get_global_queue 中執(zhí)行的,不能調(diào)用設置 UI 的方法.
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;
@end
UIKit 的 Category
UIButton+WebCache
UIImageView+WebCache
MKAnnotationView+WebCache
這些的實現(xiàn)都很簡單,都是調(diào)用 SDWebImageManager 來實現(xiàn)的..
SDWebImagePrefetcher
這個類能以低優(yōu)先級預下載一些圖片,以供后續(xù)的使用,提升用戶體驗.
以SDWebImageLowPriority 預下載,會在系統(tǒng)閑置時執(zhí)行,不會影響主線程和 cpu的效率
主要的方法就一個
- (void)prefetchURLs:(NSArray *)urls progress:(SDWebImagePrefetcherProgressBlock)progressBlock completed:(SDWebImagePrefetcherCompletionBlock)completionBlock;
也可以設置
NSUInteger maxConcurrentDownloads //最大同時下載的圖片數(shù)量
SDWebImageOptions options
內(nèi)部實現(xiàn)也是調(diào)用 SDWebImageManager的方法,不在贅述
補充
UIImage+GIF
這個分類可以讓UIImage 支持 Gif 圖片
Gif 的本質(zhì)是一張張的圖片,每張展示一小段時間,連續(xù)的切換這些圖片,看起來就是一張動圖了..
+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
if (!data) {
return nil;
}
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
1.獲取 Gif 包含的真正圖片數(shù)量
size_t count = CGImageSourceGetCount(source);
UIImage *animatedImage;
if (count <= 1) {
animatedImage = [[UIImage alloc] initWithData:data];
}
else {
NSMutableArray *images = [NSMutableArray array];
NSTimeInterval duration = 0.0f;
for (size_t i = 0; i < count; i++) {
CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
if (!image) {
continue;
}
2.獲取每一張圖片,累加他們的播放時間,計算總的 Gif 播放時間
duration += [self sd_frameDurationAtIndex:i source:source];
3.將每一張圖片存入數(shù)組中
[images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];
CGImageRelease(image);
}
if (!duration) {
duration = (1.0f / 10.0f) * count;
}
4.根據(jù)總時長創(chuàng)建 UIImage 的 frame 動畫
animatedImage = [UIImage animatedImageWithImages:images duration:duration];
}
CFRelease(source);
return animatedImage;
}
關于 Category 中的這段代碼
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
不在本文章的范圍內(nèi),如果你感興趣,可以搜索關鍵字 Assiciate Object
或者看這篇不錯的文章如何在 Category 中為類動態(tài)添加屬性