前言
這是本系列的第 4 篇,本篇將主要介紹 SDWebImageDownloader
這個負責下載的類,當然還有一些相關類及協議,如: SDWebImageDownloadToken
、SDWebImageDownloaderOperation
和 SDWebImageDownloaderOperationInterface
等。
正文
開啟正文描述之前,依舊先看 2 個重要的枚舉:SDWebImageDownloaderOptions
和 SDWebImageDownloaderExecutionOrder
,具體含義見下方代碼注釋。
// 控制下載過程的選項
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
// 降低下載任務在隊列中的優先級
SDWebImageDownloaderLowPriority = 1 << 0,
// 圖片將在下載過程中逐步展示,而不是等下載完成后才一次性展示
SDWebImageDownloaderProgressiveDownload = 1 << 1,
// 使用 NSURLCache,默認是不使用的
SDWebImageDownloaderUseNSURLCache = 1 << 2,
// 如果圖片是從 NSURLCache 讀取的,那么執行 completionHandler 的時候,回傳 nil
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
// 后臺繼續執行任務
SDWebImageDownloaderContinueInBackground = 1 << 4,
// 允許處理存儲在 NSHTTPCookieStore 中的 Cookie
SDWebImageDownloaderHandleCookies = 1 << 5,
// 允許不受信任的 SSL 證書,生產環境慎用
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
// 提高下載任務在隊列中的優先級
SDWebImageDownloaderHighPriority = 1 << 7,
// 縮放大圖
SDWebImageDownloaderScaleDownLargeImages = 1 << 8,
};
// 任務的執行順序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
// 先進先出,也是隊列的默認執行順序
SDWebImageDownloaderFIFOExecutionOrder,
// 后進先出,棧的執行順序
SDWebImageDownloaderLIFOExecutionOrder
};
接下來就到了 SDWebImageDownloader 這個類,我不準備一個屬性一個方法地按順序討論,而是先說創建方法,然后通過一個主要方法將主體串起來。先看創建方法吧!
對外其實只公開了一個創建單例的方法 sharedDownloader
,仔細查看代碼會發現,最終調用的是 - (nonnull instancetype)initWithSessionConfiguration:
這個初始化方法,主要做一些初始化工作,并創建一個新 session。如果使用單例方法創建 downloader,則只會有一個 session,而如果通過其他方法創建,則可能創建 session 之前已經有一個了,這時候就需要先將之前的 cancel 之后再創建新的。
+ (nonnull instancetype)sharedDownloader {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
- (nonnull instancetype)init {
return [self initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
}
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
if ((self = [super init])) {
// 執行下載任務的 operation
_operationClass = [SDWebImageDownloaderOperation class];
// 要求解壓圖片
_shouldDecompressImages = YES;
// 執行順序,先進先出
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
// 設置下載操作的隊列,由于最大并發數是 6,所以此 queue 是 并發隊列,如果是 1,則為串行隊列。
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
_URLOperations = [NSMutableDictionary new];
// 請求頭的字段,可接受的文件類型
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
// 鎖,這里使用了信號量
_operationsLock = dispatch_semaphore_create(1);
_headersLock = dispatch_semaphore_create(1);
// 超時時間
_downloadTimeout = 15.0;
[self createNewSessionWithConfiguration:sessionConfiguration];
}
return self;
}
// 創建新的 session
- (void)createNewSessionWithConfiguration:(NSURLSessionConfiguration *)sessionConfiguration {
// 為避免影響,先取消可能存在的下載任務
[self cancelAllDownloads];
// cancel 之前的 session,然后創建一個新的
if (self.session) {
[self.session invalidateAndCancel];
}
sessionConfiguration.timeoutIntervalForRequest = self.downloadTimeout;
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
然后,我們看一下主要方法 - (nullable SDWebImageDownloadToken *)downloadImageWithURL:url options:options progress:progressBlock completed:completedBlock
。直接調用了添加進度與完成回調的方法,并將返回值作為結果返回。
添加進度與完成回調的方法我們稍后再議,先看一下調用時傳入的 createCallback
。就做了兩件事:先創建一個 request,用于準備一些基礎參數,然后,依據 request 創建 operation,詳見代碼注釋 ?。
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// *** 1.創建 request
// 為避免重復緩存 (NSURLCache + SDImageCache) ,如果沒有明確要求使用 NSURLCache,我們默認忽略本地緩存
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
// The default is YES - in other words, cookies are sent from and stored to the cookie manager by default.
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
// 設置 header,headersFilter 是過濾頭部參數的block
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself allHTTPHeaderFields]);
} else {
request.allHTTPHeaderFields = [sself allHTTPHeaderFields];
}
// *** 2.創建下載的 operation (這個 operationClass ,給他賦什么值,他就是什么,如果不設置,就是默認值:[SDWebImageDownloaderOperation class])
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request
inSession:sself.session
options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
// NSURLCredential 身份認證
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
// NSURLCredentialPersistenceForSession: Credential should be stored only for this session.
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
// 設置優先級
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 更改執行順序:先進后出(可在此設置) or 先進先出(默認)
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// 通過反向設置依賴,指定了隊列中任務的執行順序先加進去的依賴于后加進去的,那就成了后進先出了??
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
現在,我們來看看添加進度與完成回調的方法:
- (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];
// 如果是第 1 次進來,通過 url 是取不出 URLOperation 的,但是第 2 次就有可能找到,也就是想要重復發第 2 次請求的話,就可以取到。
// 第 2 次可以取到(并且已經完成的情況下),則不會走括號里邊,也就不會執行關鍵步驟:[self.downloadQueue addOperation:operation]; ,所以就不會發起請求了,因為將 operation 添加到隊列的時候,系統會自動觸發請求。
if (!operation || operation.isFinished) {
// 創建 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];
// 添加到隊列,即開始執行!
// Add operation to operation queue only after all configuration done according to Apple's doc.
// `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
[self.downloadQueue addOperation:operation];
}
UNLOCK(self.operationsLock);
// 存放進度和完成回調的 數組 array
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
// 與下載任務關聯的一個對象,用于取消操作的時候
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
主要做了這么幾件事:
開始依然是參數校驗
然后從 self.URLOperation 里邊取 operation
第一次進來當然取不到 operation,于是就會進入
if (!operation || operation.isFinished) { ... }
的代碼塊。先執行我們傳入的createCallback()
創建 operation,然后將 operation 加入到 self.URLOperations 里邊,同時設置好 operation 的 completionBlock,到時將 operation 移除,最后將 operation 加入到操作隊列里,就會自動開始執行了。創建一個 token,他是
SDWebImageDownloadToken
的實例,將它與 operation、url、progressBlock 及 completedBlock 關聯起來,用于后邊之后的取消操作。其中 progressBlock 及 completedBlock 是通過 downloadOperationCancelToken 與 token 關聯起來的,這里用到了 operation 中的一個方法:
// SDWebImageDownloaderOperation
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
LOCK(self.callbacksLock);
[self.callbackBlocks addObject:callbacks];
UNLOCK(self.callbacksLock);
return callbacks;
}
由此可知,這個 id downloadOperationCancelToken
是一個存放 progressBlock 和 completedBlock 的 dictionary。
這里 if 語句起到了一個非常重要的作用,即 避免重復下載相同數據,具體原因就不解釋了,上邊的代碼注釋里已經寫了。
到這里是不是覺得少了點什么,是的,SDWebImageDownloaderOperation
和 SDWebImageDownloadToken
的具體實現還不知道呢,接下來我們就分別查看這 2 個類,先從簡單的開始吧!
SDWebImageDownloadToken
這個類只有 3 個屬性,前邊都用到了,屬性聲明如下:
// 下載任務對應的 url
@property (nonatomic, strong, nullable) NSURL *url;
// 實際是包含 progressBlock 和 completionBlock 的字典,是通過 `addHandlersForProgress:completed:` 返回的,用于取消操作
@property (nonatomic, strong, nullable) id downloadOperationCancelToken;
// 操作的 operation,繼承自 NSOperation,不過又遵守了 `SDWebImageDownloaderOperationInterface` 這個協議,擴展了一些方法。
@property (nonatomic, weak, nullable) NSOperation<SDWebImageDownloaderOperationInterface> *downloadOperation;
它只實現了一個協議方法 cancel
(Protocol: SDWebImageOperation),其中 self.downloadOperationCancelToken
就是存放 progressBlock 和 completionBlock 的字典,然后將這個 token 創遞給了 operation 的 cancel:
方法(也是一個協議方法),這個方法的具體實現下邊就會說到。
- (void)cancel {
if (self.downloadOperation) {
SDWebImageDownloadToken *cancelToken = self.downloadOperationCancelToken;
if (cancelToken) {
[self.downloadOperation cancel:cancelToken];
}
}
}
SDWebImageDownloaderOperationInterface
在開始介紹 operation 之前,先看看他遵守的協議 SDWebImageDownloaderOperationInterface
,聲明了以下協議方法。如果想要使用自定義的 operation,則它必須繼承自 NSOperation 并且遵守這個協議。這些方法的實現可以參考 SDWebImageDownloaderOperation
。
// SDWebImageDownloaderOperationInterface
// 初始化方法
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options;
// 保存進度和完成的回調
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
// 是否需要解壓圖片
- (BOOL)shouldDecompressImages;
- (void)setShouldDecompressImages:(BOOL)value;
// 憑證或稱證書信息
- (nullable NSURLCredential *)credential;
- (void)setCredential:(nullable NSURLCredential *)value;
// 取消
- (BOOL)cancel:(nullable id)token;
SDWebImageDownloaderOperation
現在開始討論 SDWebImageDownloaderOperation
這個類,下邊是初始化方法:
- (nonnull instancetype)init {
return [self initWithRequest:nil inSession:nil options:0];
}
// 也是協議方法
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options {
if ((self = [super init])) {
_request = [request copy];
_shouldDecompressImages = YES;
_options = options;
_callbackBlocks = [NSMutableArray new];
_executing = NO;
_finished = NO;
_expectedSize = 0;
_unownedSession = session;
_callbacksLock = dispatch_semaphore_create(1);
_coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
下面介紹 2 個比較重要的方法:
- 核心方法 start
這是重寫父類 NSOperation 的 start 方法,添加了自定義的操作。這個方法不需要手動調用,在將 operation 添加到 operationQueue 中的時候,系統會自動調用其 start 方法。重寫后的操作包括以下幾點:
①檢測操作是否已取消,如果取消了,重置數據后直接返回。
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
②如果需要 App 進入后臺時,繼續執行下載操作,需要開啟后臺任務。并設置 ExpirationHandler,取消下載任務,并結束后臺操作。
// 如果需要App進入后臺時,繼續執行此操作,需要開啟后臺任務。
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)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
③獲取或創建 session,然后創建 dataTask,
④若 dataTask 創建成功,啟動下載任務 [self.dataTask resume];
,然后執行一次 progressBlock,并發送開始下載的通知。
④若 dataTask 創建失敗,直接調用完成回調,構建 error 信息并返回,然后重置數據。
⑤關閉可能存在的后臺下載任務。
具體下載過程中的操作,都在 session 相關的那些協議方法里邊,詳見代碼注釋,這里就不啰嗦了。
- 取消操作
最后看一下取消操作,即協議方法 - (BOOL)cancel:(nullable id)token;
的實現,將取消過程中用到的所有方法都展開就是下邊這樣:
- (BOOL)cancel:(nullable id)token {
BOOL shouldCancel = NO;
LOCK(self.callbacksLock);
// 移除 token,即移除一個存儲著 completionBlock 和 progressBlock 的字典
[self.callbackBlocks removeObjectIdenticalTo:token];
// 如果已經沒有回調,就去執行整體的 cancel 操作
if (self.callbackBlocks.count == 0) {
shouldCancel = YES;
}
UNLOCK(self.callbacksLock);
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.
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
}
// *** 點開 ?
[self reset];
}
// 重置變量
- (void)reset {
LOCK(self.callbacksLock);
[self.callbackBlocks removeAllObjects];
UNLOCK(self.callbacksLock);
self.dataTask = nil;
if (self.ownedSession) {
[self.ownedSession invalidateAndCancel];
self.ownedSession = nil;
}
}
小結
SDWebImageDownloader 的內容就先介紹到這類,其他細節見 HHSDWebImageStudy 中的源碼注釋。