SDWebImage (v4.4.1)-SDWebImageDownloader
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
// 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);
NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
// There is a case that the operation may be marked as finished, but not been removed from `self.URLOperations`.
if (!operation || operation.isFinished) {
//創(chuàng)建下載隊列
operation = [self createDownloaderOperationWithUrl:url options:options];
__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);
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
下載圖片的代碼如上。
這里的會對url先進行判空,如果是空的,就直接返回。
然后會創(chuàng)建一個
//下載url作為key value是具體的下載operation 用字典來存儲,方便cancel等操作
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, NSOperation<SDWebImageDownloaderOperationInterface> *> *URLOperations;
這里我們會去創(chuàng)建一個新的方法;
- (NSOperation<SDWebImageDownloaderOperationInterface> *)createDownloaderOperationWithUrl:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options {
NSTimeInterval timeoutInterval = self.downloadTimeout;
//超時時間
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
//為了防止?jié)撛诘闹貜途彺?NSURLCache + SDImageCache),如果被告知,我們會禁用圖像請求的緩存。
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
//創(chuàng)建request 設(shè)置請求緩存策略 下載時間
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
//HTTPShouldUsePipelining設(shè)置為YES, 則允許不必等到response, 就可以再次請求. 這個會很大的提高網(wǎng)絡(luò)請求的效率,但是也可能會出問題
//因為客戶端無法正確的匹配請求與響應, 所以這依賴于服務器必須保證,響應的順序與客戶端請求的順序一致.如果服務器不能保證這一點, 那可能導致響應和請求混亂.
request.HTTPShouldUsePipelining = YES;
if (self.headersFilter) {
request.allHTTPHeaderFields = self.headersFilter(url, [self allHTTPHeaderFields]);
}
else {
request.allHTTPHeaderFields = [self allHTTPHeaderFields];
}
NSOperation<SDWebImageDownloaderOperationInterface> *operation = [[self.operationClass alloc] initWithRequest:request inSession:self.session options:options];
operation.shouldDecompressImages = self.shouldDecompressImages;
//身份認證 當移動端和服務器在傳輸過程中,服務端有可能在返回Response時附帶認證,詢問 HTTP 請求的發(fā)起方是誰,這時候發(fā)起方應提供正確的用戶名和密碼(即認證信息)。這時候就需要NSURLCredential身份認證
if (self.urlCredential) {
operation.credential = self.urlCredential;
} else if (self.username && self.password) {
operation.credential = [NSURLCredential credentialWithUser:self.username password:self.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
//設(shè)置下載的順序 是按照隊列還是棧
if (self.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
//通過依賴來模擬LIFO
[self.lastAddedOperation addDependency:operation];
self.lastAddedOperation = operation;
}
return operation;
}
在這里,通過調(diào)用
NSOperation<SDWebImageDownloaderOperationInterface> *operation = [[self.operationClass alloc] initWithRequest:request inSession:self.session options:options];
SDWebImageDownloaderOperation
來創(chuàng)建一個新的NSOperation。因為是NSOperation,所以它會直接調(diào)用start方法。所以接下來,會在SDWebImageDownloaderOperation類中,通過重寫start方法來處理下載和緩存的關(guān)系。
在這里方法里,核心是圍繞
@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;
這個方法來進行一套回調(diào),在獲取到網(wǎng)絡(luò)回調(diào)的時候,會先遍歷數(shù)組,然后會根據(jù)url來作為key,獲取這里所有key對應的回調(diào)。
這里為了保證不出線程沖突,使用了dispatch_semaphore_wait這個lock。
- 這里使用這個有意思的,有dictionary屬性array的原因,是因為array是有序的??梢宰兿嗟氖惯@個兼具array和dictionary的特性。利用dictionary的hash能力,保證同一個url只會下載一次。
在NSOperation中,有三個狀態(tài)來表示任務狀態(tài)
- isExecuting - 代表任務正在執(zhí)行中
- isFinished - 代表任務已經(jīng)完成
- isCancelled - 代表任務已經(jīng)取消執(zhí)行
并使用這兩個BOOL,來輔助。
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
在start方法中,假如我們配置的允許后臺下載,我們可以繼續(xù)在后臺下載圖片
//如調(diào)用者配置了在后臺可以繼續(xù)下載圖片,那么在這里繼續(xù)下載
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;
}
}];
}
然后,正式的請求數(shù)據(jù)
NSURLSession *session = self.unownedSession;
//判斷unownedSession是否為了nil,如果是nil則重新創(chuàng)建一個ownedSession
if (!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.
//delegateQueue為nil,所以回調(diào)方法默認在一個子線程的串行隊列中執(zhí)行
*/
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
self.ownedSession = session;
}
//獲取網(wǎng)絡(luò)請求的緩存數(shù)據(jù)
if (self.options & SDWebImageDownloaderIgnoreCachedResponse)
{
// Grab the cached data for later check
//獲取緩存的數(shù)據(jù)以供以后檢查。
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;
}
}
//使用session來創(chuàng)建一個NSURLSessionDataTask類型下載任務
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
注意,這里的unownedSession是使用weak來修飾的,其實是因為它是從上一層傳過來的值的賦值。我們并不需要關(guān)心它的生命周期。
而在cancel方法里
- (void)cancel {
@synchronized (self) {
[self cancelInternal];
}
}
- (void)cancelInternal {
if (self.isFinished) return;
[super cancel];
//如果下載圖片的任務仍在 則立即取消cancel,并且發(fā)送結(jié)束下載的通知
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];
}
......
//重新設(shè)置數(shù)據(jù)
- (void)reset {
LOCK(self.callbacksLock);
//刪除回調(diào)塊字典數(shù)組的所有元素
[self.callbackBlocks removeAllObjects];
UNLOCK(self.callbacksLock);
self.dataTask = nil;
//如果ownedSession存在,則手動調(diào)用invalidateAndCancel進行任務
if (self.ownedSession) {
[self.ownedSession invalidateAndCancel];
self.ownedSession = nil;
}
}
我們可以發(fā)現(xiàn),在cancel方法里,會在保證線程安全的情況下直接調(diào)用父類的cancel方法。
但是如果這個下載的任務還在的情況下,我們需要連帶著把這個下載任務也取消掉。
而不管下載任務在不在,都要再去判斷ownedSession是否存在,還有的話,就去調(diào)用 [self.ownedSession invalidateAndCancel];和self.ownedSession = nil;這兩個方法來取消NSURLSession。
然后接下來,就是圖像處理的一部分
NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
if (!self.imageData)
{
//根據(jù)response返回的文件大小創(chuàng)建可變data
self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
}
//向可變數(shù)據(jù)中添加接收到的數(shù)據(jù)
[self.imageData appendData:data];
//如果調(diào)用者配置了需要支持progressive下載,即展示已經(jīng)下載的部分,并expectedSize返回的圖片size大于0
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
// Get the image data
__block NSData *imageData = [self.imageData copy];
// Get the total bytes downloaded
const NSInteger totalSize = imageData.length;
// Get the finish status
//判斷是否已經(jīng)下載完成
BOOL finished = (totalSize >= self.expectedSize);
//如果不存在解壓對象就去創(chuàng)建一個新的
if (!self.progressiveCoder) {
// We need to create a new instance for progressive decoding to avoid conflicts
for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) {
if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
[((id<SDWebImageProgressiveCoder>)coder) canIncrementallyDecodeFromData:imageData]) {
self.progressiveCoder = [[[coder class] alloc] init];
break;
}
}
}
// progressive decode the image in coder queue
dispatch_async(self.coderQueue, ^{
//將imageData轉(zhuǎn)化為image
@autoreleasepool {
UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
if (image) {
//通過URL獲取緩存的key
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
//如果調(diào)用者選擇了解壓圖片,那么在這里執(zhí)行圖片解壓,這里注意,傳入的data是一個**,指向指針的指針,要用&data表示
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
}
// We do not keep the progressive decoding image even when `finished`=YES. Because they are for view rendering but not take full function from downloader options. And some coders implementation may not keep consistent between progressive decoding and normal decoding.
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
}
}
});
}
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
}
}
這里圖像的核心方法是如此
- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished {
if (!_imageSource) {
_imageSource = CGImageSourceCreateIncremental(NULL);
}
UIImage *image;
// The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
// Thanks to the author @Nyx0uf
// Update the data source, we must pass ALL the data, not just the new bytes
CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
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);
// When we draw to Core Graphics, we lose orientation information,
// which means the image below born of initWithCGIImage will be
// oriented incorrectly sometimes. (Unlike the image born of initWithData
// in didCompleteWithError.) So save it here and pass it on later.
#if SD_UIKIT || SD_WATCH
_orientation = [SDWebImageCoderHelper imageOrientationFromEXIFOrientation:orientationValue];
#endif
}
}
if (_width + _height > 0) {
// Create the image
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
if (partialImageRef) {
#if SD_UIKIT || SD_WATCH
image = [[UIImage alloc] initWithCGImage:partialImageRef scale:1 orientation:_orientation];
#elif SD_MAC
image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
#endif
CGImageRelease(partialImageRef);
image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
}
}
if (finished) {
if (_imageSource) {
CFRelease(_imageSource);
_imageSource = NULL;
}
}
return image;
}
直接通過渲染繪制出UIImage。