SDWebImage 是一個為蘋果各個平臺提供圖片下載和緩存操作的開源庫。相信只要從事 iOS 開發(fā)就算沒用過也至少聽說過。所以閱讀它的碼源并進行分析,對于開發(fā)者尤其是對組件封裝不太熟悉的,應(yīng)該有所裨益。
從 UML 圖和時序圖說起
從 UML 圖看類結(jié)構(gòu)

這張是 SDWebImage 的 UML 圖,我在圖中做了些基本關(guān)系說明。
從這張圖來分析,SD 通過分類給常見的 View 提供下載接口。其內(nèi)部的結(jié)構(gòu)主要涉及到以下幾個大類:
SDWebImageManager: 控制中心,管理圖片的下載和緩存
SDWebImageDownloder: 下載中心,對所有的下載操作進行管理
SDWebImageDownloderOperation: 下載操作,繼承 NSOperation 處理單個圖片的下載
SDImageCache: 緩存中心,負責對圖片的內(nèi)存和磁盤緩存進行管理
SDWebImageCodersManager: 圖片壓縮編碼,將圖片按照格式進行壓縮,轉(zhuǎn)換成 NSData
SDWebImagePrefetcher: 預(yù)下載圖片,負責處理需要預(yù)下載的圖片,下載等級是 SDWebImageLowPriority
從圖中感受到的一點就是,分工明確,各司其職。暴露給外部的接口簡潔清晰。
從時序圖看執(zhí)行邏輯

