终章
本篇文章是整个 SD 源码解析的最后一章,在这一篇文章中我们将着手理解图片的下载工作;并且会对这一过程中的一些遗漏的知识点做一点点补充。
还记得在 SDWebImageManager 中 在没要找到缓存的情况下调用的这个方法么:
//返回的token主要是用来便于取消单个下载任务的(为了方便读者阅读,这段代码格式有点扭曲,请见谅)
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader
downloadImageWithURL:url
options:downloaderOptions
progress:progressBlock
completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
}];
所谓把简单留给别人,把复杂留给自己,别的地方虽然就调用这么一小段就可以开始下载图片的操作,但是咱们探究实现过程才是重要的。 SDWebImageDownloader 和 SDWebImageDownloaderOperation 的关系无非就是下载任务是交给 SDWebImageDownloader 来管理,而一个个具体的任务就是封装在每个 SDWebImageDownloaderOperation 中。
SDWebImageDownloader.h
1.SDWebImageDownloaderOptions
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
SDWebImageDownloaderLowPriority = 1 << 0, 任务低优先级
SDWebImageDownloaderProgressiveDownload = 1 << 1, // 带有进度
SDWebImageDownloaderUseNSURLCache = 1 << 2, // 默认情况下都是不使用NSURLCache缓存的,这个选项开启表示启用NSURLCache缓存
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3, // 忽略缓存响应,如果在NSURLCache找到缓存图片,complete回调里面的图片还是nil
SDWebImageDownloaderContinueInBackground = 1 << 4, // 支持后台下载
SDWebImageDownloaderHandleCookies = 1 << 5, // 使用Cookies
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, // 允许验证SSL
SDWebImageDownloaderHighPriority = 1 << 7, // 任务高优先级
SDWebImageDownloaderScaleDownLargeImages = 1 << 8, // 裁剪大图片
}
2.SDWebImageDownloaderExecutionOrder
//默认情况下任务队列中的任务默认是先进去先执行的,但其实可以通过设置依赖来修改执行顺序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
SDWebImageDownloaderFIFOExecutionOrder,
SDWebImageDownloaderLIFOExecutionOrder
};
3.SDWebImageDownloadToken
这个token是在创建下载操作时返回的一个参数,可以看到包含了两个字段,一个url,一个是downloadOperationCancelToken,往后面看就知道,这其实是把下载的进度回调和完成回调封装成的一个字典:
{kProgressCallbackKey : progressBlock, kCompletedCallbackKey : completedBlock}
@interface SDWebImageDownloadToken : NSObject
@property (nonatomic, strong, nullable) NSURL *url;
@property (nonatomic, strong, nullable) id downloadOperationCancelToken;
@end
4.几个特别的属性与方法:
.h
//下载的图片是否解压到内存,可以提高效率,但是会消耗内存,默认是yes
@property (assign, nonatomic) BOOL shouldDecompressImages;
//前面提到过,更改任务下载执行顺序,默认是是先入先出
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;
//自定义过滤HTTP head,返回的head将作为那张图片请求的Head
@property (nonatomic, copy, nullable) SDWebImageDownloaderHeadersFilterBlock headersFilter;
//指定初始化方法,注意NS_DESIGNATED_INITIALIZER的使用,强调这是一个指定构造器
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration NS_DESIGNATED_INITIALIZER;
//根据token取消一个任务
- (void)cancel:(nullable SDWebImageDownloadToken *)token;
//将整个下载队列任务挂起
- (void)setSuspended:(BOOL)suspended;
.m
//用于下载的队列
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;
//最后加载到队列中的任务
@property (weak, nonatomic, nullable) NSOperation *lastAddedOperation;
//装的是正在进行下载的downloaderOperation,后面要进行cancle的时候根据url进行canle
@property (strong, nonatomic, nonnull) NSMutableDictionary *URLOperations;
//HTTP Header
@property (strong, nonatomic, nullable) SDHTTPHeadersMutableDictionary *HTTPHeaders;
//这个队列用于并发处理取消任务和创建下载任务
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t barrierQueue;
//用于下载图片的session
@property (strong, nonatomic) NSURLSession *session;
上面大致介绍了在这个类中涉及到的一些乍眼一看不容易懂的属性或者方法,其余还有一些简单的接口与属性,就不一一列举,相信读者一看源码就应该知道
4.详细介绍几个方法作用与实现
//因为将下面的另一个初始化方法通过 NS_DESIGNATED_INITIALIZER 声明为指定构造器,因此这里需要重载父类的指定构造器,并将其调用自身的指定构造器
- (nonnull instancetype)init {
return [self initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
}
//指定构造器,主要是对属性的一些初始化
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
if ((self = [super init])) {
_operationClass = [SDWebImageDownloaderOperation class];
//下载完后自动解压图片到内存
_shouldDecompressImages = YES;
//默认下载任务执行顺序,先进入先下载
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
_downloadQueue = [NSOperationQueue new];
//下载队列默认最大并发数为6
_downloadQueue.maxConcurrentOperationCount = 6;
//创建和取消任务的队列名
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
_URLOperations = [NSMutableDictionary new];
//image/webp是web格式的图片,这里表示图片优先接受image/webp,其次接受image/*的图片。
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
//设置超时时间为15秒
_downloadTimeout = 15.0;
sessionConfiguration.timeoutIntervalForRequest = _downloadTimeout;
//初始化session,注意delegateQueue为空,session将自己创建一个串行队列来处理所有的delegate回调
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
return self;
}
就是一些 setter 和 getter
- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field {
if (value) {
self.HTTPHeaders[field] = value;
}
else {
[self.HTTPHeaders removeObjectForKey:field];
}
}
- (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field {
return self.HTTPHeaders[field];
}
- (void)setMaxConcurrentDownloads:(NSInteger)maxConcurrentDownloads {
_downloadQueue.maxConcurrentOperationCount = maxConcurrentDownloads;
}
- (NSUInteger)currentDownloadCount {
return _downloadQueue.operationCount;
}
- (NSInteger)maxConcurrentDownloads {
return _downloadQueue.maxConcurrentOperationCount;
}
- (void)setOperationClass:(nullable Class)operationClass {
if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperationInterface)]) {
_operationClass = operationClass;
} else {
_operationClass = [SDWebImageDownloaderOperation class];
}
}
介绍开篇就提到的那个下载方法:
//先把代码折叠起来,发现他其实就是调用了另外一个方法,这里只是把创建 SDWebImageDownloaderOperation 的block传进去,因此,咱们在这个函数看创建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 *{}];
}
//再展开叙述创建Operation过程,这里就简写,简单的把block展开:
^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
//超时时间如果没有设置就设置为默认的15秒
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
//NSURLRequestUseProtocolCachePolicy: 对特定的 URL 请求使用网络协议中实现的缓存逻辑。这是默认的策略。
//NSURLRequestReloadIgnoringLocalCacheData:数据需要从原始地址加载。不使用现有缓存。
//NSURLRequestReloadIgnoringLocalAndRemoteCacheData:不仅忽略本地缓存,同时也忽略代理服务器或其他中间介质目前已有的、协议允许的缓存。
//NSURLRequestReturnCacheDataElseLoad:无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么从原始地址加载数据。
//NSURLRequestReturnCacheDataDontLoad:无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么放弃从原始地址加载数据,请求视为失败(即:“离线”模式)。
//NSURLRequestReloadRevalidatingCacheData:从原始地址确认缓存数据的合法性后,缓存数据就可以使用,否则从原始地址加载。
//指定默认的缓存策略就是忽略本地NSURL缓存,使用SD自己实现的缓存
NSURLRequestCachePolicy cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
//如果强行SD指定的策略为使用NSURL缓存
if (options & SDWebImageDownloaderUseNSURLCache) {
//使用使用NSURL缓存,但是缓存策略又是即使找到缓存也返回nil
if (options & SDWebImageDownloaderIgnoreCachedResponse) {
//那就只有将NSURL的缓存策略设置为不从远端加载,使用NSURLCache了
cachePolicy = NSURLRequestReturnCacheDataDontLoad;
} else {
//使用使用NSURL缓存
cachePolicy = NSURLRequestUseProtocolCachePolicy;
}
}
//创建Request
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
//使用cookie
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
//是否等到前一个请求响应了才继续请求本次的数据
request.HTTPShouldUsePipelining = YES;
//如果要过滤Http Head 就过滤
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
}
//没有过滤就直接设置
else {
request.allHTTPHeaderFields = sself.HTTPHeaders;
}
//创建一个Operation,一个Operation对应一个NSURLSessionTask ,所以这里传入request
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) {//通过设置依赖的方式颠倒执行顺序
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}
addProgress 然后返回DownloadToken的过程
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
//URL不能为空,如果为空直接回调;URL 有两个用途一方面是 SDWebImageDownloadToken 里面要存,另一方面在存当前正在下载的 Operation 的字典中 URL 是作为字典的key;
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
__block SDWebImageDownloadToken *token = nil;
//同步栅栏,当self.barrierQueue中的任务执行完了之后才会将新的任务加进去,这里目的就是返回一个Operation,已经存的就返回存在的,没有就创建一个新的
dispatch_barrier_sync(self.barrierQueue, ^{
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
//如果在正在进行的队列里面没有找到url对应的任务,那就调用创建Operation的函数,创建一个
operation = createCallback();
self.URLOperations[url] = operation;
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
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好理解,就是之前提到的将进度回调和下载回调封装而成的一个字典
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
return token;
}
以上主要的就是创建token和创建Operation两个重点,接下来看这个文件剩下的代码--delegate
所有的 delegate 都被传递给 SDWebImageDownloaderOperation 自己,我疑惑的是为什么不直接把delegate就设置为Operation呢,后来想想估计是为了复用session吧。所以咱们这里就大致介绍一下每个delegate的作用吧:
这里寻找Operation的实例时,因为前面提到一个Operation和一个task其实是一一对应的关系,因此只要比较代理回调的task的taskIdentifier和Operation对象存的task的taskIdentifier是否相等即可:
NSURLSessionDataDelegate部分
//数据传输完成
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
// Identify the operation that runs this task and pass it the delegate method
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:task];
[dataOperation URLSession:session task:task didCompleteWithError:error];
}
//接收到服务端重定向的时候
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
completionHandler(request);
}
//客户端收到服务端的认证请求时
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:task];
[dataOperation URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler];
}
NSURLSessionDataDelegate 部分
//客户端收到服务端响应时
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];
[dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}
//客户端收到数据时
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
// Identify the operation that runs this task and pass it the delegate method
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];
[dataOperation URLSession:session dataTask:dataTask didReceiveData:data];
}
//客户端在接受完所有数据之后,处理是否缓存响应,实现这个代理目的一般就是避免缓存响应
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];
[dataOperation URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
}
SDWebImageDownloaderOperation.h
SDWebImageDownloaderOperation每一个实例都是一个下载任务,这里将每一个下载任务单独封装到一个Operation中,除主要包含以下内容:
- 继承NSOperation,所需要实现的一些必须的方法
- 相关参数设置
- 开始请求与取消请求
- NSURLSession相关回调
先来看看一些初始化的东西:
//因为是被downloader持有,所以本身不持有,所以是weak
@property (weak, nonatomic) NSURLSession *unownedSession;
// This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
//如果downloader的session不存在于是自己创建并持有(其实我有点搞不清这样的目的是什么?为什么要考虑downloader传入的session不存在的情况?期待大神回复)
@property (strong, nonatomic) NSURLSession *ownedSession;
//sessiontask
@property (strong, nonatomic, readwrite) NSURLSessionTask *dataTask;
//添加和删除任务回调的队列
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t barrierQueue;
#if SD_UIKIT100
@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
#endif
@end
@implementation SDWebImageDownloaderOperation {
//图片长,宽
size_t width, height;
//图片方向
UIImageOrientation orientation;
//是否图片是从cache读出
BOOL responseFromCached;
}
初始化方法:
- (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;
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
配置回调,将进度回调和瞎子啊完成回调放在一个字典里面,并返回:
- (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];
dispatch_barrier_async(self.barrierQueue, ^{
[self.callbackBlocks addObject:callbacks];
});
两个疑问:1.为什么两个block要copy?2.为什么要将callbackBlocks的添加元素操作放在队列里面并使用栅栏异步执行?
1.我们知道Block也算是变量,默认情况下局部变量放置在栈上,因此Block要超出作用域依然不会释放的方法就是移动到堆上,之前我提到过栈上的Block在某些情况下会复制到堆上:
- 调用block的copy函数时
- Block作为函数返回值返回时
- 将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量时
- 方法中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时
而仅有一下几种情况会系统自动复制到堆上,因此别的情况下都有需要手动调用copy:
- block作为函数值返回的时候
- 部分情况下向方法或函数中传递block的时候
- Cocoa框架的方法而且方法名中含有usingBlock等时。
- Grand Central Dispatch 的API。
了解了以上规则之后,我们发现,我们现在这个Block是有必要复制到堆上的,其次,他不能自动复制到堆上,因此我们需要自己调用copy
2.我们知道,将任务放置在队列中,无非是想管理任务,这里的队列是并发队列;所以我们理一下逻辑这个是下载任务的回调,我们可能在某些时候需要将回调添加到字典中,可能某些时候任务取消,我们也需要将回调给移除掉。因为整个下载下载任务其实都是异步的,你并不知道什么时候创建什么时候取消,说白了其实就是读写互斥,个人理解,有点模糊,若有不正确的地方,还请大神指点~~~
配置任务开始任务 start将函数分开一一介绍
先是一个线程线程同步锁(以self作为互斥信号量):可我真不知道为什么
首先是判断当前这个SDWebImageDownloaderOperation是否取消了,如果取消了,即认为该任务已经完成,并且及时回收资源(即reset)。
这里简单介绍下自定义NSOperation的几个重要的步骤,如果你创建了了自定义的并发NSOperation,就需要重写 start()、isAsynchronous、isExecuting、isFinished :
isAsynchronous
:需要返回YES,代表操作队列是并发的
isExecuting
:该状态应该维护,确保其他可以被调用者正确监听操作状态,应该确保该操作是线程安全的
isFinished
:该状态确保操作完成或者取消的时候,都被正确的更新,不然,如果操作不是完成状态,则操作队列不会把改操作从队列中移除,不然会导致依赖其的操作任务无法执行,该操作应该也确保该操作是线程安全的
isExecuting
和isFinished
都必须通过KVO的方法来通知状态更新
start
:必须的,所有并发执行的 operation 都必须要重写这个方法,替换掉 NSOperation 类中的默认实现。start 方法是一个 operation 的起点,我们可以在这里配置任务执行的线程或者一些其它的执行环境。另外,需要特别注意的是,在我们重写的 start 方法中一定不要调用父类的实现;
main
:可选的,通常这个方法就是专门用来实现与该 operation 相关联的任务的。尽管我们可以直接在 start方法中执行我们的任务,但是用 main 方法来实现我们的任务可以使设置代码和任务代码得到分离,从而使 operation 的结构更清晰;
isExecuting
和 isFinished
:必须的,并发执行的 operation 需要负责配置它们的执行环境,并且向外界客户报告执行环境的状态。因此,一个并发执行的 operation 必须要维护一些状态信息,用来记录它的任务是否正在执行,是否已经完成执行等。此外,当这两个方法所代表的值发生变化时,我们需要生成相应的 KVO 通知,以便外界能够观察到这些状态的变化;
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
//...
}
接下来是一段代码,主要是考虑到APP进入后台之后的操作:
#if SD_UIKIT
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
因为这里要用 beginBackgroundTaskWithExpirationHandler,所以需要使用 [UIApplication sharedApplication],因为是第三方库,所以需要使用 NSClassFromString 获取到 UIApplication。这里需要提及的就是 shouldContinueWhenAppEntersBackground,也就是说下载选项中需要设置SDWebImageDownloaderContinueInBackground。
注意 beginBackgroundTaskWithExpirationHandler 并不是意味着立即执行后台任务,它只是相当于注册了一个后台任务,函数后面的 handler block 表示程序在后台运行时间到了后,要运行的代码。这里,后台时间结束时,如果下载任务还在进行,就取消该任务,并且调用 endBackgroundTask,以及置backgroundTaskId 为 UIBackgroundTaskInvalid。
接下来就是绑定 task 了创建下载 dataTask ,如果传入的初始化Operation的时候没有传入session那么就由Operation自己创建并持有,所以这里就解释来开始的属性里面有两个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];
if (self.dataTask) {
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
}
最后还有一个地方值得注意:到这里为止,为什么start结束就把把这个backgroundTaskId置为UIBackgroundTaskInvalid了?万一这会儿还没下载完,正在下载,进入后台了?那岂不是没用了?希望大神解惑~~
#if SD_UIKIT
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
接下来到delegate,比较重要,那些管理Operation状态的可以直接忽略,比较简单,
NSURLSession相关回调:
收到响应时:
//除了304以外<400的工程情况
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;
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, expected, self.request.URL);
}
//创建能够接受的图片大小数据
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.
//304说明图片没有变化,停止operation然后返回cache
if (code == 304) {
[self cancelInternal];
} else {
//其他情况停止请求
[self.dataTask cancel];
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
//完成block,返回错误
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:((NSHTTPURLResponse *)response).statusCode userInfo:nil]];
//结束
[self done];
}
if (completionHandler) {
completionHandler(NSURLSessionResponseAllow);
}
收到数据时:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
//不断将数据添加到iamgeData里面
[self.imageData appendData:data];
//如果需要显示进度
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
// 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);
//为0说明是第一段数据
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 didCompleteWithError.) So save it here and pass it on later.
//当我们绘制Core Graphic,我们会失去图片方向信息
//着意味着用initWithCGIImage将会有的时候并不正确,(不像在didCompleteWithError里用initWithData),所以保存信息
#if SD_UIKIT || SD_WATCH
orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
#endif
}
}
//不是第一段数据,并且没有下载完毕
if (width + height > 0 && totalSize < self.expectedSize) {
// Create the image
//创建图片来源
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#if SD_UIKIT || SD_WATCH
//处理iOS失真图片,确实看不太懂
// 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) {
#if SD_UIKIT || SD_WATCH
//获取图片
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
#elif SD_MAC
UIImage *image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
#endif //获得key
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);
//调回调
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
}
}
CFRelease(imageSource);
}
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
//调用进度回调
progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
}
}
客户端在接受完所有数据之后,处理是否缓存响应,实现这个代理目的一般就是避免缓存响应
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
NSCachedURLResponse *cachedResponse = proposedResponse;
//如果缓存策略是忽略本地缓存的话
if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
// Prevents caching of responses
//忽略本地缓存
cachedResponse = nil;
}
//完成回调
if (completionHandler) {
completionHandler(cachedResponse);
}
}
//下载完成的回调
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
@synchronized(self) {
self.dataTask = nil;
//这里为什么要在主线程去发通知呢,因为通知有个约定就是:发通知的和处理通知的必须在同一个线程,因为大部分UI都在主线程,因此作者挪到了主线程
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
if (!error) {
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
}
});
}
//如果有error,将error返回
if (error) {
[self callCompletionBlocksWithError:error];
} else {
//如果存在完成block
if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
/**
* If you specified to use `NSURLCache`, then the response you get here is what you need.
* if you specified to only use cached data via `SDWebImageDownloaderIgnoreCachedResponse`,
* the response data will be nil.
* So we don't need to check the cache option here, since the system will obey the cache option
*/
if (self.imageData) {
UIImage *image = [UIImage sd_imageWithData:self.imageData];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
//图片适应屏幕
image = [self scaledImageForKey:key image:image];
// Do not force decoding animated GIFs
//如果不是GIF
if (!image.images) {
//压缩图片
if (self.shouldDecompressImages) {
if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
#if SD_UIKIT || SD_WATCH
image = [UIImage decodedAndScaledDownImageWithImage:image];
[self.imageData setData:UIImagePNGRepresentation(image)];
#endif
} else {
image = [UIImage decodedImageWithImage:image];
}
}
}
//图片大小为0,则报错
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
} else {
//完成整个图片处理
[self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
}
} else {
//图片为空
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
}
}
}
[self done];
}
总结
本文主要是对SD中下载图片的部分做了阅读,若大神有觉得小生有理解不正确的地方,还望指出,求轻喷~
参考文献
iOS-采用现代化的Objective-C
NS_DESIGNATED_INITIALIZER作用
正确编写Designated Initializer的几个原则
由#ifdef SD_WEBP
引出的对Build Configuration
的简单探索:
- Xcode多种Build Configuration配置使用
- 使用 Preprocessor Macros 区分 release 和 debug 版本
- Xcode 工程中的那些概念 Target什么的
别的一些细节
- 为什么不能在init中调用accessor
- NSURLCache
- iOS NSNotificationCenter 使用姿势详解