前言
这是本系列的第 4 篇,本篇将主要介绍 SDWebImageDownloader
这个负责下载的类,当然还有一些相关类及协议,如: SDWebImageDownloadToken
、SDWebImageDownloaderOperation
和 SDWebImageDownloaderOperationInterface
等。
正文
开启正文描述之前,依旧先看 2 个重要的枚举:SDWebImageDownloaderOptions
和 SDWebImageDownloaderExecutionOrder
,具体含义见下方代码注释。
// 控制下载过程的选项
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
// 降低下载任务在队列中的优先级
SDWebImageDownloaderLowPriority = 1 << 0,
// 图片将在下载过程中逐步展示,而不是等下载完成后才一次性展示
SDWebImageDownloaderProgressiveDownload = 1 << 1,
// 使用 NSURLCache,默认是不使用的
SDWebImageDownloaderUseNSURLCache = 1 << 2,
// 如果图片是从 NSURLCache 读取的,那么执行 completionHandler 的时候,回传 nil
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
// 后台继续执行任务
SDWebImageDownloaderContinueInBackground = 1 << 4,
// 允许处理存储在 NSHTTPCookieStore 中的 Cookie
SDWebImageDownloaderHandleCookies = 1 << 5,
// 允许不受信任的 SSL 证书,生产环境慎用
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
// 提高下载任务在队列中的优先级
SDWebImageDownloaderHighPriority = 1 << 7,
// 缩放大图
SDWebImageDownloaderScaleDownLargeImages = 1 << 8,
};
// 任务的执行顺序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
// 先进先出,也是队列的默认执行顺序
SDWebImageDownloaderFIFOExecutionOrder,
// 后进先出,栈的执行顺序
SDWebImageDownloaderLIFOExecutionOrder
};
接下来就到了 SDWebImageDownloader 这个类,我不准备一个属性一个方法地按顺序讨论,而是先说创建方法,然后通过一个主要方法将主体串起来。先看创建方法吧!
对外其实只公开了一个创建单例的方法 sharedDownloader
,仔细查看代码会发现,最终调用的是 - (nonnull instancetype)initWithSessionConfiguration:
这个初始化方法,主要做一些初始化工作,并创建一个新 session。如果使用单例方法创建 downloader,则只会有一个 session,而如果通过其他方法创建,则可能创建 session 之前已经有一个了,这时候就需要先将之前的 cancel 之后再创建新的。
+ (nonnull instancetype)sharedDownloader {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
- (nonnull instancetype)init {
return [self initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
}
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
if ((self = [super init])) {
// 执行下载任务的 operation
_operationClass = [SDWebImageDownloaderOperation class];
// 要求解压图片
_shouldDecompressImages = YES;
// 执行顺序,先进先出
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
// 设置下载操作的队列,由于最大并发数是 6,所以此 queue 是 并发队列,如果是 1,则为串行队列。
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
_URLOperations = [NSMutableDictionary new];
// 请求头的字段,可接受的文件类型
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
// 锁,这里使用了信号量
_operationsLock = dispatch_semaphore_create(1);
_headersLock = dispatch_semaphore_create(1);
// 超时时间
_downloadTimeout = 15.0;
[self createNewSessionWithConfiguration:sessionConfiguration];
}
return self;
}
// 创建新的 session
- (void)createNewSessionWithConfiguration:(NSURLSessionConfiguration *)sessionConfiguration {
// 为避免影响,先取消可能存在的下载任务
[self cancelAllDownloads];
// cancel 之前的 session,然后创建一个新的
if (self.session) {
[self.session invalidateAndCancel];
}
sessionConfiguration.timeoutIntervalForRequest = self.downloadTimeout;
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
然后,我们看一下主要方法 - (nullable SDWebImageDownloadToken *)downloadImageWithURL:url options:options progress:progressBlock completed:completedBlock
。直接调用了添加进度与完成回调的方法,并将返回值作为结果返回。
添加进度与完成回调的方法我们稍后再议,先看一下调用时传入的 createCallback
。就做了两件事:先创建一个 request,用于准备一些基础参数,然后,依据 request 创建 operation,详见代码注释 ☟。
- (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 *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// *** 1.创建 request
// 为避免重复缓存 (NSURLCache + SDImageCache) ,如果没有明确要求使用 NSURLCache,我们默认忽略本地缓存
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:cachePolicy
timeoutInterval:timeoutInterval];
// The default is YES - in other words, cookies are sent from and stored to the cookie manager by default.
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
// 设置 header,headersFilter 是过滤头部参数的block
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself allHTTPHeaderFields]);
} else {
request.allHTTPHeaderFields = [sself allHTTPHeaderFields];
}
// *** 2.创建下载的 operation (这个 operationClass ,给他赋什么值,他就是什么,如果不设置,就是默认值:[SDWebImageDownloaderOperation class])
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request
inSession:sself.session
options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
// NSURLCredential 身份认证
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
// NSURLCredentialPersistenceForSession: Credential should be stored only for this session.
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
// 设置优先级
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 更改执行顺序:先进后出(可在此设置) or 先进先出(默认)
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// 通过反向设置依赖,指定了队列中任务的执行顺序先加进去的依赖于后加进去的,那就成了后进先出了
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
现在,我们来看看添加进度与完成回调的方法:
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)(void))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 nil;
}
LOCK(self.operationsLock);
SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
// 如果是第 1 次进来,通过 url 是取不出 URLOperation 的,但是第 2 次就有可能找到,也就是想要重复发第 2 次请求的话,就可以取到。
// 第 2 次可以取到(并且已经完成的情况下),则不会走括号里边,也就不会执行关键步骤:[self.downloadQueue addOperation:operation]; ,所以就不会发起请求了,因为将 operation 添加到队列的时候,系统会自动触发请求。
if (!operation || operation.isFinished) {
// 创建 operation
operation = createCallback();
__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);
// 存放进度和完成回调的 数组 array
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
// 与下载任务关联的一个对象,用于取消操作的时候
SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
token.downloadOperation = operation;
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
主要做了这么几件事:
开始依然是参数校验
然后从 self.URLOperation 里边取 operation
第一次进来当然取不到 operation,于是就会进入
if (!operation || operation.isFinished) { ... }
的代码块。先执行我们传入的createCallback()
创建 operation,然后将 operation 加入到 self.URLOperations 里边,同时设置好 operation 的 completionBlock,到时将 operation 移除,最后将 operation 加入到操作队列里,就会自动开始执行了。创建一个 token,他是
SDWebImageDownloadToken
的实例,将它与 operation、url、progressBlock 及 completedBlock 关联起来,用于后边之后的取消操作。其中 progressBlock 及 completedBlock 是通过 downloadOperationCancelToken 与 token 关联起来的,这里用到了 operation 中的一个方法:
// SDWebImageDownloaderOperation
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
LOCK(self.callbacksLock);
[self.callbackBlocks addObject:callbacks];
UNLOCK(self.callbacksLock);
return callbacks;
}
由此可知,这个 id downloadOperationCancelToken
是一个存放 progressBlock 和 completedBlock 的 dictionary。
这里 if 语句起到了一个非常重要的作用,即 避免重复下载相同数据,具体原因就不解释了,上边的代码注释里已经写了。
到这里是不是觉得少了点什么,是的,SDWebImageDownloaderOperation
和 SDWebImageDownloadToken
的具体实现还不知道呢,接下来我们就分别查看这 2 个类,先从简单的开始吧!
SDWebImageDownloadToken
这个类只有 3 个属性,前边都用到了,属性声明如下:
// 下载任务对应的 url
@property (nonatomic, strong, nullable) NSURL *url;
// 实际是包含 progressBlock 和 completionBlock 的字典,是通过 `addHandlersForProgress:completed:` 返回的,用于取消操作
@property (nonatomic, strong, nullable) id downloadOperationCancelToken;
// 操作的 operation,继承自 NSOperation,不过又遵守了 `SDWebImageDownloaderOperationInterface` 这个协议,扩展了一些方法。
@property (nonatomic, weak, nullable) NSOperation *downloadOperation;
它只实现了一个协议方法 cancel
(Protocol: SDWebImageOperation),其中 self.downloadOperationCancelToken
就是存放 progressBlock 和 completionBlock 的字典,然后将这个 token 创递给了 operation 的 cancel:
方法(也是一个协议方法),这个方法的具体实现下边就会说到。
- (void)cancel {
if (self.downloadOperation) {
SDWebImageDownloadToken *cancelToken = self.downloadOperationCancelToken;
if (cancelToken) {
[self.downloadOperation cancel:cancelToken];
}
}
}
SDWebImageDownloaderOperationInterface
在开始介绍 operation 之前,先看看他遵守的协议 SDWebImageDownloaderOperationInterface
,声明了以下协议方法。如果想要使用自定义的 operation,则它必须继承自 NSOperation 并且遵守这个协议。这些方法的实现可以参考 SDWebImageDownloaderOperation
。
// SDWebImageDownloaderOperationInterface
// 初始化方法
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options;
// 保存进度和完成的回调
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
// 是否需要解压图片
- (BOOL)shouldDecompressImages;
- (void)setShouldDecompressImages:(BOOL)value;
// 凭证或称证书信息
- (nullable NSURLCredential *)credential;
- (void)setCredential:(nullable NSURLCredential *)value;
// 取消
- (BOOL)cancel:(nullable id)token;
SDWebImageDownloaderOperation
现在开始讨论 SDWebImageDownloaderOperation
这个类,下边是初始化方法:
- (nonnull instancetype)init {
return [self initWithRequest:nil inSession:nil options:0];
}
// 也是协议方法
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options {
if ((self = [super init])) {
_request = [request copy];
_shouldDecompressImages = YES;
_options = options;
_callbackBlocks = [NSMutableArray new];
_executing = NO;
_finished = NO;
_expectedSize = 0;
_unownedSession = session;
_callbacksLock = dispatch_semaphore_create(1);
_coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
下面介绍 2 个比较重要的方法:
- 核心方法 start
这是重写父类 NSOperation 的 start 方法,添加了自定义的操作。这个方法不需要手动调用,在将 operation 添加到 operationQueue 中的时候,系统会自动调用其 start 方法。重写后的操作包括以下几点:
①检测操作是否已取消,如果取消了,重置数据后直接返回。
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
②如果需要 App 进入后台时,继续执行下载操作,需要开启后台任务。并设置 ExpirationHandler,取消下载任务,并结束后台操作。
// 如果需要App进入后台时,继续执行此操作,需要开启后台任务。
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;
}
}];
}
③获取或创建 session,然后创建 dataTask,
④若 dataTask 创建成功,启动下载任务 [self.dataTask resume];
,然后执行一次 progressBlock,并发送开始下载的通知。
④若 dataTask 创建失败,直接调用完成回调,构建 error 信息并返回,然后重置数据。
⑤关闭可能存在的后台下载任务。
具体下载过程中的操作,都在 session 相关的那些协议方法里边,详见代码注释,这里就不啰嗦了。
- 取消操作
最后看一下取消操作,即协议方法 - (BOOL)cancel:(nullable id)token;
的实现,将取消过程中用到的所有方法都展开就是下边这样:
- (BOOL)cancel:(nullable id)token {
BOOL shouldCancel = NO;
LOCK(self.callbacksLock);
// 移除 token,即移除一个存储着 completionBlock 和 progressBlock 的字典
[self.callbackBlocks removeObjectIdenticalTo:token];
// 如果已经没有回调,就去执行整体的 cancel 操作
if (self.callbackBlocks.count == 0) {
shouldCancel = YES;
}
UNLOCK(self.callbacksLock);
if (shouldCancel) {
// *** 点开 ☟
[self cancel];
}
return shouldCancel;
}
- (void)cancel {
@synchronized (self) {
// *** 点开 ☟
[self cancelInternal];
}
}
- (void)cancelInternal {
if (self.isFinished) return;
[super cancel];
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];
}
// 重置变量
- (void)reset {
LOCK(self.callbacksLock);
[self.callbackBlocks removeAllObjects];
UNLOCK(self.callbacksLock);
self.dataTask = nil;
if (self.ownedSession) {
[self.ownedSession invalidateAndCancel];
self.ownedSession = nil;
}
}
小结
SDWebImageDownloader 的内容就先介绍到这类,其他细节见 HHSDWebImageStudy 中的源码注释。