SDWebImage是現(xiàn)在最好用也是用的最廣泛的網(wǎng)絡(luò)圖片下載第三方庫(kù),內(nèi)部封裝很好,非常值得學(xué)習(xí),現(xiàn)整理如下。文中有部分內(nèi)容來自于作者 Haley_Wong的SDWebImageV3.7.5源碼解析踩娘。
代碼詳解
SDWebImage通過添加category的方式,為UIImageView、UIButton擴(kuò)展設(shè)置網(wǎng)絡(luò)圖片的方法养渴。以UIImageView舉例雷绢,其包含如下設(shè)置圖片的方法:
- (void)sd_setImageWithURL:(NSURL *)url;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;
- (void)sd_setImageWithURL:(NSURL *)url completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setAnimationImagesWithURLs:(NSArray *)arrayOfURLs;
如上這些方法,最終都是調(diào)用各category中的方法:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
每個(gè)category中的該方法具體邏輯不盡相同理卑,這里以UIImageView為例翘紊,其內(nèi)部的邏輯大體為:
1.取消該UIImageView的當(dāng)前圖片加載操作。(重點(diǎn)一)
2.利用runtime的關(guān)聯(lián)對(duì)象AssociatedObject為該UIImageView設(shè)置網(wǎng)絡(luò)圖片的url藐唠,可以通過方法- (NSURL *)sd_imageURL;
來獲取該對(duì)象的URL帆疟。(runtime的使用場(chǎng)景)
3.設(shè)置默認(rèn)圖片。(即placeholder宇立,若設(shè)置了延遲設(shè)置placeholder踪宠,則跳過該步)
4.判斷url是否存在,不存在則回調(diào)completedBlock妈嘹,返回錯(cuò)誤信息柳琢;若存在,執(zhí)行下一步润脸。
5.判斷是否添加ActivityIndicatorView柬脸。
6.調(diào)用SDWebImageManager,創(chuàng)建下載圖片的operation毙驯。(重點(diǎn)二倒堕,SDWebImage的核心內(nèi)容)
7.為該UIImageView設(shè)置下載的operation。(同樣是通過runtime的關(guān)聯(lián)對(duì)象AssociatedObject實(shí)現(xiàn))
8.執(zhí)行下載完成的completedBlock回調(diào)爆价。(這一步也值得詳細(xì)解析)
重點(diǎn)一
取消UIImageView的當(dāng)前圖片加載操作涩馆。為什么需要取消當(dāng)前加載操作呢?假如某一對(duì)象(UIImageView *
)設(shè)置了多個(gè)網(wǎng)絡(luò)圖片時(shí)允坚,因?yàn)橄螺d過程是異步的,假如不取消前一下載操作蛾号,那么最終得到的圖片很可能是錯(cuò)誤的稠项。且浪費(fèi)流量及資源。
- (void)sd_cancelCurrentImageLoad {
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}
- (void)sd_cancelCurrentAnimationImagesLoad {
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewAnimationImages"];
}
可以看到鲜结,這里有兩個(gè)不同的取消方法展运,因?yàn)閁IImageView除了可以設(shè)置單張圖片,還可以設(shè)置多張網(wǎng)絡(luò)圖片展示動(dòng)畫效果精刷。這兩個(gè)方法內(nèi)部調(diào)用的是同一個(gè)方法:
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
// Cancel in progress downloader from queue
NSMutableDictionary *operationDictionary = [self operationDictionary];
id operations = [operationDictionary objectForKey:key];
if (operations) {
if ([operations isKindOfClass:[NSArray class]]) {
for (id <SDWebImageOperation> operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id<SDWebImageOperation>) operations cancel];
}
[operationDictionary removeObjectForKey:key];
}
}
上面這個(gè)方法是UIView的一個(gè)category方法拗胜,在UIView + WebCacheOperation
中。其中[self operationDictionary]
利用runtime
的關(guān)聯(lián)對(duì)象(AssociatedObject
)獲取當(dāng)前視圖的operationDictionary怒允,沒有的話就創(chuàng)建一個(gè)set上去埂软,具體實(shí)現(xiàn)如下:
- (NSMutableDictionary *)operationDictionary {
//獲取關(guān)聯(lián)對(duì)象
NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
if (operations) {
return operations;
}
operations = [NSMutableDictionary dictionary];
//設(shè)置關(guān)聯(lián)對(duì)象
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
后面幾行內(nèi)容就是取消掉當(dāng)前operation的下載操作。因?yàn)榭赡苁荱IImageView的動(dòng)畫圖片纫事,所以就去數(shù)組中一個(gè)個(gè)的取消勘畔。如果是SDWebImage自定義的對(duì)象肯定會(huì)實(shí)現(xiàn)自定義的取消協(xié)議所灸,則轉(zhuǎn)換對(duì)象后取消。否則直接將這個(gè)object從字典中刪除炫七。
至此爬立,取消當(dāng)前圖片下載步驟完畢。
重點(diǎn)二
調(diào)用SDWebImageManager万哪,創(chuàng)建下載圖片的operation侠驯。
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
該方法的內(nèi)部實(shí)現(xiàn)是SDWebImage的核心,所有的精華都在這里奕巍。
實(shí)現(xiàn)中多次使用dispatch_main_sync_safe
和dispatch_main_async_safe
吟策。他們倆分別對(duì)應(yīng)兩個(gè)宏,一是為防止在主線程執(zhí)行主線程操作發(fā)生死鎖伍绳;二是避免不必要的開銷踊挠。dispatch_async不管怎么說都會(huì)有一定的開銷吧(此處存疑)。
#define dispatch_main_sync_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_sync(dispatch_get_main_queue(), block);\
}
#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
SDWebImageManager方法downloadImageWithURL
具體步驟
第一步
驗(yàn)證url冲杀,如果是字符串轉(zhuǎn)換為NSURL效床,如果不是NSURL類型,url置為nil权谁。
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
第二步
創(chuàng)建一個(gè)SDWebImageCombinedOperation對(duì)象剩檀,代表一個(gè)圖片加載任務(wù),但是實(shí)際下載圖片的事是由另一個(gè)Operation來做旺芽,該類也實(shí)現(xiàn)了SDWebImageOperation協(xié)議沪猴。因?yàn)榭赡軙?huì)在block中調(diào)用operation,所以先處理處理好循環(huán)引用問題采章。
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
第三步
BOOL isFailedUrl = NO;
//該步驟可能會(huì)出現(xiàn)多線程讀取問題运嗜,所以添加@synchronized同步鎖。
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
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;
}
第四步
將operation加進(jìn)數(shù)組中悯舟,需要添加同步鎖担租,保證數(shù)組的讀寫安全。
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
第五步
獲取該網(wǎng)絡(luò)圖片緩存用的key抵怎。
NSString *key = [self cacheKeyForURL:url];
展開這個(gè)方法是:
- (NSString *)cacheKeyForURL:(NSURL *)url {
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
}
else {
return [url absoluteString];
}
}
這里如果給manager設(shè)置過cacheKeyFilter奋救,則會(huì)按照自己的設(shè)置返回一個(gè)字符串作為key,否則會(huì)直接返回url 的絕對(duì)路徑absoluteString反惕。
第六步
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
這一步是為上面第二步創(chuàng)建的operation對(duì)象設(shè)置cacheOperation尝艘。
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
//key不存在,報(bào)錯(cuò)
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
//緩存中沒有姿染,創(chuàng)建任務(wù)異步讀取磁盤
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
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;
}
第七步
在cacheOperation的doneBlock中。如果圖片取到了緩存圖片,則直接將圖片等信息通過completedBlock返回隘梨。
從runningOperation中刪除步驟二中創(chuàng)建的該operation程癌。
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];
}
如果返回的圖片為nil,或者需要下載轴猎,則通過SDWebImageDownloader
的方法創(chuàng)建下載圖片的operation
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
下載過程待會(huì)詳細(xì)分析嵌莉,這里先分析下載完成后的操作。
情形一:回調(diào)返回的error捻脖,如果不為空锐峭,則返回錯(cuò)誤給completedBlock。如果url有問題可婶,則把url添加到failedURLs中沿癞。
情形二:如果成功,則
先從failedURLs中刪除url矛渴,里面不包含也沒關(guān)系椎扬。
如果url對(duì)應(yīng)的圖片是url不變,但是圖片會(huì)變的具温,則不緩存蚕涤。
如果圖片需要轉(zhuǎn)換,則將圖片轉(zhuǎn)換后保存到內(nèi)存和磁盤中铣猩,調(diào)用block返回圖片揖铜。如果不需要轉(zhuǎn)換,則直接保存和回調(diào)block达皿。
下載過程解析
在downloader中有一個(gè)URLCallbacks的可變字典天吓,每一個(gè)url作為key,對(duì)應(yīng)一個(gè)數(shù)組(數(shù)組中是字典對(duì)象峦椰,字典中保存下載operation的progressBlock和completeBlock)龄寞,然后判斷該url是否是首次下載,如果是汤功,則調(diào)用創(chuàng)建operation的block萄焦,否則直接返回沒有初始化的operation(nil)。
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
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();
}
});
}
createCallBack內(nèi)部創(chuàng)建operation過程:
先創(chuàng)建一個(gè)NSMutableURLRequest冤竹,需要保證該url不被緩存。過程如下:
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
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;
}
然后創(chuàng)建一個(gè)operation(SDWebImageDownloaderOperation
)對(duì)象茬射,將其放入downloadQueue中鹦蠕,并判斷是否設(shè)置過后進(jìn)先出的執(zhí)行順序,默認(rèn)是先進(jìn)先出的執(zhí)行在抛。
SDWebImageDownloaderOperation
SDWebImageDownloaderOperation為自定義的NSOperation钟病,其初始化方法為
- (id)initWithRequest:(NSURLRequest *)request
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock {
if ((self = [super init])) {
_request = request;
_shouldDecompressImages = YES;
_shouldUseCredentialStorage = YES;
_options = options;
_progressBlock = [progressBlock copy];
_completedBlock = [completedBlock copy];
_cancelBlock = [cancelBlock copy];
_executing = NO;
_finished = NO;
_expectedSize = 0;
responseFromCached = YES; // Initially wrong until `connection:willCacheResponse:` is called or not called
}
return self;
}
這里需要重寫start()
方法,在start()
方法中創(chuàng)建NSURLSession,并應(yīng)用起代理方法來更新progressBlock 和 completionHandler肠阱。還會(huì)在不同的結(jié)果時(shí)票唆,發(fā)送通知。
NSURLSession部分還沒理順屹徘,會(huì)在后期進(jìn)行更新