title: SDWebImage之圖片下載
categories:
- 第三方框架
tags: - 三方框架解析
我們經(jīng)常使用SDWebImage在加載圖片苟穆,但對于圖片加載過程中怎么樣實(shí)現(xiàn)不會深究。下面我們就對SDWebImage進(jìn)行相應(yīng)的分析:
源碼地址
SDWebImage的下載器
SDWebImage的下載器是SDWebImageDownloader利用單例模式sharedDownloader,可以對下載的圖片進(jìn)行相關(guān)配置往衷。可以配置的部分如下:
<ul>
<li>下載選項(xiàng)</li>
<li>HTTP的頭部</li>
<li>壓縮布隔、下載超時(shí)霎匈、下載順序、最大并發(fā)數(shù)等</li>
</ul>
下載選項(xiàng)
<pre>
<code>
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
//下載的優(yōu)先級
SDWebImageDownloaderLowPriority = 1 << 0,
//下載進(jìn)度
SDWebImageDownloaderProgressiveDownload = 1 << 1,
//下載路徑緩存
SDWebImageDownloaderUseNSURLCache = 1 << 2,
//下載過程的請求緩存
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
//后臺進(jìn)行繼續(xù)下載
SDWebImageDownloaderContinueInBackground = 1 << 4,
SDWebImageDownloaderHandleCookies = 1 << 5,
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
SDWebImageDownloaderHighPriority = 1 << 7,
};
</code>
</pre>完
HTTP的頭部設(shè)置
<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的參數(shù)進(jìn)行相應(yīng)HTTPheader的設(shè)置乱投,使用者可以對頭部信息進(jìn)行相關(guān)的添加或者是刪除HTTP頭部信息咽笼。
線程安全
在圖片下載過程中我們要保線程訪問的安全性,barrierQueue是實(shí)現(xiàn)網(wǎng)絡(luò)響應(yīng)的序列化實(shí)例戚炫。
<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進(jìn)行增改都需要放在dispatch_barrier_sync的形式放入到barrierQueue。但是如果我們只要進(jìn)行相關(guān)的查詢那就使用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>完
回調(diào)
在我們下載圖片的過程中施掏,每一張圖片都需要開啟一個(gè)線程,在每一個(gè)線程中都需要對執(zhí)行一定的回調(diào)信息茅糜。這些回調(diào)的信息會以block的實(shí)行出現(xiàn):
<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>圖片下載的這些回調(diào)信息存儲在SDWebImageDownloader類的URLCallbacks屬性中七芭,該屬性是一個(gè)字典,key是圖片的URL地址蔑赘,value則是一個(gè)數(shù)組狸驳,包含每個(gè)圖片的多組回調(diào)信息。
下載器
整個(gè)下載過程中我們需要執(zhí)行在本小結(jié)講述的下載器中進(jìn)行米死,下載器對于下載的管理都是放在-(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>我們在上面的方法中調(diào)用的方法(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>完
下載操作
在每張圖片下載過程中都要調(diào)用一次具體的操作都會調(diào)用Operation,下面就分析一下中間的具體過程峦筒。
我們打開SDWebImage的文件夾可以到其中有一個(gè)SDWebImageOperation的類究西,如下:
<pre>
<code>
import <Foundation/Foundation.h>
@protocol SDWebImageOperation <NSObject>
-(void)cancel;
@end
</code>
</pre>其中我們使用NSOpation的子類來完成具體圖片下載的過程,這個(gè)類就是SDWebImageDownloaderOperation物喷。在SDWebImageDownloaderOperation類中繼承NSOperation的類而且實(shí)現(xiàn)SDWebImageOperation的cancel的取消協(xié)議卤材。除了繼承而來的方法遮斥,該類只向外暴露了一個(gè)方法,即上面所用到的初始化方法initWithRequest:options:pregress:completed:cancelled:扇丛。
對于圖片的下載术吗,SDWebImageDownloaderOperation完全依賴于URL加載系統(tǒng)中的NSURLConnection類(并未使用iOS7以后的NSURLSession類)。我們先來分析一下SDWebImageDownloaderOperation類中對于圖片實(shí)際數(shù)據(jù)的下載處理帆精,即NSURLConnection各代理方法的實(shí)現(xiàn)较屿。
首先,SDWebImageDownloaderOperation在Extention中采用了NSURLConnectionDataDelegate協(xié)議卓练,并實(shí)現(xiàn)了協(xié)議的以下幾個(gè)方法:
<pre>
<code>
connection:didReceiveResponse:
connection:didReceiveData:
connectionDidFinishLoading:
connection:didFailWithError:
connection:willCacheResponse:
connectionShouldUseCredentialStorage:
connection:willSendRequestForAuthenticationChallenge:
</code>
</pre>這些方法我們就不逐一分析了隘蝎,就終點(diǎn)分析一下connection:didReceiveResponse:和connection:didReceiveData:兩個(gè)方法。
connection:didReceiveResponse方法通過判斷NSURLResponse的實(shí)際類型和狀態(tài)碼襟企,對除304以外400以內(nèi)的狀態(tài)碼反應(yīng)嘱么。
<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:方法的主要任務(wù)是接受數(shù)據(jù)。每次接收到數(shù)據(jù)時(shí)顽悼,都會用現(xiàn)有的數(shù)據(jù)創(chuàng)建一個(gè)CGImageSourceRef對象以作處理曼振。在首次獲取到數(shù)據(jù)時(shí)(width+height==0)會從這些包含圖像信息的數(shù)據(jù)中取出圖像的長、寬蔚龙、方向等信息以備使用冰评。而后在圖片下載完成之前,會使用CGImageSourceRef對象創(chuàng)建一個(gè)圖像對象木羹,經(jīng)過縮放集索、解壓縮操作后生成一個(gè)UIImage對象供完成回調(diào)使用。當(dāng)然汇跨,在這個(gè)方法中還需要處理的就是進(jìn)度信息。如果我們有設(shè)置進(jìn)度回調(diào)的話妆距,就調(diào)用進(jìn)度回調(diào)以處理當(dāng)前圖片的下載進(jìn)度穷遂。
<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類。它沒有簡單的實(shí)現(xiàn)main方法娱据,而是采用更加靈活的start方法蚪黑,以便自己管理下載的狀態(tài)。
在start方法中中剩,創(chuàng)建了我們下載所使用的NSURLConnection對象忌穿,開啟了圖片的下載,同時(shí)拋出一個(gè)下載開始的通知结啼。start方法的具體實(shí)現(xiàn)如下:
<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>在下載完后或者是下載失敗后都會停止當(dāng)前調(diào)用的runloop掠剑,清楚鏈接隨后就拋出下載停止的消息。
如果下載成功郊愧,則會處理完整的圖片數(shù)據(jù)朴译,對其進(jìn)行適當(dāng)?shù)目s放與解壓縮操作井佑,以提供給完成回調(diào)使用。具體可參考-connectionDidFinishLoading:與-connection:didFailWithError:的實(shí)現(xiàn)眠寿。
總結(jié)
我們在上面的介紹可以看出在下載圖片的過程中躬翁,每次下載圖片都會調(diào)用NSOperation的函數(shù)進(jìn)行處理,每次數(shù)據(jù)實(shí)際實(shí)現(xiàn)是使用NSURLConnection盯拱。我們把具體實(shí)現(xiàn)的線程放置在隊(duì)列中進(jìn)行執(zhí)行操作盒发。如果下載成功,則會處理完整的圖片數(shù)據(jù)狡逢,對其進(jìn)行適當(dāng)?shù)目s放與解壓縮操作宁舰,以提供給完成回調(diào)使用。具體可參考-connectionDidFinishLoading:與-connection:didFailWithError:的實(shí)現(xiàn)甚侣。