第四篇的寫在前面
本篇文章為SDWebImage源碼閱讀解析的最后一篇文章,主要介紹SDWebImage的圖片下載功能。主要涉及兩個重要的類——SDWebImageDownloader
和SDWebImageDownloaderOperation
。在第一篇介紹的SDWebImageManager
類中持有SDWebImageDownloader
屬性,通過loadImageWithURL()
方法調用SDWebImageDownloader
中的downloadImageWithURL()
方法對網絡圖片進行下載。
本模塊的設計設計NSURLSession的使用,如果對這個類不熟悉的話,可以參考官方開發者文檔URL Session Programming Guide。
//使用更新的downloaderOptions開啟下載圖片任務
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
//do something...
}];
SDWebImageDownloader的設計
在SDWebImage中,SDWebImageDownloader
被設計為一個單例,與SDImageCache
和SDWebImageManagerd
類似。
用于管理
NSURLRequest
對象請求頭的封裝、緩存、cookie的設置,加載選項的處理等功能。管理Operation之間的依賴關系。SDWebImageDownloaderOperation
是一個自定義的并行Operation子類。這個類主要實現了圖片下載的具體操作、以及圖片下載完成以后的圖片解壓縮、Operation生命周期管理等。
初始化方法和相關變量
SDWebImageDownloader
提供了一個全能初始化方法,在里面對一些屬性和變量做了初始化工作:
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
if ((self = [super init])) {
_operationClass = [SDWebImageDownloaderOperation class];
//默認需要對圖片進行解壓
_shouldDecompressImages = YES;
//默認的任務執行方式為FIFO隊列
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
_downloadQueue = [NSOperationQueue new];
//默認最大并發任務的數目為6個
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
_URLOperations = [NSMutableDictionary new];
//設置默認的HTTP請求頭
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
//設置默認的請求超時
_downloadTimeout = 15.0;
sessionConfiguration.timeoutIntervalForRequest = _downloadTimeout;
/**
*初始化session,delegateQueue設為nil因此session會創建一個串行任務隊列來處理代理方法
*和請求回調。
*/
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
return self;
}
downloadImageWithURL()方法
downloadImageWithURL ()
是下載器的核心方法。
/**
* 通過創建異步下載器實例來根據url下載圖片
*
* 當圖片下載完成后或者有錯誤產生時將通知代理對象
*
*
* @param url The URL to the image to download
* @param options The options to be used for this download
* @param progressBlock 當圖片在下載時progressBlock會被反復調用以通知下載進度,該block在后臺隊列執行
* @param completedBlock 圖片下載完成后執行的回調block
*
* @return A token (SDWebImageDownloadToken) 返回的token可以被用在 -cancel 方法中取消任務
*/
- (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;
//1. 設置超時
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
//2. 關閉NSURLCache,防止重復緩存圖片請求
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSURLRequestCachePolicy cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
//如果options中設置了使用NSURLCache則開啟NSURLCache,默認關閉
if (options & SDWebImageDownloaderUseNSURLCache) {
if (options & SDWebImageDownloaderIgnoreCachedResponse) {
cachePolicy = NSURLRequestReturnCacheDataDontLoad;
} else {
cachePolicy = NSURLRequestUseProtocolCachePolicy;
}
}
//3. 初始化URLRequest
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
//4. 添加請求頭
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = sself.HTTPHeaders;
}
//5. 初始化operation 對象
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
//6. 指定驗證方式
if (sself.urlCredential) {
//SSL驗證
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
//Basic驗證
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
//7. 將當前operation添加到下載隊列
[sself.downloadQueue addOperation:operation];
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
//為operation添加依賴關系
//模擬棧的數據結構 先進后出
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
在downloadImageWithURL
方法中,直接返回了一個名為addProgressCallback
的方法,并將其中的代碼保存到block中傳給這個方法。
addProgressCallback方法
addProgressCallback
方法主要用于設置一些回調并且保存,并且執行downloadImage
中保存的代碼將返回的operation
添加到數組中保存。block保存的數據結構如下圖:
/** 為callbackBlocks添加callbackBlock->callbackBlock中包含:completedBlock,progressBlock
*
* @return SDWebImageDownloadToken
*/
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)())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.
//URL用于給 callbakcs 字典 設置鍵,因此不能為nil
//如果url == nil 直接執行completedBlock回調
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
// 初始化 token 其實就是一個標記
__block SDWebImageDownloadToken *token = nil;
// 在barrierQueue中同步執行
dispatch_barrier_sync(self.barrierQueue, ^{
//1. 根據url獲取operation
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
//2. operation不存在
//執行operationCallback回調的代碼 初始化SDWebImageDownloaderOperation
operation = createCallback();
//3. 保存operation
self.URLOperations[url] = operation;
__weak SDWebImageDownloaderOperation *woperation = operation;
//4. 保存完成的回調代碼
operation.completionBlock = ^{
//下載完成后在字典中移除operation
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
[self.URLOperations removeObjectForKey:url];
};
};
}
//5. 保存將回調保存到operation中的callbackBlocks數組 注意這是屬于operation的對象方法
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
//6. 設置token的屬性
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
return token;
}
SDWebImageDownloaderOperation
Downloader部分的第二個類就是SDWebImageDownloaderOperation
在上面的部分也已經用到過不少。它是NSOperation
的子類,如果對NSOperation
不熟悉的話,可以參考這篇文章。
內部自定義了以下幾個通知:
NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
NSString *const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification";
NSString *const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification";
NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification";
對NSOperation
進行自定義需要對以下幾個方法進行重載:
/**
* 重寫NSOperation的start方法 在里面做處理
*/
- (void)start {
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
#if SD_UIKIT
//1. 進行后臺任務的處理
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;
}
}];
}
#endif
//2. 初始化session
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
//如果Downloader沒有傳入session(self 對 unownedSession弱引用,因為默認該變量為downloader強引用)
//使用defaultSessionConfiguration初始化session
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.
*/
/**
* 初始化自己管理的session
* delegate 設為 self 即需要自動實現代理方法對任務進行管理
* delegateQueue設為nil, 因此session會創建一個串行的任務隊列處理代理方法和回調
*/
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
//使用request初始化dataTask
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
//開始執行網絡請求
[self.dataTask resume];
if (self.dataTask) {
//對callbacks中的每個progressBlock進行調用,并傳入進度參數
//#define NSURLResponseUnknownLength ((long long)-1)
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
//主隊列通知SDWebImageDownloadStartNotification
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
} else {
//連接不成功
//執行回調輸出錯誤信息
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
}
#if SD_UIKIT
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
/**
* 重寫NSOperation的cancel方法
*/
- (void)cancel {
@synchronized (self) {
[self cancelInternal];
}
}
/**
* 內部方法cancel
*/
- (void)cancelInternal {
if (self.isFinished) return;
[super cancel];
if (self.dataTask) {
[self.dataTask cancel];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
// As we cancelled the connection, 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];
}
/**
* 重設operation
*/
- (void)reset {
dispatch_barrier_async(self.barrierQueue, ^{
[self.callbackBlocks removeAllObjects];
});
self.dataTask = nil;
self.imageData = nil;
if (self.ownedSession) {
[self.ownedSession invalidateAndCancel];
self.ownedSession = nil;
}
}
重點看start
方法。NSOperation中
需要執行的代碼需要寫在start
方法中。
讓一個
NSOperation
操作開始,你可以直接調用-start
,或者將它添加到NSOperationQueue
中,添加之后,它會在隊列排到它以后自動執行。
類的內部定義了兩個屬性作為任務的標記:
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
需要注意到的是,如果我們在聲明屬性的時候使用了getter =
的語義,則需要自己手動寫getter
,編譯器不會幫我們合成。源碼中手動聲明了getter
方法,并實現KVO。
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}
在初始化NSURLSession
的時候,SDWebImageDownloaderOperation
將自己聲明為delegate
:
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
因此,需要實現NSURLSessionDelegate
代理方法。具體的實現這里不贅述,主要是針對各種情況進行處理。但是需要注意到的是,在SDWebImageDownloader
中同樣遵守了NSURLSessionDelegate
代理的方法,但是在downloader
中只是簡單的把operation
數組中與task
對應的operation
取出,然后把對應的參數傳入到SDWebImageDownloaderOperation
中的對應的方法進行處理。例如:
// 接收到服務器的響應
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];
//將參數傳入dataOperation中進行處理
[dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}
/**
* 根據task取出operation
*/
- (SDWebImageDownloaderOperation *)operationWithTask:(NSURLSessionTask *)task {
SDWebImageDownloaderOperation *returnOperation = nil;
for (SDWebImageDownloaderOperation *operation in self.downloadQueue.operations) {
if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
returnOperation = operation;
break;
}
}
return returnOperation;
}
結尾
對于SDWebImage的主要功能在這四篇解析文章中大致的分析,除此以外的功能可能還需要對源碼進行更深入的閱讀分析才能有更深的了解。由于筆者水平有限,未免出現有分析不準確或者有不到位的地方,請見諒。
源碼閱讀是一個需要耐心的過程,盡管在途中會遇到一些困難,但是只要多查資料多思考,就會有收獲。
參考文獻:
- SDWebImage源碼閱讀
- iOS中使用像素位圖(CGImageRef)對圖片進行處理
- 一張圖片引發的深思
- SDWebImage源碼解讀_之SDWebImageDecoder
- Difference between [UIImage imageNamed…] and [UIImage imageWithData…]?
- How-is-SDWebImage-better-than-X?
- Understanding SDWebImage - Decompression
- why decode image after [UIImage initwithData:] ?
- Image Resizing Techniques
- 多線程之NSOperation簡介
- SDWebImage源碼閱讀筆記
- URL Session Programming Guide
- SDWebImage源碼解析
- Quartz 2D Programming Guide
- Avoiding Image Decompression Sickness
- NSOperation