在前面的SDWebImage源码解析(一)和SDWebImage源码解析(二)中,解析了开源异步图片下载库SDWebImage的缓存部分。接下来本篇文章将对SDWebImage的下载器部分进行解析。
SDWebImage的异步下载器SDWebImageDownload利用它的单例对象sharedDownloader,可以很好的对图片的下载过程进行配置。sharedDownloader可以配置的部分:
- 下载选项
- HTTP头部
- 压缩、下载顺序、最大并发数、下载超时等
下载选项
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
SDWebImageDownloaderLowPriority = 1 << 0,
SDWebImageDownloaderProgressiveDownload = 1 << 1,
/**
* 默认,request不使用NSURLCache。如果设置该选项,则以默认的缓存策略来使用NSURLCache
*/
SDWebImageDownloaderUseNSURLCache = 1 << 2,
/**
* 如果从NSURLCache缓存中读取图片,则使用nil传递给imageData参数来调用完成block
*/
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
/**
* 在iOS 4+系统中,允许程序进入后台继续下载图片。该操作通过向系统申请额外的时间来完成后台下载。如果后台任务超时,则此操作会被取消。
*/
SDWebImageDownloaderContinueInBackground = 1 << 4,
/**
* 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES来处理存储在NSHTTPCookieStore中的cookie
*/
SDWebImageDownloaderHandleCookies = 1 << 5,
/**
* 设置允许非信任的SSL证书。主要用于测试目的
*/
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
/**
* 将图像下载放到高优先级队列中
*/
SDWebImageDownloaderHighPriority = 1 << 7,
};
可以看出,这些选项主要涉及到下载的优先级、缓存、后台任务执行、cookie处理以及认证几个方面。
HTTP头部
@property (strong, nonatomic) NSMutableDictionary *HTTPHeaders;
#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];
}
通过设置request.allHTTPHeaderFields来指定HTTP头部,HTTP头部中包含可接受图片类型信息,使用者也可以自己添加或者删除HTTP头部信息。
线程安全
使用自定义的并行调度队列barrierQueue处理所有下载操作的网络响应序列化任务。
// 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;
为了保证线程安全,所有增改回调集合URLCallbacks的操作使用dispatch_barrier_sync放入队列barrierQueue中,而查询URLCallbakcs的操作只需使用dispatch_sync放入队列barrierQueue中。
//查询URLCallbacks
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
//增改URLCallbacks
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
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();
}
});
回调
每一个图片的下载都会对应一些回调操作,如下载进度回调,下载完成回调等,这些回调操作是以block形式来呈现的,如下所示:
//下载进度
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
//下载完成
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
//headers过滤
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);
图片下载的这些回调信息存储在SDWebImageDownloader类的URLCallbacks属性中,该属性是一个字典,key是图片的URL地址,value则是一个数组,包含每个图片的多组回调信息。
下载器
整个下载器对于下载请求的管理都是放在downloadImageWithURL:options:progress:completed:方法里面来处理的。
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
// block中要修改的变量需要__block修饰
__block SDWebImageDownloaderOperation *operation;
// 防止retain cycle
__weak __typeof(self)wself = self;
// 调用另一方法将,并在创建回调的block中创建新的操作,配置之后将其放入downloadQueue操作队列中。
[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 创建请求对象,并根据options参数设置其属性
// 为了避免潜在的重复缓存(NSURLCache+SDImageCache),如果没有明确告知,则禁用图片请求的缓存操作。
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;
}
// 创建SDWebImageDownloaderOperation操作对象,并进行配置
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);
}
}
// 取消操作将该URL对应的回调信息从URLCallbacks中删除
cancelled:^{
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
operation.shouldDecompressImages = wself.shouldDecompressImages;
// operation的认证配置
if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
// operation的优先级配置
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 将操作加入到操作队列downloadQueue中
[wself.downloadQueue addOperation:operation];
// 如果是LIFO顺序,则将新的操作作为原队列中的最后一个操作的依赖,然后将新操作设置为最后一个操作。
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
return operation;
}
上面的方法调用了addProgressCallback:andCompletedBlock:forURL:createCallback:方法来将请求的信息存入下载器
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
// URL作为调用字典URLCallbacks的键值,不能为空。如若为空的话,会立即调用完成block,没有图像或者数据。
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
// 以dispatch_barrier_synv操作为保证同一时间只有一个线程能对URLCallbacks进行操作。
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
// 处理同一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();
}
});
}
下载操作
每个图片的下载都是一个Operation操作。我们在上面分析过这个操作的创建及加入操作队列的过程。现在我们来看看单个操作的具体实现。
SDWebImage定义了一个协议,即SDWebImageOperation作为图片下载操作的基础协议。它只声明了一个cancel方法,用于取消操作。协议的具体声明如下:
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
SDWebImage自定义了一个Operation类,即SDWebImageDownloaderOperation,它继承自NSOperation,并采用了SDWebImageOperation协议。除了继承而来的方法,该类只向外暴露了一个方法,即上面所用到的初始化方法initWithRequest:options:pregress:completed:cancelled:。
对于图片的下载,SDWebImageDownloaderOperation完全依赖于URL加载系统中的NSURLConnection类(并未使用iOS7以后的NSURLSession类)。我们先来分析一下SDWebImageDownloaderOperation类中对于图片实际数据的下载处理,即NSURLConnection各代理方法的实现。
首先,SDWebImageDownloaderOperation在Extention中采用了NSURLConnectionDataDelegate协议,并实现了协议的以下几个方法
connection:didReceiveResponse:
connection:didReceiveData:
connectionDidFinishLoading:
connection:didFailWithError:
connection:willCacheResponse:
connectionShouldUseCredentialStorage:
connection:willSendRequestForAuthenticationChallenge:
这些方法我们就不逐一分析了,就终点分析一下connection:didReceiveResponse:和connection:didReceiveData:两个方法。
connection:didReceiveResponse方法通过判断NSURLResponse的实际类型和状态码,对除304以外400以内的状态码反应。
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
//'304 Not Modified' 另外考虑
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];
// 当服务器返回'304 Not Modified'的时候,意味着remote的图像没有改变。
// 304的情况下我们仅需要取消operation并返回缓存中的图像
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];
}
}
connection:didReceiveData:方法的主要任务是接受数据。每次接收到数据时,都会用现有的数据创建一个CGImageSourceRef对象以作处理。在首次获取到数据时(width+height==0)会从这些包含图像信息的数据中取出图像的长、宽、方向等信息以备使用。而后在图片下载完成之前,会使用CGImageSourceRef对象创建一个图像对象,经过缩放、解压缩操作后生成一个UIImage对象供完成回调使用。当然,在这个方法中还需要处理的就是进度信息。如果我们有设置进度回调的话,就调用进度回调以处理当前图片的下载进度。
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.imageData appendData:data];
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
// 以下的代码来自于 http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
// 谢谢作者 @Nyx0uf
// 获得已经下载的总共字节数
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);
// 当绘制到Core Graphics时,我们会丢失方向信息,这意味着有时候由initWithCGIImage创建的
// 图片的方向不对。(不像在connectionDidFinishLoading中用initWithData创建的图片)
// 所以在这边我们先保存这个信息并在后面使用
orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
}
}
// 图片还未下载完成
if (width + height > 0 && totalSize < self.expectedSize) {
// 使用现有的数据创建图片对象,如果数据中存有多张图片,则取第一张
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#ifdef TARGET_OS_IPHONE
// 适用于iOS变形图像的解决方案。
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);
}
}
我们前面说过SDWebImageDownloaderOperation类是继承自NSOperation类。它没有简单的实现main方法,而是采用更加灵活的start方法,以便自己管理下载的状态。
在start方法中,创建了我们下载所使用的NSURLConnection对象,开启了图片的下载,同时抛出一个下载开始的通知。start方法的具体实现如下:
- (void)start {
@synchronized (self) {
//管理下载状态,如果已取消,则重置当前下载并设置完成状态YES
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];
});
// 确保我们的后台线程启动runloop,这样就能处理下载的数据。
// Fail、Finish或者cancel的时候CFRunloopStop(CFRunloopGetCurrent())关闭runloop。如果不写 // CFRunloopRun(),根本不会执行NSURLConnection的代理方法的,因为该线程没开启runloop,马上就完了。 // runloop相当于子线程循环,可以灵活控制子线程的生命周期。
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
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
}
当然,在下载完成或者下载失败后,需要停止当前线程的runloop,清除连接,并抛出下载停止的通知。如果下载成功,则会处理完整的图片数据,对其进行适当的缩放与解压缩操作,以提供给完成回调使用。具体可参考-connectionDidFinishLoading:与-connection:didFailWithError:的实现。
小结
下载的核心其实就是利用NSURLConnection对象来加载数据。每个图片的下载都由一个Operation操作来完成,并将这些操作放到一个操作队列中。这样可以实现图片的并发下载。