最近項目進度緩慢了下來,決定看看各種源碼來漲點知識。就先從SDWebImage開始吧!
在項目中用的最多的方法應該是UIImageView+WebCache與UIButton+WebCache里面的sd_setImageWithURL:這個系列的方法了。這里從UIImageView+WebCache開始看起。
其中該系列所有方法都基于:
- (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL*)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
參數很簡單明了,url是需要加載的圖片url;placeholder是在url指向的圖片加載完成之前暫時使用的圖片;options是個枚舉,用來控制加載圖片時的一些設置(具體百度即可,大把);progressBlock則可以通過返回的receivedSize與expectedSize來花式展現下載進度;completedBlock則是下載完成后的Block;
下面來看作者的具體實現:
[self sd_cancelCurrentImageLoad];
按照意思來看是取消當前所有的圖片加載,進入方法內部看:
- (void)sd_cancelImageLoadOperationWithKey:(NSString*)key {
// Cancel in progress downloader from queue
NSMutableDictionary*operationDictionary = [selfoperationDictionary];
idoperations = [operationDictionaryobjectForKey:key];
if(operations) {
if([operationsisKindOfClass:[NSArrayclass]]) {
for(id operationinoperations) {
if(operation) {
[operationcancel];
}
}
}elseif([operationsconformsToProtocol:@protocol(SDWebImageOperation)]){
[(id) operationscancel];
}
[operationDictionaryremoveObjectForKey:key];
}
}
首先取出一個operationDictionary,在取出這個字典的過程中,作者在分類中使用了objc_setAssociatedObject與objc_getAssociatedObject,來給分類添加屬性。
接著通過傳過來的key(@"UIImageViewImageLoad")來獲取ImageView的加載隊列。
最后,花式cancle掉這個隊列中的任務。
[operation cancel];
[operation DictionaryremoveObjectForKey:key];
回到主方法,第二行又通過objc_setAssociatedObject方法將url關聯到分類中,接著通過位與運算判斷下option是否為SDWebImageDelayPlaceholder,來設置默認的占位圖片。其中dispatch_main_async_safe這個宏很好用,避免了在主線程中造成死鎖的情況。
然后是判斷一下是否需要轉動菊花:
if([selfshowActivityIndicatorView]) {
[selfaddActivityIndicator];
}
接下來是這個方法的核心部分,調用了SDWebImageManager中的
- (id)downloadImageWithURL:(NSURL*)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
由此方法完成圖片的下載。
我們可以跳到SDWebImageManager.h中查看一下作者對于該方法的描述:
翻譯過來的意思大概是:如果URL指定的圖片不在緩存中就下載該圖片,否則就返回緩存的版本。
回到這個方法的實現,前幾行是判斷URL的類型是否正確。
BOOLisFailedUrl =NO;
@synchronized(self.failedURLs) {
isFailedUrl = [self.failedURLscontainsObject:url];
}
這幾句來獲取傳入的URL是否為之前下載失敗過的URL,用一個BOOL值來記錄下來。
如果URL不為空或者未設置options為SDWebImageRetryFailed項、且URL在黑名單之中,就會直接返回掉。
@synchronized(self.runningOperations) {
[self.runningOperationsaddObject:operation];
}
這段代碼是先給運行中的下載隊列加鎖,避免多個線程同時對數組進行操作,將一個SDWebImageCombinedOperation對象加入到下載隊列中。
NSString*key = [selfcacheKeyForURL:url];
operation.cacheOperation= [self.imageCache queryDiskCacheForKey:key done:^(UIImage*image,SDImageCacheTypecacheType) {
if(operation.isCancelled) {
@synchronized(self.runningOperations) {
[self.runningOperationsremoveObject:operation];
}
return;
}
將圖片的URL當做key值,再調用 queryDiskCacheForKey:done:來獲取緩存中的圖片。
我們來看看這個方法的內部實現:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
//這里封裝了NSChace的objectForKey方法,直接從內存緩存中獲取圖片對象
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
//如果內存緩存中獲取到則直接返回
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);
//在主線程中回調Block
});
}
});
return operation;
}
緩存這里獲取完成之后,來看下面的代碼,有點長,我們分解開來一部分一部分閱讀,先看這個判斷:
if ((!image || options & SDWebImageRefreshCached)
//圖片未從緩存中獲取,或者是 option設置需要刷新緩存
&& (![self.delegate respondsToSelector:
@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]))
/*這部分條件中,如果imageManager:shouldDownloadImageForURL:方法未實現、或是實現了并且返回YES。可以從方法的名字中來理解,代理方法返回的BOOL為是否應該下載URL對應的圖片。
*/
總而言之就是判斷各種條件之下,圖片是否應該被下載,讓我們進入方法內部。
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
// If image was found in the cache bug SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
//翻譯一下,如果圖片在緩存中被找到但是options設置了SDWebImageRefreshCached(刷新緩存),通知這個緩存圖片,并且試圖從新下載這個圖片,讓服務端有機會刷新這個緩存。
completedBlock(image, nil, cacheType, YES, url);
});
}
SDWebImageDownloaderOptions downloaderOptions = 0;
//初始化downloaderOptions
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
//如果options為低優先級,則設置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) {
// force progressive off if image already cached but forced refreshing
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
//翻譯一哈,兩行代碼的意思是,如果圖片存在緩存并且需要刷新緩存,則強制取消掉SDWebImageDownloaderProgressiveDownload模式(漸進下載),
然后忽略從緩存中讀取的圖片。
}
接下來使用SDWebImageDownloader來執行一個下載任務
id <SDWebImageOperation> subOperation =
[self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:
^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished)
來看下載completedBlock中的第一部分處理
if (weakOperation.isCancelled) {
/*這里什么都沒做操作,源代碼中提及了#699號更新,于是我去看了下,大概意思是說:
當weakOperation取消的時候不要試圖去調用completion block,dispatch_main_sync_safe()也無法保證這個block被終止的時候沒有其他的代碼在運行,所以其他代碼運行時可能會被截斷。
比如說,如果取消weakOperation后再調用completion block,那么在隨后的一個TableViewCell中加載Image的completion block將會和這個completion block產生競爭關系。說的通俗一點就是,先調用的completion block里面的數據可能會被第二個completion block的數據覆蓋掉。
*/
}
else if (error) {
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
}
//有錯誤信息,完成回調。
});
if (error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
//將發送錯誤的URL添加到黑名單里面
}
}
}
else {
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
//options設置為SDWebImageRefreshCached選項,在緩存中又找到了image且沒有下載成功
// Image refresh hit the NSURLCache cache, do not call the completion block
//圖片刷新時遇到了具有緩存的情況,不調用 completion block
}
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage))
&& [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
//圖片下載成功且圖片設置為SDWebImageTransformAnimatedImage并且實現了imageManager:transformDownloadedImage:withURL:方法
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
//調用delegate方法完成圖片的變形
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:data forKey:key toDisk:cacheOnDisk];
//將變形后的圖片緩存起來
}
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
//在主線程中回調completedBlock
}
});
});
}
else {
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
//如果沒設置圖片變形,并且下載完成,則直接緩存圖片
}
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
//在主線程中完成回調
}
});
}
}
if (finished) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
//從下載隊列移除
}
}
}];
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:weakOperation];
}
};
//設置operation取消之后的一些操作
else if (image) {
else if (image) {
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
//在緩存中找到圖片并且設置了不能下載的選項,完成回調
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
else {
//在緩存中沒有找到圖片,并且設置不能下載的選項
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
}
//完成回調
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
}];
return operation;
這么一大段的方法按照功能排序來看,分解為首先創建下載operation,再讀取系統的內存緩存與磁盤緩存,接著判斷是否需要下載來進行下載操作,最后對下載的圖片進行處理。