從時序圖中可以看出 SDWebImage 的執(zhí)行邏輯是,外部提供 URL ,之后 Manager 根據(jù) URL 在緩存中查找,是否有相應(yīng)的圖片。如果有,則將圖片返回給外部。如果緩存中沒有圖片或者外部要求直接從網(wǎng)絡(luò)下載圖片,則從網(wǎng)絡(luò)異步下載圖片。圖片下載完成之后,異步存儲到內(nèi)存和磁盤,同時返回給外部。
從 UML 和 時序圖,可以很直觀的看出整個項目的架構(gòu)和內(nèi)部邏輯,自己以后在封裝組件的時候也應(yīng)該試著畫一畫,不但方便別人理解,自己也能從更宏觀的角度看整個組件,往往能找可以優(yōu)化的點,甚至是新的設(shè)計思路。
SDWebImage 下載和緩存
SD 最核心的內(nèi)容就是其下載和緩存有下列特性,同時提供的接口又簡潔清晰,所以才如此受大家歡迎。
- 使用分類實現(xiàn) UIImageView, UIButton, MKAnnotationView 的圖片下載以及緩存管理
- 圖片異步下載
- 異步緩存圖(內(nèi)存和磁盤),同時有相應(yīng)的移除策略
- 后臺進行圖片解壓
- 對 url 進行校驗,同樣的 url 不會多次重復下載
- 對失敗的 url 進行校驗,不會重復多次去執(zhí)行下載操作
- 不會阻塞主線程
下面就從碼源分析,以上的特性是如何實現(xiàn)的。
SDWebImageManager 控制中心
SDWebImageManager 提供了一個單利對象,用來統(tǒng)一控制下載和緩存操作。操作中主要涉及的類和方法如下:
SDWebImageOptions: 枚舉類來控制下載操作中的配置,主要配置下載的優(yōu)先級,策略,緩存策略等
SDWebImageCombinedOperatio: 一個操作對象,負責一個圖片的下載和緩存
SDWebImageCacheKeyFilterBlock: 允許用戶根據(jù) Url 自定義緩存的 Key
SDWebImageManagerDelegate: 提供下載圖片之前和之后的回調(diào)
當外部需要根據(jù) Url 獲取一張圖片,會調(diào)用下面方法
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 沒有定義完成回調(diào),期望這種下載操作通過 SDWebImagePrefetcher 預(yù)下載完成。
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// 如果外部不小心傳入的是 string 轉(zhuǎn)換為對應(yīng)的 url
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 防止因為因為類型錯誤,引發(fā)崩潰
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
// 創(chuàng)建一個操作對象
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
BOOL isFailedUrl = NO;
if (url) {
//檢測該 Url 是否是之前下載失敗的 Url,通過 @synchronized 加鎖同步操作 _failedURLs
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}
// url 錯誤,或者該 url 是失敗 url 且不要求重試失敗 url ,則直接返回
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;
}
// 在操作數(shù)組中,加入當前操作
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
NSString *key = [self cacheKeyForURL:url];
// 獲取當前緩存配置
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
// 根據(jù) options 的配置,從緩存中讀取 image
__weak SDWebImageCombinedOperation *weakOperation = operation;
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
//先檢查當前 operation
if (!strongOperation || strongOperation.isCancelled) {
[self safelyRemoveOperationFromRunning:strongOperation];
return;
}
// 緩存沒有獲取到圖片,或者外部要求從網(wǎng)絡(luò)刷新圖片,則進行下載操作
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) {
// 如果緩存中取得了圖片,并且要求刷新圖片,則先返回緩存中的圖片給外部,之后繼續(xù)從網(wǎng)絡(luò)下載并刷新緩存中的圖片
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
// 獲取當前下載配置
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) {
// 如果是刷新緩存操作,不執(zhí)行漸進下載,同時需要執(zhí)行緩存操作
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
// 下載完成操作 block 回調(diào),需要再一次 weak-strong 操作,避免循環(huán)引用。
__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];
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) {
//下載失敗,并且排除上述網(wǎng)絡(luò)原因,將該 Url 置為操作失敗的 Url
[self.failedURLs addObject:url];
}
}
}
else {
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
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
// 設(shè)置了 NSUrlCache 作為默認緩存,則不執(zhí)行自定義的緩存
} 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
[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) 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) {
[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) {
//直接返回緩存中的 image
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
} else {
// 緩存中沒有 image ,也不允許從網(wǎng)絡(luò)加載
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
return operation;
}
SDWebImageCombinedOperation 遵循 SDWebImageOperation 協(xié)議,實現(xiàn)了取消操作方法。
- (void)cancel {
@synchronized(self) {
self.cancelled = YES;
//取消緩存操作
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
//取消下載操作
if (self.downloadToken) {
[self.manager.imageDownloader cancel:self.downloadToken];
}
//將操作移除
[self.manager safelyRemoveOperationFromRunning:self];
}
}
SDWebImageManager 的核心就是這個,統(tǒng)籌負責下載和緩存的協(xié)調(diào)。
下載
SD 的下載主要由 SDWebImageDownloader 和 SDWebImageDownloaderOperation 這兩個類來實現(xiàn)。前者是一個單例,統(tǒng)籌所有的下載操作,后者繼承自 NSOperation 自定義了下載操作。
SDWebImageDownloader 下載中心
SDWebImageDownloader 主要負責圖片的下載控制,主要涉及下面的屬性和方法:
SDWebImageDownloaderOptions: 枚舉下載配置,配置來源是 SDWebImageOptions 中的下載配置
SDWebImageDownloaderExecutionOrder: 下載順序,有 FIFO 的隊列形式和 LIFO 的棧形式
SDWebImageDownloadToken: 單個下載操作記號
maxConcurrentDownloads: 并發(fā)的最大下載數(shù)
urlCredential : 處理 url 證書信息
SDWebImageManager 進入下載環(huán)節(jié)之后,會調(diào)用下列方法:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
// 返回給外部一個 SDWebImageDownloadToken,可以取消該次下載操作
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
//返回一個下載 operation
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 避免重復緩存,確認使用的是默認的 NSURLCache 策略,還是自定義的緩存策略
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
//管道下載技術(shù)
request.HTTPShouldUsePipelining = YES;
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself allHTTPHeaderFields]);
}
else {
request.allHTTPHeaderFields = [sself allHTTPHeaderFields];
}
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
//解壓圖片,會展示高質(zhì)量的圖片,但是會增加內(nèi)存消耗
operation.shouldDecompressImages = sself.shouldDecompressImages;
//url 證書信任
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
[sself.downloadQueue addOperation:operation];
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
// LIFO 的情況下,將前一個下載操作依賴當前操作,實現(xiàn) LIFO 邏輯,在這種情況下設(shè)置 NSOperationQueue 的最大并發(fā)數(shù)并沒有效果,按照依賴來
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
上面的下載方法,在 block 中定義了怎么創(chuàng)建當前下載的 operation ,對 operation 及其回調(diào)的操作在下列方法中。
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
//加鎖同步操作
LOCK(self.operationsLock);
SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
if (!operation) {
//調(diào)用 block 創(chuàng)建 operation
operation = createCallback();
__weak typeof(self) wself = self;
operation.completionBlock = ^{
__strong typeof(wself) sself = wself;
if (!sself) {
return;
}
//操作完成,加鎖同步移除當前操作
LOCK(sself.operationsLock);
[sself.URLOperations removeObjectForKey:url];
UNLOCK(sself.operationsLock);
};
[self.URLOperations setObject:operation forKey:url];
}
UNLOCK(self.operationsLock);
//創(chuàng)建當前下載操作的回調(diào)的字典, 該字典類型在 SDWebImageDownloaderOperation 中申明且未公開,所以這里使用 id ,外部不需要關(guān)心具體的類型
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
上面添加回調(diào)的方法中加鎖的實現(xiàn)采用了信號量來構(gòu)建
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
使用信號量來做的主要原因是,性能比其他加鎖方式高;
@synchronized 這種加鎖方式開銷大,但代碼簡單易使,比較適合用來做一些簡單的操作。
SDWebImageDownloadToken 遵循并實現(xiàn)了 SDWebImageOperation 協(xié)議
- (void)cancel {
if (self.downloadOperation) {
//取消當前下載操作, SDWebImageManager 的當前 combinedOperation 調(diào)用 cancel 的時候會調(diào)用到這
SDWebImageDownloadToken *cancelToken = self.downloadOperationCancelToken;
if (cancelToken) {
[self.downloadOperation cancel:cancelToken];
}
}
}
SDWebImageManager 和 SDWebImageDownloader 都用一個單獨的類根據(jù) url 來管理當前的 operation ,實現(xiàn)取消的方法。這樣就將取消操作部分邏輯剝離出來,比較清晰明了。
SDWebImageDownloaderOperation 下載操作
typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary; 用來存放 progress 和 complete 回調(diào)的字典。SDWebImageDownloadToken 的屬性 downloadOperationCancelToken 真正的類型就是這個。
SDWebImageDownloaderOperation 繼承自 NSoperation ,關(guān)于自定義 NSOperation 在這里不做闡述。需要的人可以參考這篇文章iOS 并發(fā)編程之 Operation Queues
這里著重講下里面的 start 方法,主要下載操作的邏輯都在這里。
- (void)start {
@synchronized (self) {
//檢查操作是否被取消
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
#if SD_UIKIT
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
//如果外部配置了后臺執(zhí)行下載任務(wù),執(zhí)行時間到達設(shè)定時間時,取消該后臺任務(wù)
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
/**
* Create the session for this task
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
* method calls and completion handler calls.
*/
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
// 采用的是 NSURLCache 緩存的話,先獲取之前的緩存,之后對比
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
// Grab the cached data for later check
NSURLCache *URLCache = session.configuration.URLCache;
if (!URLCache) {
URLCache = [NSURLCache sharedURLCache];
}
NSCachedURLResponse *cachedResponse;
// NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483
@synchronized (URLCache) {
cachedResponse = [URLCache cachedResponseForRequest:self.request];
}
if (cachedResponse) {
self.cachedData = cachedResponse.data;
}
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
[self.dataTask resume];
if (self.dataTask) {
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
});
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
}
#if SD_UIKIT
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
//如果后臺任務(wù)還在執(zhí)行,結(jié)束后臺任務(wù)
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
下面說下取消下載任務(wù)方法:
- (BOOL)cancel:(nullable id)token {
__block BOOL shouldCancel = NO;
/*
使用柵欄來做線程同步,因為是同步執(zhí)行,barrierQueue 是并行隊列,這里的邏輯是等到 block 里面的執(zhí)行完畢,才繼續(xù)下一步。
值得注意的是,這里不能用串行隊列,如果這樣會造成死鎖
**/
dispatch_barrier_sync(self.barrierQueue, ^{
[self.callbackBlocks removeObjectIdenticalTo:token];
if (self.callbackBlocks.count == 0) {
shouldCancel = YES;
}
});
if (shouldCancel) {
[self cancel];
}
return shouldCancel;
}
- (void)cancel {
@synchronized (self) {
[self cancelInternal];
}
}
- (void)cancelInternal {
if (self.isFinished) return;
[super cancel];
if (self.dataTask) {
[self.dataTask cancel];
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
});
// As we cancelled the task, its callback won't be called and thus won't
// maintain the isFinished and isExecuting flags.
// 修改操作狀態(tài), 在他們的 setter 方法中,會發(fā)送 KVO 通知
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
}
[self reset];
}
- (void)reset {
__weak typeof(self) weakSelf = self;
dispatch_barrier_async(self.barrierQueue, ^{
[weakSelf.callbackBlocks removeAllObjects];
});
self.dataTask = nil;
NSOperationQueue *delegateQueue;
if (self.unownedSession) {
delegateQueue = self.unownedSession.delegateQueue;
} else {
delegateQueue = self.ownedSession.delegateQueue;
}
if (delegateQueue) {
NSAssert(delegateQueue.maxConcurrentOperationCount == 1, @"NSURLSession delegate queue should be a serial queue");
[delegateQueue addOperationWithBlock:^{
weakSelf.imageData = nil;
}];
}
if (self.ownedSession) {
[self.ownedSession invalidateAndCancel];
self.ownedSession = nil;
}
}
緩存
如果外部沒有特殊需求,默認會走 SD 自定義的緩存策略,而不是 NSUrlCache。
SDImageCache 緩存中心
SDImageCache 是一個單例,來操作所有緩存。
初始化
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory {
if ((self = [super init])) {
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
// Create IO serial queue
// 創(chuàng)建了一個串行隊列,用于操作本地緩存
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
_config = [[SDImageCacheConfig alloc] init];
// Init the memory cache
_memCache = [[NSCache alloc] init];
_memCache.name = fullNamespace;
// Init the disk cache
if (directory != nil) {
_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
} else {
NSString *path = [self makeDiskCachePath:ns];
_diskCachePath = path;
}
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
#if SD_UIKIT
// Subscribe to app events
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
}
return self;
}
存儲圖片
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
if (!image || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
// if memory cache is enabled
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
if (toDisk) {
dispatch_async(self.ioQueue, ^{
/*
ARC 不使用于 Core Graphics 和 Core Text 這類 C 的庫,需要添加自定釋放池
**/
@autoreleasepool {
NSData *data = imageData;
if (!data && image) {
// If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
SDImageFormat format;
if (SDCGImageRefContainsAlpha(image.CGImage)) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
}
[self _storeImageDataToDisk:data forKey:key];
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
}
- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
if (!imageData || !key) {
return;
}
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// get cache Path for image key
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// transform to NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
// disable iCloud backup
if (self.config.shouldDisableiCloud) {
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
存儲操作時有用到自動釋放池,哪些時候需要使用自動釋放池,可以根據(jù) 蘋果的文檔 來判斷。
查詢
內(nèi)存查詢主要根據(jù) key 來檢索有沒有圖片,磁盤查詢主要檢索有沒有根據(jù) key 生成的 path 路徑相關(guān)文件的存在。
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
NSOperation *operation = [NSOperation new];
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
//可能有大量的 image 圖片需要查詢,在一個 RunLoop 循環(huán)中會產(chǎn)生很多臨時對象在 Autorelease Pool 中不能釋放,使用 @autorelease{} 來及時釋放對象,避免內(nèi)存占用過高。
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage = image;
if (!diskImage && diskData) {
// decode image data only if in-memory cache missed
diskImage = [self diskImageForKey:key data:diskData];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
if (doneBlock) {
if (options & SDImageCacheQueryDiskSync) {
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
}
};
if (options & SDImageCacheQueryDiskSync) {
queryDiskBlock();
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
移除策略
當內(nèi)存發(fā)出警告 SD 會清理內(nèi)存,
當 APP 將被掛起或者殺死,SD 都會查詢一遍本地緩存,如果本地緩存超出設(shè)置的容量,則 SD 會相應(yīng)的清理本地磁盤。
//NOTE 先刪除超過存儲時限的文件,要是還超過最大容量,則按照時間正序刪除文件至最大容量的一半
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
//NOTE 獲取緩存文件相關(guān)屬性(文件路徑,最后修改日期,文件大?。? // This enumerator prefetches useful properties for our cache files.
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
// Enumerate all of the files in the cache directory. This loop has two purposes:
//
// 1. Removing files that are older than the expiration date.
// 2. Storing file attributes for the size-based cleanup pass.
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSError *error;
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
// Skip directories and errors.
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// Remove files that are older than the expiration date;
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// Store a reference to this file and account for its total size.
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
cacheFiles[fileURL] = resourceValues;
}
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
// If our remaining disk cache exceeds a configured maximum size, perform a second
// size-based cleanup pass. We delete the oldest files first.
if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
// Target half of our maximum cache size for this cleanup pass.
const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
// Sort the remaining cache files by their last modification time (oldest first).
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// Delete files until we fall below our desired cache size.
for (NSURL *fileURL in sortedFiles) {
if ([_fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
總結(jié)
以上就是 SD 閱讀的主要記錄,當然還有很多細節(jié)和旁支沒有寫出,之后會再對個人覺得 SD 中好的點做一個記錄。同時如果你發(fā)現(xiàn)上文中有什么不明白或者不對的地方,都歡迎與我交流,相互成長??。