前言
SDWebImage 想必對於每一個iOS開發者來說,都不會陌生。每當我們的專案中需要給UIImageView設定網路圖片的時候,我們基本上第一個想到就是這個強大的第三方庫。我們可以手動匯入靜態庫,也可以直接用CocoaPod管理,只要簡單地輸入pod 'SDWebImage'
即可匯入,大家可以隨意選擇。
雖然在專案中經常用到這個庫,但是有時候我們可能只是知道用,卻並沒有去了解下這個強大庫實現原理,或者是寫這個庫的大佬程式碼思路、風格和各種技巧。這篇文章就是從SDWebImage
一行行原始碼來分析大佬的神級操作。
SDWebImage流程示意圖
從sd_setImageWithURL開始
在我們將SDWebImage
匯入到專案中的時候,點開core
資料夾可能會看到一大串檔案,剛開始肯定覺得很頭疼,這麼多檔案,這麼多檔案,我要看到什麼時候去,畏難心湧上來了,很多同學就在這裡倒下,把Xcode
一關,然後幹別的事去了...。其實這樣很正常,不過我們可以整理下,其實SDWebImage
真正核心的也就幾個檔案,我們可以把他們單獨拿出來,比如這樣
這樣可能會看得舒服點。把檔案整理好了,那我們就開始看下每個檔案有什麼用,幹什麼的了。我們先從最常用的方法開始:sd_setImageWithURL
我們隨便佈局一個UIImageView,然後設定圖片:
UIImageView *img = [[UIImageView alloc] init];
img.frame = CGRectMake(0, 0, 100, 100);
img.center = self.view.center;
img.backgroundColor = [UIColor orangeColor];
[self.view addSubview:img];
//載入圖片
[img sd_setImageWithURL:[NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1516084683&di=aeeadb9d3ae89483d06792970a46f829&imgtype=jpg&er=1&src=http%3A%2F%2Fp3.qhimg.com%2Ft010f0383e8e5f9bdbb.png"] placeholderImage:nil];
複製程式碼
我們可以點進這個方法看看是怎麼實現的:
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}
複製程式碼
然後一路點下去,你會發現,它們最終都會調到UIView+WebCache的sd_internalSetImageWithURL
方法裡面來執行,我們看看這裡面的程式碼:
- (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 *)context {
//獲取到繫結的key
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
//取消當前進行的操作組合
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
//用imageURLKey做key,將url作為屬性繫結到UIView上
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//“&”在這裡是個位運算,if括號裡面的運算表示options的值不是“延遲佔點陣圖展示”的話就成立。
if (!(options & SDWebImageDelayPlaceholder)) {
//巨集定義,回到主執行緒中設定圖片(設定佔點陣圖)
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
if (url) {
// check if activityView is enabled or not
//簡單翻譯就是 設定了activityView(正在載入指示器)為顯示的話,那麼就呼叫顯示activityView的方法...
if ([self sd_showActivityIndicatorView]) {
//去建立菊花(activityView)然後轉起來...
[self sd_addActivityIndicator];
}
__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) {
__strong __typeof (wself) sself = wself;
//先把菊花停掉...
[sself sd_removeActivityIndicator];
//如果 self被提前釋放了,直接返回
if (!sself) { return; }
//建立個bool變數 shouldCallCompletedBlock 只要圖片下載完成或者options設定為不需要自動設定圖片,它的值也為真。
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
//同樣 如果有圖片但是options值為SDWebImageAvoidAutoSetImage,或者沒有返回圖片且options值為SDWebImageDelayPlaceholder 值為真。
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
//一個block(執行完成回撥)
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
//如果 self被提前釋放了,直接返回
if (!sself) { return; }
//需要直接設定圖片的話,直接呼叫setNeedsLayout
if (!shouldNotSetImage) {
[sself sd_setNeedsLayout];
}
//將圖片用block回撥回去。
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) {
//主執行緒中執行 callCompletedBlockClojure block
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}
UIImage *targetImage = nil;
NSData *targetData = nil;
//如果我們獲取到了圖片,而且沒有設定“禁止自動設定圖片”,則將圖片賦值給targetImage,將圖片資料賦值給targetData.
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
targetImage = image;
targetData = data;
}
//如果我們沒有獲取到圖片,而且我們設定了“延遲設定佔點陣圖”,則將佔點陣圖賦值給targetImage,targetData設定為nil.
else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
targetImage = placeholder;
targetData = nil;
}
BOOL shouldUseGlobalQueue = NO;
//判斷是否設定圖片需要在併發對列中執行
//context是一個字典,可以設定key為“SDWebImageInternalSetImageInGlobalQueueKey”的值
if (context && [context valueForKey:SDWebImageInternalSetImageInGlobalQueueKey]) {
shouldUseGlobalQueue = [[context valueForKey:SDWebImageInternalSetImageInGlobalQueueKey] boolValue];
}
//三目運算,如果需要在全域性併發佇列執行則獲取獲取全域性併發佇列賦值給targetQueue,否則賦值主執行緒。
dispatch_queue_t targetQueue = shouldUseGlobalQueue ? dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0) : dispatch_get_main_queue();
dispatch_async(targetQueue, ^{
//在目標佇列中設定圖片
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
//主執行緒回撥
dispatch_main_async_safe(callCompletedBlockClojure);
});
}];
//為UIView繫結新的操作,因為之前把UIView上的操作取消了。
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
//如果url為空的話,
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);
}
});
}
}
複製程式碼
這個方法裡面程式碼有點多,剛開始一般過一遍都比較模稜兩可。不過跟著註釋應該能瞭解個大概的流程了。其實,總的來說這個方法差不多做了這三件事:
- 根據
key
取消當前的操作組合- 將
url
作為屬性繫結到UIView
上SDWebImageManager
根據url下載圖片
我們一個一個來看看:
1.取消當前操作
//根據key取消當前進行的操作組合
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
複製程式碼
這個很有必要,舉個栗子:當tableView
的cell
包含了UIImageView
被重用的時候,首先會執行這個方法,保證了下載和快取操作組合被取消了。
當我們點進方法去看可以看到它是在UIView+WebCacheOperation
裡實現的:
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
// Cancel in progress downloader from queue
//遍歷UIView上的所有正在下載的佇列,並取消佇列
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對應的Value,也就是所有的operations
[operationDictionary removeObjectForKey:key];
}
}
複製程式碼
- 在這個方法裡面有一個
SDOperationsDictionary
型別的operationDictionary
字典,其實SDOperationsDictionary
是NSMutableDictionary
的一個型別別名,它這樣定義的:
typedef NSMutableDictionary<NSString *, id> SDOperationsDictionary;
複製程式碼
這裡[self operationDictionary]
返回了一個字典賦給了operationDictionary
- 看看
operationDictionary
方法的實現:
- (SDOperationsDictionary *)operationDictionary {
//SDOperationsDictionary是NSMutableDictionary的一個型別別名,跟之前版本有點不一樣
//這裡用到了runtime的型別繫結,給UIView繫結了一個SDOperationsDictionary的operations
//先通過key來獲取UIView的operations屬性值
SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
//如果能獲取到值,直接返回
if (operations) {
return operations;
}
//如果沒有值,則初始化一個空的字典繫結給UIView,然後返回
operations = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
複製程式碼
註釋還算清楚,用runtime
給UIView
繫結了一個operation
字典,字典的key
是針對針對不同型別的試圖和不同型別的操作設定的字串,字典的value
是具體的操作(operation
)。
2.將url
作為屬性繫結到UIView
上
//用imageURLKey做key,將url作為屬性繫結到UIView上
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
複製程式碼
這裡也是利用了下runtime
的型別繫結,用imageURLKey
做為key
,將url
繫結到UIView
上。這樣,我們可以通過當前的UIView
直接獲取圖片的url
。
3.根據url
下載圖片
//下載圖片的方法
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
}
複製程式碼
我們可以看到,它是由一個SDWebImageManager
單例來呼叫loadImageWithURL
方法,最後會返回一個SDWebImageOperation
類型別的operation
。
1.我們點進去看看這個
SDWebImageManager
是怎麼初始化的:
//單例
+ (nonnull instancetype)sharedManager {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
//初始化,建立一個SDImageCache單例和SDWebImageDownloader單例,
- (nonnull instancetype)init {
SDImageCache *cache = [SDImageCache sharedImageCache];
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
return [self initWithCache:cache downloader:downloader];
}
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
if ((self = [super init])) {
//賦值SDImageCache和SDWebImageDownloader
_imageCache = cache;
_imageDownloader = downloader;
//新建一個NSMutableSet來儲存下載失敗的url.
_failedURLs = [NSMutableSet new];
//新建一個NSMutableArray儲存下載中的Operation
_runningOperations = [NSMutableArray new];
}
return self;
}
複製程式碼
在初始化中,我們還初始化了四個了變數:
SDImageCache
型別的_imageCache
,實現快取管理的變數;SDWebImageDownloader
型別的_imageDownloader
,實現圖片下載的變數;NSMutableSet
型別的_failedURLs
,存那些失敗的url的變數;NSMutableArray
型別的_runningOperations
,存放正在執行的任務的比變數。
這裡要囉嗦下的就是為什麼_failedURLs
要用NSMutableSet
型別而不用NSMutableArray
型別。因為我們在搜尋列表中元素的時候,NSSet要比NSArray更高效,NSSet裡面的實現用到hash(雜湊)演算法,存和取都可以快速的定位。而在NSSArray中,要找其中某個元素,基本上都是要把所有元素遍歷一遍,很明顯的效率就低了很多。還有就是NSSet裡面不會有重複的元素,同一個下載失敗的url也只會存在一個。(看吧,小技巧~)
2.看它的下載方法裡面是怎麼實現的
//通過url建立一個Operation
- (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是字串型別,則將url轉換成NSURL型別(容錯處理)
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
//如果經過上述處理後的url還是不是NSURL型別,那麼將url設定為nil.
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO;
if (url) {
//建立一個互斥鎖防止,保證執行緒安全
@synchronized (self.failedURLs) {
//判斷url是否是下載失敗過,
isFailedUrl = [self.failedURLs containsObject:url];
}
}
//如果url不存在那麼直接返回一個block,如果url存在那麼繼續
//如果options的值沒有設定為失敗後重試並且url下載失敗過,執行完成block返回錯誤
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;
}
//將operations加入到runningOperations陣列中
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
//根據url獲取到對應的key
NSString *key = [self cacheKeyForURL:url];
//根據key非同步在快取中查詢是否有圖片,且返回一個operation
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
//如果operation取消了,將它從self.runningOperations中移除
if (operation.isCancelled) {
[self safelyRemoveOperationFromRunning:operation];
return;
}
//條件1.在快取中沒有找到圖片或者options設定為了SDWebImageRefreshCached
//條件2.代理允許下載,或者代理不響應imageManager:shouldDownloadImageForURL:方法
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
//如果在快取中找到了圖片,但是需要更新快取
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.
//傳遞快取中的image,同時嘗試重新下載它來讓NSURLCache有機會接收伺服器端的更新
[self callCompletionBlockForOperation:weakOperation 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
//設定downloaderOptions
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;
}
//到這裡才真的去下載圖片...
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
//如果操作取消,do nothing
if (!strongOperation || strongOperation.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
}
//如果有錯誤,將error回撥出去
else if (error) {
[self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost
&& error.code != NSURLErrorNetworkConnectionLost) {
@synchronized (self.failedURLs) {
//將url新增到失敗列表中、
[self.failedURLs addObject:url];
}
}
}
else {
//如果設定了下載失敗重試,則將url從列表中移除、
if ((options & SDWebImageRetryFailed)) {
@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
}
//圖片下載成功,且設定了圖片變形,代理也能響應變形方法
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), ^{
//呼叫代理方法完成圖片的transform
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
//對已經transform的圖片進行快取
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
// pass nil if the image was transformed, so we can recalculate the data from the image
[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
}
//執行回撥
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {
//如果不需要做圖片的變形,而且圖片下載完成,那麼就直接快取
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
//執行完成回撥
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
//如果已經完成下載了,將operation移除
if (finished) {
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
@synchronized(operation) {
// Need same lock to ensure cancelBlock called because cancel method can be called in different queue
operation.cancelBlock = ^{
[self.imageDownloader cancel:subOperationToken];
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self safelyRemoveOperationFromRunning:strongOperation];
};
}
}
//其他情況:代理不允許下載,或者是沒有設定更新快取
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];
//移除operation
[self safelyRemoveOperationFromRunning:operation];
}
//圖片既沒有在快取中找到,代理也不允許下載
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];
//移除operation
[self safelyRemoveOperationFromRunning:operation];
}
}];
return operation;
}
複製程式碼
看上去程式碼量有點多,邏輯很複雜,其實確實也是這樣的~(哈哈,我剛看的時候,腦殼都要繞暈了~)。不過整理下發現,其實差不多也就是做了兩件事:
- 通過url對應的key去快取裡找有沒有對應的圖片;
self.imageDownloadem
呼叫下載方法去下載圖片。
然後要補充的幾點是:
- SDWebImageCombinedOperation
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;
@end
@implementation SDWebImageCombinedOperation
- (void)setCancelBlock:(nullable SDWebImageNoParamsBlock)cancelBlock {
// check if the operation is already cancelled, then we just call the cancelBlock
if (self.isCancelled) {
if (cancelBlock) {
cancelBlock();
}
_cancelBlock = nil; // don't forget to nil the cancelBlock, otherwise we will get crashes
} else {
_cancelBlock = [cancelBlock copy];
}
}
- (void)cancel {
@synchronized(self) {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
self.cancelBlock = nil;
}
}
}
@end
複製程式碼
它有三個屬性,一個是否取消的Bool
屬值,一個取消``Bool和一個快取
operation,很明顯,我們可以通過它來取消下載
operation`。
在上面的下載方法中,有一個__block
修飾的operation
和一個__weak
修飾的weakOperation
,
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
複製程式碼
我們說下***__block*** 和 ***__weak***的區別:
__block
用於指明當前宣告的變數被block
捕獲後,可以在block
中改變變數的值。因為在block
宣告的同時會捕獲該block
所使用的全部自動變數的值,這些值在block
中只有“使用權”而沒有“修改權”。而用__block
修飾的變數,則可以在block
改變變數的值了。同時,__block
並不是弱引用,如果有迴圈引用的話,我們需要在block
內部手動釋放,將其設定為nil
。
__weak
是所有權修飾符,__weak
本身是可以避免迴圈引用的,我們在block
內部使用的時候,需要用__strong
來強引用,防止變數提前釋放掉了。
4.在快取中查詢圖片:queryCacheOperationForKey
這個方法是SDImageCache
裡面定義的一個方法,我們開方法的實現:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
//如果key為空,回撥完成block,return
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// First check the in-memory cache...
//首先通過key在記憶體的快取中查詢有沒有圖片...
UIImage *image = [self imageFromMemoryCacheForKey:key];
//如果有記憶體快取中有圖片
if (image) {
NSData *diskData = nil;
//如果gif圖片(非動圖)
if (image.images) {
//通過key找到磁碟中圖片的data
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
//執行完成block
if (doneBlock) {
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
return nil;
}
//如果記憶體中沒有,則在磁碟中查詢,如果找到,則將其放在記憶體快取中,並呼叫doneBlock
NSOperation *operation = [NSOperation new];
//在ioQueue中序列處理所有磁碟快取
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
//建立自動釋放池,記憶體及時釋放
@autoreleasepool {
//根據key去磁碟中找到圖片的data資料
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
//根據圖片的url的對應的key去磁碟快取中查詢圖片
UIImage *diskImage = [self diskImageForKey:key];
//如果磁碟中有圖片,並且設定了記憶體快取機制,再將圖片快取在記憶體中
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
//用NSCache將圖片儲存到記憶體中
[self.memCache setObject:diskImage forKey:key cost:cost];
}
if (doneBlock) {
//主執行緒執行完成block
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
});
return operation;
}
複製程式碼
方法中用到了一個序列佇列self.ioQueue
,它的宣告和初始化是這樣的:
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;
// Create IO serial queue
ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
複製程式碼
總結
到目前為止,只進行到了通過key去快取中查詢有沒有圖片,在完成回撥中,我們經過一些邏輯處理,到最後面才去用self.imageDownloader
去呼叫download
方法。下篇我們可能就會去了解下download
方法是如何下載的,網路是怎麼處理的。
(PS,有很多理解不到位的地方,還希望和大家一起交流學習~)