概述
本篇分析一下SDWebImage中負責下載圖片數據的相關代碼,SDWebImageDownloader和SDWebImageDownloaderOperation。
SDWebImageDownloader
SDWebImageDownloader是管理下載圖片數據的類,初始化方法代碼注釋如下:
- (id)init {
if ((self = [super init])) {
_operationClass = [SDWebImageDownloaderOperation class];
_shouldDecompressImages = YES; //是否解壓圖片
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
_downloadQueue = [NSOperationQueue new]; //下載的隊列
_downloadQueue.maxConcurrentOperationCount = 6;
_URLCallbacks = [NSMutableDictionary new]; //請求回調dic
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy]; //請求webp圖片數據,報文頭部accept多包含image/webp字段
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT); //
_downloadTimeout = 15.0; //請求圖片數據超時超時時間
//初始化NSURLSessionConfiguration對象
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = _downloadTimeout; //設置超時時間
self.session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil]; //創建session
}
return self;
}
該方法初始化了一些參數用于網絡請求,SDWebImageDownloader是通過NSURLSession的方式請求數據。同時提供了一些方法用于外部設置。代碼注釋如下:
//設置請求報文頭部
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field {
if (value) {
self.HTTPHeaders[field] = value;
} else {
[self.HTTPHeaders removeSafeObjectForKey:field];
}
}
//獲取設置的報文頭部
- (NSString *)valueForHTTPHeaderField:(NSString *)field {
return self.HTTPHeaders[field];
}
//設置下載隊列的最大并發個數
- (void)setMaxConcurrentDownloads:(NSInteger)maxConcurrentDownloads {
_downloadQueue.maxConcurrentOperationCount = maxConcurrentDownloads;
}
//獲取下載隊列的當前operation個數
- (NSUInteger)currentDownloadCount {
return _downloadQueue.operationCount;
}
//獲取下載隊列的最大并發個數
- (NSInteger)maxConcurrentDownloads {
return _downloadQueue.maxConcurrentOperationCount;
}
SDWebImageDownloader最主要的方法是downloadImageWithURL:options:progress:completed:方法,調用該方法開始下載數據。下面主要分析一下具體執行步驟:
-
首先調用addProgressCallback:completedBlock:forURL:createCallback:方法為當前網絡請求關聯回調block,包括請求進度的回調和請求結束的回調,主要代碼如下:
dispatch_barrier_sync(self.barrierQueue, ^{ BOOL first = NO; if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; //url對應block數組 first = YES; } NSMutableArray *callbacksForURL = self.URLCallbacks[url]; NSMutableDictionary *callbacks = [NSMutableDictionary new]; //設置當前請求進度progress回調block和請求完成回調block if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; [callbacksForURL addObject:callbacks]; //添加callbacks self.URLCallbacks[url] = callbacksForURL; if (first) { createCallback(); //執行createCallback } });
該方法允許設置一組回調block,key是請求的url,數組中每個元素又是一個dictionary,包含請求進度progress回調block和請求完成回調block,最后執行createCallback。
-
downloadImageWithURL方法接著在createCallback中構建請求報文,代碼如下:
//創建request對象 NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; //設置參數 request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); request.HTTPShouldUsePipelining = YES;
首先創建request對象,根據option值來決定本次網絡請求的緩存策略,option是SDWebImageDownloaderOptions類型的枚舉值,通過|的方式多選。SDWebImage默認忽略NSURLCache緩存機制,即request的cachePolicy是NSURLRequestReloadIgnoringLocalCacheData,而是用自定義的SDImageCache來緩存數據,上一篇文章分析了SDImageCache,如果option設置了SDWebImageDownloaderUseNSURLCache,則啟用NSURLCache緩存機制,并且設置cachePolicy為NSURLRequestUseProtocolCachePolicy,即通過服務端響應報文的緩存策略來決定本地NSURLCache是否緩存數據。另外還支持pipelining和處理cookies。
-
downloadImageWithURL方法接著創建一個SDWebImageDownloaderOperation類型的NSOperation對象,將本次網絡請求作為一個operation執行。代碼注釋如下:
//創建一個operation對象 operation = [[wself.operationClass alloc] initWithRequest:request inSession:self.session options:options progress:^(NSInteger receivedSize, NSInteger expectedSize) { SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; dispatch_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; //取出callback數組 }); for (NSDictionary *callbacks in callbacksForURL) { //遍歷callback數組 dispatch_async(dispatch_get_main_queue(), ^{ SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; //取出progress的callback if (callback) callback(receivedSize, expectedSize); //執行block }); } } completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; dispatch_barrier_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; if (finished) { [sself.URLCallbacks removeObjectForKey:url]; //刪除回調 } }); for (NSDictionary *callbacks in callbacksForURL) { //遍歷callback數組 SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; if (callback) callback(image, data, error, finished); //執行block } } cancelled:^{ SDWebImageDownloader *sself = wself; if (!sself) return; dispatch_barrier_async(sself.barrierQueue, ^{ [sself.URLCallbacks removeObjectForKey:url]; //刪除url }); }]; operation.shouldDecompressImages = wself.shouldDecompressImages; //圖片數據下載完成后是否要解壓
請求圖片數據過程中觸發progress回調,receivedSize是當前接收數據的大小,expectedSize是數據總大小,請求完成觸發completed回調,刪除url對應的相關callback,并且執行SDWebImageDownloaderCompletedBlock。當operation被cancel,觸發cancelled回調,刪除url對應的相關callback。
最后將operation加入downloadQueue隊列中,開始執行當前網絡請求任務。同時設置了operation的優先級和依賴關系。
由于SDWebImageDownloader將self設置為urlsession的delegate,當發起網絡請求時,觸發urlsession的回調方法。SDWebImageDownloader實現了urlsession的相關代理方法,但是邏輯交給當前dataTask對應的SDWebImageDownloaderOperation對象來處理,通過operationWithTask找到dataTask對應的operation。
- (SDWebImageDownloaderOperation *)operationWithTask:(NSURLSessionTask *)task {
SDWebImageDownloaderOperation *returnOperation = nil;
for (SDWebImageDownloaderOperation *operation in self.downloadQueue.operations) {
//遍歷downloadQueue中當前所有operation,匹配task
if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
returnOperation = operation;
break;
}
}
return returnOperation; //返回
}
SDWebImageDownloaderOperation
SDWebImageDownloaderOperation是繼承NSOperation的類,對應一次網絡請求任務,首先通過初始化方法初始化一些參數設置,參數值有SDWebImageDownloader傳入。
start方法
當operation加入downloadQueue中,觸發start方法,相關代碼注釋如下:
- (void)start {
@synchronized (self) { //加鎖,多線程數據同步
if (self.isCancelled) { //如果operation被取消
self.finished = YES; //則operation結束
[self reset];
return;
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
//如果當前operation標記為在app進入后臺時繼續下載圖片數據
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
//app進入后允許執行一段時間,超過期限執行ExpirationHandler
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) { //如果沒有在初始化方法設置unownedSession,則創建一個urlsession對象
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
session = self.ownedSession;
}
//通過session創建一個dataTask
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES; //當前operation標記為執行狀態
self.thread = [NSThread currentThread]; //獲取當前的線程
}
[self.dataTask resume]; //開始執行dataTask
if (self.dataTask) {
if (self.progressBlock) { //當前進度為0
self.progressBlock(0, NSURLResponseUnknownLength);
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
}
else {
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
...
}
該方法主要開啟一個NSURLSessionTask,并且調用resume方法執行task,開始進行網絡請求。
控制operation生命周期
提供了setFinished:方法,setExecuting:方法控制operation的狀態,實現是修改operation的狀態值,然后手動拋通知給外部。同時提供了cancel和cancelInternal方法取消當前operation。代碼注釋如下:
- (void)cancelInternal {
if (self.isFinished) return;
[super cancel]; //取消operation
if (self.cancelBlock) self.cancelBlock();
//取消當前網絡請求
if (self.dataTask) {
[self.dataTask cancel];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
//operation狀態為結束
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
}
[self reset]; //相關屬性置為nil
}
NSURLSessionDataDelegate
網絡請求執行的過程中,觸發NSURLSession的相關代理方法,SDWebImageDownloader是delegate,實現了相關代理方法,會調用operation來執行。operation實現了以下4個方法:
-
-(void)URLSession:dataTask:didReceiveResponse:completionHandler:方法
當請求建立連接時,服務器發送響應給客戶端,觸發該方法,代碼注釋如下:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { //根據服務端響應的statusCode執行不同邏輯 if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) { //連接成功 NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0; self.expectedSize = expected; if (self.progressBlock) { self.progressBlock(0, expected); } //創建imageData接收數據 self.imageData = [[NSMutableData alloc] initWithCapacity:expected]; self.response = response; dispatch_async(dispatch_get_main_queue(), ^{ //拋通知 [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self]; }); } else { NSUInteger code = [((NSHTTPURLResponse *)response) statusCode]; if (code == 304) { [self cancelInternal]; //304表示服務端圖片沒有更新,結束operation,用緩存中的圖片數據 } else { [self.dataTask cancel];//請求發生錯誤,取消本次請求, } dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; }); if (self.completedBlock) { self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES); } [self done]; //結束operation } if (completionHandler) { completionHandler(NSURLSessionResponseAllow); } }
該方法發生在客戶端收到服務端的響應,但是還沒開始傳輸數據的時候,如果相應報文的statusCode小于400且不是304,則說明連接正常,則創建imageData結束數據。如果statusCode是304,說明網絡請求開啟緩存功能,且客戶端的請求報文中帶有上次緩存到本地的lastModified和ETag信息,服務端在對比本地資源和報文中的字段,發現資源沒有修改后,返回304,且不返回響應的報文body數據。具體可以參考這篇文章。這種情況下不創建imageData接收數據,直接取消dataTask,結束operation,即使用緩存中的圖片數據。如果statusCode不是304,則說明請求失敗,取消dataTask,最后在done方法中結束operation。
-
-(void)URLSession: dataTask: didReceiveData:方法
當開始下載數據時,觸發該方法,如果數據比較多,會不斷觸發,代碼注釋如下:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { [self.imageData appendData:data]; //1.拼接數據 if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) { //當前已經下載的數據大小 const NSInteger totalSize = self.imageData.length; CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL); if (width + height == 0) { CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); if (properties) { NSInteger orientationValue = -1; CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight); //圖像高度 if (val) CFNumberGetValue(val, kCFNumberLongType, &height); val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth); //圖像寬度 if (val) CFNumberGetValue(val, kCFNumberLongType, &width); val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); //圖像方向 if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue); CFRelease(properties); orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)]; //圖片方向 } } //圖像寬度和高度不為0,還沒接受完數據 if (width + height > 0 && totalSize < self.expectedSize) { CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); #ifdef TARGET_OS_IPHONE if (partialImageRef) { const size_t partialHeight = CGImageGetHeight(partialImageRef); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); //創建context CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); CGColorSpaceRelease(colorSpace); if (bmContext) { //畫位圖 CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef); CGImageRelease(partialImageRef); //取出位圖數據 partialImageRef = CGBitmapContextCreateImage(bmContext); CGContextRelease(bmContext); } else { CGImageRelease(partialImageRef); partialImageRef = nil; } } #endif if (partialImageRef) { //創建image對象 UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation]; NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; //縮放圖片 UIImage *scaledImage = [self scaledImageForKey:key image:image]; //需要解碼 if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:scaledImage]; } else { image = scaledImage; } CGImageRelease(partialImageRef); dispatch_main_sync_safe(^{ if (self.completedBlock) { self.completedBlock(image, nil, nil, NO); } }); } } CFRelease(imageSource); } if (self.progressBlock) { self.progressBlock(self.imageData.length, self.expectedSize); } }
首先將拼接數據,如果option包含SDWebImageDownloaderProgressiveDownload,即支持邊下載邊展示圖片,首先獲取圖片的寬、高、方向等信息,然后創建上下文對象bmContext,并調用CGContextDrawImage方法繪制圖像,最后生成UIImage對象,通過completedBlock返回。
-
-(void)URLSession: dataTask: willCacheResponse: completionHandler:方法
當服務器緩存數據到本地時,觸發該方法,代碼注釋如下:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler { responseFromCached = NO; //不是從緩存中取數據 NSCachedURLResponse *cachedResponse = proposedResponse; if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) { cachedResponse = nil; //不緩存數據 } if (completionHandler) { completionHandler(cachedResponse); } }
首先將responseFromCached置為NO,說明本次數據不是200 from cache,而是從服務器下載的,然后判斷cachePolicy如果是NSURLRequestReloadIgnoringLocalCacheData,說明采用的緩存策略是忽略本地NSURLCache緩存,這個時候不將數據存入本地。上文分析到SDWebImage默認忽略NSURLCache緩存,即request的cachePolicy是NSURLRequestReloadIgnoringLocalCacheData,所以該方法不將cachedResponse存入本地。(勘誤:經過實驗,無論是200 from cache還是200,都會觸發NSURLSession的該方法,而NSURLConnection不會觸發)。
-
-(void)URLSession: task: didCompleteWithError:方法
當數據下載完成時,觸發該方法,代碼注釋如下:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { @synchronized(self) { self.thread = nil; self.dataTask = nil; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; if (!error) { [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self]; } }); } if (error) { if (self.completedBlock) { self.completedBlock(nil, nil, error, YES); } } else { SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock; if (completionBlock) { if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) { //如果緩存中存在數據,且沒有更新緩存,則返回nil,即圖片數據不更新 completionBlock(nil, nil, nil, YES); } else if (self.imageData) { //存在數據,處理數據 UIImage *image = [UIImage sd_imageWithData:self.imageData]; NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; image = [self scaledImageForKey:key image:image]; // GIF圖不解壓 if (!image.images) { if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:image];//解壓圖片 } } if (CGSizeEqualToSize(image.size, CGSizeZero)) { completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES); } else { completionBlock(image, self.imageData, nil, YES); //返回圖片 } } else { completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES); } } } self.completionBlock = nil; [self done]; //完成本次operation }
該方法首先判斷緩存策略,如果設置了option是SDWebImageRefreshCached(參考SDWebImageManager中的代碼),則downloadOption是SDWebImageDownloaderIgnoreCachedResponse和SDWebImageDownloaderUseNSURLCache,說明使用NSURLCache的緩存機制來決定圖片的更新,通過服務器的cache-control字段來控制,具體情況分析如下:
一、第一次下載圖片:服務器cache-control指定緩存時間,會將response存入NSURLCache,同時會進入else if(self.imageData)中,將數據轉成圖片,并調用completionBlock()回調,將圖片顯示出來,并存入SDWebImageCache中。
二、第二次下載相同url的圖片,如果本地緩存未過期,即NSURLCache中存在緩存數據,responseFromCached=YES,同時由于設置了SDWebImageDownloaderIgnoreCachedResponse選項,則completionBlock回調nil給外層。如果本地緩存過期則從服務端重新下載數據,responseFromCached=NO,進入else if(self.imageData)中,和(1)一樣處理。
這種機制可以通過實現相同url圖片的更新,而不是SDWebImage默認的機制,一個url對應一張圖片,如果下載到本地(SDWebImageCache中存儲),則從緩存中取,不再下載。
但是讓我疑惑的是SD實現NSURLSession的-(void)URLSession: dataTask: willCacheResponse: completionHandler:方法,每次都會觸發,無論數據是否是從服務器下載還是緩存中取,導致responseFromCached=NO,每次都會進入else if (self.imageData)這個邏輯分支。之前老的SD版本基于NSURLConnection,如果是200 from cache,不會觸發類似的willCacheResponse方法。這里不太清楚作者的用意。
-
-(void)URLSession: task:didReceiveChallenge: completionHandler:方法
當發送HTTPS的url時,觸發該方法用于校驗服務端下發的證書。具體不進行分析。
?
?