前言
SDWebImage是我们开发iOS APP中最常使用到的一个第三方框架,它提供对图片的异步下载与缓存功能。无疑,通过阅读这类优秀开源框架的源码对我们以后的开发也有很大帮助。这篇文章主要记录一下SDWebImage中下载相关的实现分析。
Downloader
SDWebImage中,实现异步下载图片的文件主要是
- SDWebImageDownloader
- SDWebImageDownloaderOperation
下面我们就分别对这两个类的设计与方法实现进行分析。
SDWebImageDownloader
先从.h文件看起,SDWebImageDownloader提供了下载相关的一些枚举,属性还有回调使用的通知和block。
- 下载的策略枚举:
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,
SDWebImageDownloaderScaleDownLargeImages = 1 << 8,
};
- 下载队列的执行策略枚举,提供队列和栈两种策略
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
/**
* 默认,队列方式
*/
SDWebImageDownloaderFIFOExecutionOrder,
/**
* 栈方式
*/
SDWebImageDownloaderLIFOExecutionOrder
};
- 回调相关的通知和block
FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadStartNotification;
FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadStopNotification;
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL);
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished);
typedef NSDictionary<NSString *, NSString *> SDHTTPHeadersDictionary;
typedef NSMutableDictionary<NSString *, NSString *> SDHTTPHeadersMutableDictionary;
typedef SDHTTPHeadersDictionary * _Nullable (^SDWebImageDownloaderHeadersFilterBlock)(NSURL * _Nullable url, SDHTTPHeadersDictionary * _Nullable headers);
新版的SDWebImage已经更新使用NSURLSession进行请求下载。所以SDWebImageDownloader类提供了session相关的属性和设置。例如下面的最大并发数,当前下载数,超时时间和HTTP头的设置等。
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field;
- (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field;
然后来看看.m,SDWebImageDownloader的内部私有属性包括以下
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;
@property (weak, nonatomic, nullable) NSOperation *lastAddedOperation;
@property (assign, nonatomic, nullable) Class operationClass;
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;
@property (strong, nonatomic, nullable) SDHTTPHeadersMutableDictionary *HTTPHeaders;
// This queue is used to serialize the handling of the network responses of all the download operation in a single queue
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t barrierQueue;
// The session in which data tasks will run
@property (strong, nonatomic) NSURLSession *session;
比较重要的就是downloadQueue(下载队列),barrierQueue(gcd队列,用户让图片依次下载)
核心下载方法
downloadImageWithURL函数
SDWebImageDownloader中方法,大部分都是设置或读取参数。核心下载方法只有
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
来看看这个方法的具体实现分析:
__weak SDWebImageDownloader *wself = self;
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
/******
*这个block中只是在构造一个SDWebImageDownloaderOperation的实例,提供给addProgressCallback方法使用
******/
————————————————华丽的分割线————————————————
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = sself.HTTPHeaders;
}
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
[sself.downloadQueue addOperation:operation];
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
我们可以看到,这个方法其实就是调用另一个addProgressCallback
方法,并返回了一个SDWebImageDownloadToken
类型的对象。而addProgressCallback
方法中可能需要block返回一个SDWebImageDownloaderOperation
类型的对象。关于SDWebImageDownloaderOperation
类将在下面讲到。这里我们只需要知道这个方法需要构造一个SDWebImageDownloaderOperation
类型的对象给内部使用。
addProgressCallback函数
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
__block SDWebImageDownloadToken *token = nil;
#使用dispatch_barrier_sync,保证同一时间只有一个线程能对 URLCallbacks 进行操作
dispatch_barrier_sync(self.barrierQueue, ^{
//使用url保存下载任务,如果这个URL被多次下载,也不会创建额外的任务
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
//若该URL没有被下载,则使用外部创建的operation实例。
operation = createCallback();
self.URLOperations[url] = operation;
//保存这个operation下载任务
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
dispatch_barrier_sync(self.barrierQueue, ^{
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
[self.URLOperations removeObjectForKey:url];
};
});
};
}
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
return token;
这个函数中主要目的就是查找或构造一个SDWebImageDownloaderOperation
类型的实例,并保存到self.URLOperations
中。这里还用到一个addHandlersForProgress
函数,将progressBlock和completedBlock传到operation实例中保存起来用户后面的下载回调。
SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
dispatch_barrier_async(self.barrierQueue, ^{
[self.callbackBlocks addObject:callbacks];
});
return callbacks;
返回一个SDCallbacksDictionary类型的callbacks对象,里面存储了这个任务用到的回调block,下图是这个类型的数据结构:
03.png上面内容中我们多次提到了SDWebImageDownloaderOperation
类,接下来我们就对每一个下载任务进行分析。
SDWebImageDownloaderOperation类
SDWebImageDownloaderOperation是NSOperation的子类。也就是说它本身就是一个可以执行在子线程的任务。(参考:每日一问13——多线程之NSOperation与NSOperationQueue)
@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageDownloaderOperationInterface, SDWebImageOperation, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
并且这个类遵循了NSURLSession相关的协议。猜测这个类的实例对象应该是处理用于单张图片的下载任务。果然,我们可以看到里面重写了start方法。撇开后台相关的代码不看,我们可以看到这个任务其实就是使用NSURLSession请求了一个request。
重写Start方法,创建下载任务
//管理下载状态,如果已取消,则重置当前下载并设置完成状态为YES
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
//是否有缓存结果
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
// Grab the cached data for later check
NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request];
if (cachedResponse) {
self.cachedData = cachedResponse.data;
}
}
//检查外部是否创建了session对象,如果没有则自己重新创建
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
[self.dataTask resume];
//通知外部开始下载该URL的任务
if (self.dataTask) {
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
});
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
}
处理返回结果
通过NSURLSeesion的代理获取返回结果。
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
任务接受到响应时的回调,在这里面可以判断本次请求是否成功,并将预计文件数据大小,下载的URL等信息回调给外部。
//只要返回response的code<400并且不是304那么就认为是成功的
if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode < 400 && ((NSHTTPURLResponse *)response).statusCode != 304)) {
//获取预估大小,回调给进度block
NSInteger expected = (NSInteger)response.expectedContentLength;
expected = expected > 0 ? expected : 0;
self.expectedSize = expected;
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, expected, self.request.URL);
}
//由于图片数据是分批返回的,这里先为imagedata分配一块大小合适的内存空间,并通知外部接受到了成功的返回
self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
self.response = response;
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:weakSelf];
});
} else {
NSUInteger code = ((NSHTTPURLResponse *)response).statusCode;
//失败情况则停止本次下载任务,并通知外部该任务已被取消
if (code == 304) {
[self cancelInternal];
} else {
[self.dataTask cancel];
}
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
});
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:((NSHTTPURLResponse *)response).statusCode userInfo:nil]];
[self done];
}
在知道本次请求成功后,就可以开始处理返回的数据了。通过代理方法:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
这个代理会执行很多次,每一次都会返回一部分数据回来,下面看看这里面具体是怎么处理的:
//将本次返回的数据添加到imageData中
[self.imageData appendData:data];
//判断是否需要进度
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
//获取当前数据总大小,与预估大小比较,判断是否下载完毕
// Get the image data
NSData *imageData = [self.imageData copy];
// Get the total bytes downloaded
const NSInteger totalSize = imageData.length;
// Get the finish status
BOOL finished = (totalSize >= self.expectedSize);
//这里先判断下载图片格式,如果是webP格式的话就不作处理,其他格式图片则新创建一个支持SDWebImageProgressiveCoder协议的对象。
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;
}
}
}
//对数据进行强制解压,生成位图返回给外部,可以做到部分显示变全
UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
if (image) {
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
}
[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);
}
当所有数据接收完毕后,会执行完成回调:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
在这个回调中,主要就是通知外部本次请求结束,然后将结果回调出去
@synchronized(self) {
self.dataTask = nil;
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
if (!error) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:weakSelf];
}
});
}
if (error) {
[self callCompletionBlocksWithError:error];
} else {
if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
/**
* If you specified to use `NSURLCache`, then the response you get here is what you need.
*/
NSData *imageData = [self.imageData copy];
if (imageData) {
//检测时候开启缓存策略,并且当前下载的图片和缓存的图片是否一致
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
//是缓存过的图片,则直接回调nil
[self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
} else {
//没有缓存过,则对图片进行解压,然后回调给外部
UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
BOOL shouldDecode = YES;
// Do not force decoding animated GIFs and WebPs
if (image.images) {
shouldDecode = NO;
} else {
#ifdef SD_WEBP
SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
if (imageFormat == SDImageFormatWebP) {
shouldDecode = NO;
}
#endif
}
if (shouldDecode) {
if (self.shouldDecompressImages) {
BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
}
}
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
} else {
[self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
}
}
} else {
//回调失败信息
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
}
}
}
//结束本次下载任务
[self done];
小结:关于SDWebImage下载模块主要需要注意的就是任务的缓存处理,合理的创建下载资源,使用NSOperation创建并发任务,将每一个下载任务分配到operation中。
相关文章
SDWebImage源码阅读笔记
SDWebImage源码阅读
SDWebImage 源码阅读笔记(三)
SDWebImage源码阅读笔记
网友评论