SDWebImage (v4.4.1)
以在一個UIImageView上加載網絡圖片為例,看看要經歷那些流程。
第一步,調用
- (void)sd_setImageWithURL:(nullable NSURL *)url;
- 這里運用了一個代碼技巧,Designated Initializer,我們可以稱之為"全能初始化法"。提供了一個初始化方法,其他方法均調用此方法。
第二步,調用
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock;
第三步,調用
/**
* @param url 圖片地址鏈接
* @param placeholder 占位圖
* @param options 下載圖片的枚舉。包括優先級、是否寫入硬盤等
* @param operationKey 一個記錄當前對象正在加載操作的key、保證只有最新的操作在進行、默認為類名。
所以如果你想下載多個圖片并且都展示一下、可以嘗試自定義幾個operationKey來操作。(我猜)
* @param setImageBlock 給開發者自定義set圖片的callback
* @param progressBlock 下載進度callback
* @param completedBlock 下載完成的callback(sd已經給你set好了、只是會把圖片給你罷了)
* @param context 一些額外的上下文字典。比如你可以搞一個專屬的imageManager進來干活。
*/
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock;
這個方法是加載圖片的最后一步。
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
context:(nullable NSDictionary<NSString *, id> *)context {
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);//省略版三目運算,實際上直接執行后邊這個方法
[self sd_cancelImageLoadOperationWithKey:validOperationKey];//取消之前的下載任務
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);//動態添加屬性
if (!(options & SDWebImageDelayPlaceholder)) {
//如果模式不是 SDWebImageDelayPlaceholder, 則先設置占位圖
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
if (url) {
#if SD_UIKIT
// check if activityView is enabled or not
if ([self sd_showActivityIndicatorView]){
[self sd_addActivityIndicator];
}
#endif
// reset the progress
self.sd_imageProgress.totalUnitCount = 0;
self.sd_imageProgress.completedUnitCount = 0;
SDWebImageManager *manager = [context objectForKey:SDWebImageExternalCustomManagerKey];//創建一個空的單例
if (!manager) {
manager = [SDWebImageManager sharedManager];//創建單例
}
__weak __typeof(self)wself = self;
SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
wself.sd_imageProgress.totalUnitCount = expectedSize;
wself.sd_imageProgress.completedUnitCount = receivedSize;
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
//圖片下載&讀取完成
__strong __typeof (wself) sself = wself;
if (!sself) { return; }
#if SD_UIKIT
[sself sd_removeActivityIndicator];//移除進度條
#endif
// if the progress not been updated, mark it to complete state
if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!sself) { return; }
if (!shouldNotSetImage) {
[sself sd_setNeedsLayout];
}
if (completedBlock && shouldCallCompletedBlock) {
//操作完成的回調
completedBlock(image, error, cacheType, url);
}
};
// case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
// OR
// case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
//如果沒有image,則使用占位圖
targetImage = placeholder;
targetData = nil;
}
#if SD_UIKIT || SD_MAC
// check whether we should use the image transition
SDWebImageTransition *transition = nil;
if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
transition = sself.sd_imageTransition;
}
#endif
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
#endif
callCompletedBlockClojure();
});
}];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
//沒有url的情況
dispatch_main_async_safe(^{
#if SD_UIKIT
[self sd_removeActivityIndicator];
#endif
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
整個過程可以分為以下幾部分。
取消該控件之前的下載任務->將圖片url作為屬性關聯到UIImageView->設置占位圖->設置進度條->SDWebImageManager獲取圖片(同時將operation加入到SDOperationsDictionary)->成功/失敗回調
SDWebImageManager獲取圖片獲取圖片
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
//非常奇怪的錯誤。
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
//為該類型不匹配拋出任何警告。這里我們通過允許URL作為NSString來傳遞這個錯誤。
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;
}
//創建下載操作的對象
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
BOOL isFailedUrl = NO;
if (url)
{
//加鎖,這里原來使用@synchronized,后來換成dispatch_semaphore_wait,為了性能
LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
//發出失敗的回調
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
return operation;
}
LOCK(self.runningOperationsLock);
//將操作添加到正在進行的操作數池
[self.runningOperations addObject:operation];
UNLOCK(self.runningOperationsLock);
//默認就是url作為key
NSString *key = [self cacheKeyForURL:url];
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
__weak SDWebImageCombinedOperation *weakOperation = operation;
#pragma mark -----在本地查找緩存
//通過key、查找本地圖片
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled)
{
//操作被取消、移除操作池
[self safelyRemoveOperationFromRunning :strongOperation];
return;
}
// Check whether we should download image from network
//本地沒有圖片 || 刷新緩存
BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
if (shouldDownload) {
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.
//發出刷新圖片的回調
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
// download if no image or requested to refresh anyway, and download allowed by delegate
//下面是根據調用者傳進來的option,來匹配設置了哪些,就給downloaderOptions賦值哪些option
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 (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
if (cachedImage && 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;
}
// `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle
//下載圖片
__weak typeof(strongOperation) weakSubOperation = strongOperation;
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
if (!strongSubOperation || strongSubOperation.isCancelled) {
// Do nothing if the operation was cancelled
// See #699 for more details
// if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
} else if (error) {
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
BOOL shouldBlockFailedURL;
// Check whether we should block failed url
if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
} else {
shouldBlockFailedURL = ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost
&& error.code != NSURLErrorNetworkConnectionLost);
}
if (shouldBlockFailedURL)
{
LOCK(self.failedURLsLock);
//失敗記錄
[self.failedURLs addObject:url];
UNLOCK(self.failedURLsLock);
}
}
else
{
if ((options & SDWebImageRetryFailed))
{
//失敗重新下載
LOCK(self.failedURLsLock);
//從失敗記錄移除
[self.failedURLs removeObject:url];
UNLOCK(self.failedURLsLock);
}
//是否磁盤緩存
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
// We've done the scale process in SDWebImageDownloader with the shared manager, this is used for custom manager and avoid extra scale.
if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
//縮放
downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
//成功下載圖片、自定義實現了圖片處理的代理
} 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), ^{
@autoreleasepool {
//獲取轉換用戶后的圖片
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
//用戶處理成功
if (transformedImage && finished)
{
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
NSData *cacheData;
// pass nil if the image was transformed, so we can recalculate the data from the image
if (self.cacheSerializer) {
cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
} else {
cacheData = (imageWasTransformed ? nil : downloadedData);
}
//用戶處理的后若未生成新的圖片、則保存下載的二進制文件。
//不然則由imageCache內部生成二進制文件保存
[self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}
//回調
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
});
} else {
//下載成功且未自定義代理--默認保存
if (downloadedImage && finished) {
if (self.cacheSerializer) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
[self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}
});
} else {
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
}
//回調
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
if (finished) {
[self safelyRemoveOperationFromRunning:strongSubOperation];
}
}];
} else if (cachedImage) {
//本地有圖片--回調、關閉當前操作
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
} else {
// Image not in cache and download disallowed by delegate
//本地沒有、也不下載--回調、關閉當前操作
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
return operation;
}
這里的開頭部分,我們需要去判斷url的合法性
if ([url isKindOfClass:NSString.class])
{
url = [NSURL URLWithString:(NSString *)url];
}
......
if (![url isKindOfClass:NSURL.class])
{
url = nil;
}
然后去創建下載操作的對象,這里我們需要再判斷一次,url是否在失敗清單上。
這里的失敗列表是NSMutableSet來創建的,原因么,因為hash的原因,判斷一個元素在不在里面非常高效。
同樣的道理,如果url沒有問題,就需要添加到正在進行操作的set中。
在這里,將url直接做為key。
NSString *key = [self cacheKeyForURL:url];
然后,去看如何在本次查找緩存,這個同樣是個非常復雜的方法。如果取消了操作,就把當前操作剔除出正在進行操作的set中。
這里有個有個不大眼熟的代碼
if (cachedImage && 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;
}
x|=y; 等價于x=x|y; 為位運算里面的或
x&=~y 等價于 x=x&(~y); 先對y取反 再和x做與運算
如果本地沒有圖片,或者需要刷新緩存,這里就需要去下載圖片了。這里有個很有意思的點
SDWebImageCombinedOperation
->SDWebImageDownloadToken
->downloadOperationCancelToken
, which is aSDCallbacksDictionary
and retain the completed block below, so we need weak-strong again to avoid retain cycle
這里產生了一個retain cycle。所以添加了一個循環
__weak typeof(strongOperation) weakSubOperation = strongOperation;
......
__strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
下載成功之后,在全局隊列將數據轉換成為圖片,并且存入緩存。