Asynchronous image downloader with cache support as a UIImageView category
支持圖片異步下載和緩存的UIImageView分類
UIView+WebCache 最基本的方法是UIImageView+WebCache中這個方法 - (void)sd_setImageWithURL:(nullable NSURL *)url;
復制代碼 一步步走下來,會發現實際運用的是UIView+WebCache中的方法,包括UIButton+WebCache內部核心方法也是調用的下面的方法,其中SDWebImageOptions策略詳細介紹可以看這里 - (void)sd_internalSetImageWithURL:(nullable NSURL *)urlplaceholderImage:(nullable UIImage *)placeholderoptions:(SDWebImageOptions)optionsoperationKey:(nullable NSString *)operationKey
set ImageBlock:(nullable SDSetImageBlock)
set ImageBlockprogress:(nullable SDWebImageDownloaderProgressBlock)progressBlockcompleted:(nullable SDExternalCompletionBlock)completedBlock;
復制代碼 進入方法內部:先取消相關的所有下載 // operationKey 用來描述當前操作的關鍵字標識,默認值是類名字,即 @
"UIImageView" NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);// 取消當前view下 跟validOperationKey有關的所有下載操作,以保證不會跟下面的操作有沖突[self sd_cancelImageLoadOperationWithKey:validOperationKey];// 通過runtime的關聯對象給UIView添加屬性,設置圖片地址objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
復制代碼 其中取消操作方法內部涉及到一個協議<SDWebImageOperation>,這個協議只有一個cancel方法,可見,這個協議就是用來取消操作的,只要遵守該協議的類,必定會有cancel方法。
@protocol SDWebImageOperation <NSObject>- (void)cancel;@end
復制代碼 取消方法的具體實現:
涉及到一個字典`SDOperationsDictionary`類型為`NSMutableDictionary<NSString *, id>`,也是通過關聯對象添加為UIView的屬性,用來存儲UIView的所有下載操作,方便之后的取消/移除
復制代碼 - (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {// 從隊列中取消跟key有關的所有下載操作// 任何實現協議的對象都執行取消操作SDOperationsDictionary *operationDictionary = [self operationDictionary];id operations = operationDictionary[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];}// 最后從字典中移除key[operationDictionary removeObjectForKey:key];}}
復制代碼 如果沒有設置延遲加載占位圖SDWebImageDelayPlaceholder,就會先進行加載占位圖, if (!(options & SDWebImageDelayPlaceholder)) {dispatch_main_async_safe(^{// 返回主線程中進行UI設置,把占位圖當成image進行圖片設置,在方法內部會進行UIButton和UIImageView的判斷區分[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:
set ImageBlock];});}
復制代碼 其中有一個宏定義,通過字符串的比較來獲取主線程
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\block();\}
else {\dispatch_async(dispatch_get_main_queue(), block);\}
復制代碼 判斷url,url為空的情況下,直接返回錯誤信息 if (url) {// check
if activityView is enabled or not// 檢查菊花
if ([self sd_showActivityIndicatorView]) {[self sd_addActivityIndicator];}// url 存在的情況下進行的操作...}
else {// url 為nil的情況下,生成錯誤信息,并返回 dispatch_main_async_safe(^{// 移除菊花[self sd_removeActivityIndicator];
if (completedBlock) {NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @
"Trying to load a nil url" }];completedBlock(nil, error, SDImageCacheTypeNone, url);}});}
復制代碼 url不為nil的情況下,獲取圖片信息,并生成operation,然后存儲。 // 返回的是一個遵從了SDWebImageOperation協議的NSObject的子類,目的是方便之后的取消/移除操作
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { /*完成之后的操作*/}];// 根據validOperationKey把生成的operation放入字典`SDOperationsDictionary`中,這個字典也是通過關聯對象,作為UIView的一個屬性。[self sd_setImageLoadOperation:operation
for Key:validOperationKey];
復制代碼 SDInternalCompletionBlock是在UIView內部使用的completedBlock,在block中,返回獲取到的圖片,以及相關信息。最后在主線程中,進行UI更新并更新布局。 // weak 避免 保留環__weak __typeof(self)wself = self;id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {// block 中強引用替換,避免使用過程中被系統自動釋放__strong __typeof (wself) sself = wself;// 加載完成移除菊花[sself sd_removeActivityIndicator];
if (!sself) {
return ;}dispatch_main_async_safe(^{
if (!sself) {
return ;}// SDWebImageAvoidAutoSetImage, 對圖片進行手動設置,開發者在外面的complete里面可以對圖片設置特殊效果,然后賦值ImageView.image
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {completedBlock(image, error, cacheType, url);
return ;}
else if (image) {// 更新圖片,內部會進行imageView或者button的判斷[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:
set ImageBlock];// 更新布局Layout[sself sd_setNeedsLayout];}
else {// SDWebImageDelayPlaceholder 延遲加載占位圖,下載完成后才會進行設置
if ((options & SDWebImageDelayPlaceholder)) {[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:
set ImageBlock];[sself sd_setNeedsLayout];}}// 如果有返回block,返回block和其它信息
if (completedBlock && finished) {completedBlock(image, error, cacheType, url);}});}];
復制代碼 總結:
SDWebImageManager The SDWebImageManager is the class behind the UIImageView+WebCache category and likes. It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache). You can use this class directly to benefit from web image downloading with caching in another context than a UIView.
SDWebImageManager起一個承上啟下的作用,緊密連接圖片下載SDWebImageDownloader和圖片緩存SDImageCache,可以直接通過這個類獲取緩存中的圖片。
核心方法(也是UIView+WebCache的第6步):
- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)urloptions:(SDWebImageOptions)optionsprogress:(nullable SDWebImageDownloaderProgressBlock)progressBlockcompleted:(nullable SDInternalCompletionBlock)completedBlock;
復制代碼 內部實現:
如果completedBlock為空,直接閃退并拋出錯誤信息。即,completedBlock不能為空。 // Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @
"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead" );
復制代碼 確保url是正確的,加安全驗證,雖然url偶爾在字符串的情況下不報警告,但最好還是轉換成NSURL類型, if ([url isKindOfClass:NSString.class]) {url = [NSURL URLWithString:(NSString *)url];}// 防止url在某些特殊情況下(eg:NSNull)導致app閃退
if (![url isKindOfClass:NSURL.class]) {url = nil;}
復制代碼 首先方法要返回的是遵從了<SDWebImageOperation>協議的對象,所以聲明了一個對象SDWebImageCombinedOperation,該對象遵從了協議,下面會對其屬性進行一一設置。 而cancelled屬性是在UIView+WebCache第3點設置的。 @property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;
復制代碼 最后需要返回operation,所以進行創建、屬性賦值、返回。
// 創建一個SDWebImageCombinedOperation,加上 __block,可以讓它在后續block內進行修改,__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];// 加上__weak 避免保留環__weak SDWebImageCombinedOperation *weakOperation = operation;/*對operation 進行賦值操作,最后返回*/
return operation;
復制代碼 再次對url進行判斷,failedURLs類型是NSMutableSet<NSURL *>,是用來存儲錯誤url的集合 // 聲明一個BOOL值,isFailedUrlBOOL isFailedUrl = NO;
if (url) {// 創建一個同步鎖,@synchronized{}它防止不同的線程同時執行同一段代碼@synchronized (self.failedURLs) {// 錯誤的url都會放在failedURLs 中,判斷該url是否在里面,返回并賦值isFailedUrlisFailedUrl = [self.failedURLs containsObject:url];}}// 如果url長度為0,或者 options中沒有 SDWebImageRetryFailed(一直進行下載), 并且是錯誤的url
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {// 不再向下執行,直接回調completeBlock,并傳遞錯誤信息,url不存在,NSURLErrorFileDoesNotExist[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
return operation;}
復制代碼 runningOperations類型為NSMutableArray<SDWebImageCombinedOperation *>存儲所有待執行的操作任務 @synchronized (self.runningOperations) {// 把operation 存儲起來[self.runningOperations addObject:operation];}
復制代碼 在緩存中查找圖片,并將找到的圖片的相關信息返回, 同時對operation.cacheOperation屬性賦值。 (該方法是SDImageCache類的實例方法,下篇再分析) // 根據url 返回一個本地用來緩存的標志 keyNSString *key = [self cacheKeyForURL:url];operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key
done :^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) { // 查詢結束后執行操作 }];
復制代碼 緩存中是否查找到圖片,分別處理: a. 找不到圖片,但是允許從網絡下載,就進行網絡下載 // 如果執行過程中操作取消,安全移除操作
//
return 是跳出這個block
if (operation.isCancelled) {[self safelyRemoveOperationFromRunning:operation];
return ;}// 1. 如果不存在緩存圖片,或者需要刷新緩存 2. 代理可以響應方法,或者代理直接執行該方法,即從網絡下載圖片// 1 和 2 是并且關系
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) { // 網絡下載圖片
}
復制代碼 b. 如果找到了緩存圖片,回調圖片及相關信息,操作結束,安全移除操作
else if (cachedImage) {__strong __typeof(weakOperation) strongOperation = weakOperation;[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];[self safelyRemoveOperationFromRunning:operation];}
復制代碼 c. 緩存中找不到圖片,也不允許網絡下載圖片:
else {// Image not
in cache and download disallowed by delegate__strong __typeof(weakOperation) strongOperation = weakOperation;[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];[self safelyRemoveOperationFromRunning:operation];
}
復制代碼 對a步驟一步步分析:如果有緩存圖片,同時還要求刷新緩存,那么界面先加載緩存圖片,然后網絡下載,下載成功之后界面加載網絡圖片,然后在緩存中刷新之前的緩存圖片 if (cachedImage && options & SDWebImageRefreshCached) {// If image was found
in the cache but 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.// 如果在緩存中找到了圖片,但是設置了SDWebImageRefreshCached,因此要NSURLCache重新從服務器下載// 先調用completeBlock后續進行網絡下載[self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];}
復制代碼 根據SDWebImageOptions的選項對SDWebImageDownloaderOptions進行對接,一一對應,協調處理。|= 可以理解為添加 SDWebImageDownloaderOptions的詳細介紹點這里 downloaderOptions |= SDWebImageDownloaderLowPriority
// 等同于
downloaderOptions = downloaderOptions | SDWebImageDownloaderLowPriority
復制代碼 // downloaderOptions 默認為0SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;// 如果需要刷新緩存,downloaderOptions強制解除SDWebImageDownloaderProgressiveDownload,并且添加SDWebImageDownloaderIgnoreCachedResponse選項
if (cachedImage && options & SDWebImageRefreshCached) {// force progressive off
if image already cached but forced refreshingdownloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;// ignore image
read from NSURLCache
if image
if cached but force refreshingdownloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;}
復制代碼 通過url進行網絡下載圖片: 每一個下載SDWebImageDownloader對象對應于一個SDWebImageDownloadToken對象,目的是用于取消/移除SDWebImageDownloader對象。 通過SDWebImageDownloader的實例方法生成一個SDWebImageDownloadToken對象。(該方法下篇分析) SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
// 圖片下載完成之后的操作。
}];
復制代碼 對operation.cancelBlock賦值。 通過上面生成的subOperationToken來進行取消SDWebImageDownloader操作
operation.cancelBlock = ^{[self.imageDownloader cancel:subOperationToken];__strong __typeof(weakOperation) strongOperation = weakOperation;[self safelyRemoveOperationFromRunning:strongOperation];};
復制代碼 操作取消或者存在網絡錯誤的情況下: // 操作不存在或者操作取消的情況下不做任何處理。
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {// https://github.com/rs/SDWebImage/pull/699}
else if (error) {[self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];// 在下面情況下(不是因為網絡問題),url本身有問題的情況下,才會添加進failedURLs
if ( error.code != NSURLErrorNotConnectedToInternet&& error.code != NSURLErrorCancelled&& error.code != NSURLErrorTimedOut&& error.code != NSURLErrorInternationalRoamingOff&& error.code != NSURLErrorDataNotAllowed&& error.code != NSURLErrorCannotFindHost&& error.code != NSURLErrorCannotConnectToHost) {// 跟前面第4點對應,failedURLs添加錯誤的url@synchronized (self.failedURLs) {[self.failedURLs addObject:url];}}}
復制代碼 成功情況下: a. 對應于第8點,刷新緩存,并且圖片下載失敗的情況下: // 下載選項,允許失敗后重新下載,
if ((options & SDWebImageRetryFailed)) {// 重新下載,得保證 url 是正確的,不在failedURLs里面@synchronized (self.failedURLs) {[self.failedURLs removeObject:url];}}// 是否允許磁盤緩存BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);// 沒有下載圖片的情況下,不能刷新緩存
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {// Image refresh hit the NSURLCache cache,
do not call the completion block// 對應于第8點,已經返回completeBlock,這里不做任何處理。}
復制代碼 b. 1. 有下載圖片 2. 界面上下載圖片尚未賦值,或者策略允許圖片變換 3. 代理響應了圖片變換操作 1,2,3 是并且關系。 圖片先進行變換,然后緩存,最后回調
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {// 在全局隊列(并發)中,開啟一個子線程,異步執行,優先級比較高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];// pass nil
if the image was transformed, so we can recalculate the data from the image// 如果圖片變換成功,imageData傳nil,這樣在緩存圖片的時候,可以重新計算data大小,反之,就傳downloadedData[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData)
for Key:key toDisk:cacheOnDisk completion:nil];}// 回調信息[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];});}
復制代碼 c. 不對圖片進行處理,直接緩存圖片并回調。
else {
if (downloadedImage && finished) {[self.imageCache storeImage:downloadedImage imageData:downloadedData
for Key:key toDisk:cacheOnDisk completion:nil];}[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];}
復制代碼 總結:
總結
以上是生活随笔 為你收集整理的图片加载之SDWebImage(上) 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。