title: SDWebImage之圖片下載
categories:
- 第三方框架
tags: - 三方框架解析
我們經常使用SDWebImage在加載圖片,但對于圖片加載過程中怎么樣實現不會深究。下面我們就對SDWebImage進行相應的分析:
源碼地址
SDWebImage的下載器
SDWebImage的下載器是SDWebImageDownloader利用單例模式sharedDownloader,可以對下載的圖片進行相關配置。可以配置的部分如下:
<ul>
<li>下載選項</li>
<li>HTTP的頭部</li>
<li>壓縮、下載超時、下載順序、最大并發數等</li>
</ul>
下載選項
<pre>
<code>
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
//下載的優先級
SDWebImageDownloaderLowPriority = 1 << 0,
//下載進度
SDWebImageDownloaderProgressiveDownload = 1 << 1,
//下載路徑緩存
SDWebImageDownloaderUseNSURLCache = 1 << 2,
//下載過程的請求緩存
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
//后臺進行繼續下載
SDWebImageDownloaderContinueInBackground = 1 << 4,
SDWebImageDownloaderHandleCookies = 1 << 5,
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
SDWebImageDownloaderHighPriority = 1 << 7,
};
</code>
</pre>完
HTTP的頭部設置
<pre>
<code>
ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
endif
-(void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field {
if (value) {
self.HTTPHeaders[field] = value;
}
else {
[self.HTTPHeaders removeObjectForKey:field];
}
}
-(NSString *)valueForHTTPHeaderField:(NSString *)field {
return self.HTTPHeaders[field];
}</code>
</pre>我們可以通過上述forHTTPHeaderField的參數進行相應HTTPheader的設置,使用者可以對頭部信息進行相關的添加或者是刪除HTTP頭部信息。
線程安全
在圖片下載過程中我們要保線程訪問的安全性,barrierQueue是實現網絡響應的序列化實例。
<pre>
<code>
// This queue is used to serialize the handling of the network responses of all
the download operation in a single queue
@property (SDDispatchQueueSetterSementics, nonatomic)
dispatch_queue_t barrierQueue;
</code>
</pre>在保證線程安全的起見,我們對于URLCallbacks進行增改都需要放在dispatch_barrier_sync的形式放入到barrierQueue。但是如果我們只要進行相關的查詢那就使用dispatch_sync放入barrierQueue中即可。
<pre><code>
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
// Handle single download of simultaneous download request for the same URL
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
if (first) {
createCallback();
}
});</code></pre>完
回調
在我們下載圖片的過程中,每一張圖片都需要開啟一個線程,在每一個線程中都需要對執行一定的回調信息。這些回調的信息會以block的實行出現:
<pre>
<code>
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize,
NSInteger expectedSize);
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data,
NSError *error, BOOL finished);
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url,
NSDictionary *headers);
</code>
</pre>圖片下載的這些回調信息存儲在SDWebImageDownloader類的URLCallbacks屬性中,該屬性是一個字典,key是圖片的URL地址,value則是一個數組,包含每個圖片的多組回調信息。
下載器
整個下載過程中我們需要執行在本小結講述的下載器中進行,下載器對于下載的管理都是放在-(id <SDWebImageOperation>)downloadImageWithURL:中的:
<pre>
<code>
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
NSTimeInterval timeoutInterval = wself.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
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ?
NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData)
timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (wself.headersFilter) {
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
operation = [[wself.operationClass alloc] initWithRequest:request
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];
});
for (NSDictionary *callbacks in callbacksForURL) {
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
});
}
}
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) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
cancelled:^{
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
operation.shouldDecompressImages = wself.shouldDecompressImages;
if (wself.urlCredential) {
operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
return operation;
}
</code>
</pre>我們在上面的方法中調用的方法(void)addProgressCallback:completedBlock:forURL:createCallback:將在訪問圖片請求的信息直接放入下載器。
<pre>
<code>
-
(void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL*)url createCallback:(SDWebImageNoParamsBlock)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;
}
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}// Handle single download of simultaneous download request for the same URL
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;if (first) {
createCallback();
}
});
}
</code>
</pre>完
下載操作
在每張圖片下載過程中都要調用一次具體的操作都會調用Operation,下面就分析一下中間的具體過程。
我們打開SDWebImage的文件夾可以到其中有一個SDWebImageOperation的類,如下:
<pre>
<code>
import <Foundation/Foundation.h>
@protocol SDWebImageOperation <NSObject>
-(void)cancel;
@end
</code>
</pre>其中我們使用NSOpation的子類來完成具體圖片下載的過程,這個類就是SDWebImageDownloaderOperation。在SDWebImageDownloaderOperation類中繼承NSOperation的類而且實現SDWebImageOperation的cancel的取消協議。除了繼承而來的方法,該類只向外暴露了一個方法,即上面所用到的初始化方法initWithRequest:options:pregress:completed:cancelled:。
對于圖片的下載,SDWebImageDownloaderOperation完全依賴于URL加載系統中的NSURLConnection類(并未使用iOS7以后的NSURLSession類)。我們先來分析一下SDWebImageDownloaderOperation類中對于圖片實際數據的下載處理,即NSURLConnection各代理方法的實現。
首先,SDWebImageDownloaderOperation在Extention中采用了NSURLConnectionDataDelegate協議,并實現了協議的以下幾個方法:
<pre>
<code>
connection:didReceiveResponse:
connection:didReceiveData:
connectionDidFinishLoading:
connection:didFailWithError:
connection:willCacheResponse:
connectionShouldUseCredentialStorage:
connection:willSendRequestForAuthenticationChallenge:
</code>
</pre>這些方法我們就不逐一分析了,就終點分析一下connection:didReceiveResponse:和connection:didReceiveData:兩個方法。
connection:didReceiveResponse方法通過判斷NSURLResponse的實際類型和狀態碼,對除304以外400以內的狀態碼反應。
<pre>
<code>
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
//'304 Not Modified' is an exceptional one
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);
}
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];
//This is the case when server returns '304 Not Modified'. It means that
remote image is not changed.
//In case of 304 we need just cancel the operation and return cached image
from the cache.
if (code == 304) {
[self cancelInternal];
} else {
[self.connection 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);
}
CFRunLoopStop(CFRunLoopGetCurrent());
[self done];
}
}
</code>
</pre>connection:didReceiveData:方法的主要任務是接受數據。每次接收到數據時,都會用現有的數據創建一個CGImageSourceRef對象以作處理。在首次獲取到數據時(width+height==0)會從這些包含圖像信息的數據中取出圖像的長、寬、方向等信息以備使用。而后在圖片下載完成之前,會使用CGImageSourceRef對象創建一個圖像對象,經過縮放、解壓縮操作后生成一個UIImage對象供完成回調使用。當然,在這個方法中還需要處理的就是進度信息。如果我們有設置進度回調的話,就調用進度回調以處理當前圖片的下載進度。
<pre>
<code>
-
(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[self.imageData appendData:data];if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
// The following code is from http://www.cocoaintheshell.com/2011/05/
progressive-images-download-imageio/
// Thanks to the author @Nyx0uf
// Get the total bytes downloaded
const NSInteger totalSize = self.imageData.length;
// Update the data source, we must pass ALL the data, not just the new bytes
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);
// 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 connectionDidFinishLoading.) So save it here and pass it on later.
orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
}
}
if (width + height > 0 && totalSize < self.expectedSize) {
// Create the image
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
ifdef TARGET_OS_IPHONE
// Workaround for iOS anamorphic image
if (partialImageRef) {
const size_t partialHeight = CGImageGetHeight(partialImageRef);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
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) {
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);
}
}
</code>
</pre>我們前面說過SDWebImageDownloaderOperation類是繼承自NSOperation類。它沒有簡單的實現main方法,而是采用更加靈活的start方法,以便自己管理下載的狀態。
在start方法中,創建了我們下載所使用的NSURLConnection對象,開啟了圖片的下載,同時拋出一個下載開始的通知。start方法的具體實現如下:
<pre>
<code>
- (void)start {
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[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)];
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
self.executing = YES;
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
self.thread = [NSThread currentThread];
}
[self.connection start];
if (self.connection) {
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
// Make sure to run the runloop in our background thread so it can process downloaded data
// Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
// not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466)
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
}
else {
CFRunLoopRun();
}
if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
}
else {
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
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
}
</code>
</pre>在下載完后或者是下載失敗后都會停止當前調用的runloop,清楚鏈接隨后就拋出下載停止的消息。
如果下載成功,則會處理完整的圖片數據,對其進行適當的縮放與解壓縮操作,以提供給完成回調使用。具體可參考-connectionDidFinishLoading:與-connection:didFailWithError:的實現。
總結
我們在上面的介紹可以看出在下載圖片的過程中,每次下載圖片都會調用NSOperation的函數進行處理,每次數據實際實現是使用NSURLConnection。我們把具體實現的線程放置在隊列中進行執行操作。如果下載成功,則會處理完整的圖片數據,對其進行適當的縮放與解壓縮操作,以提供給完成回調使用。具體可參考-connectionDidFinishLoading:與-connection:didFailWithError:的實現。