SDWebImage源码阅读(八)SDWebImageDownloaderOperation(下)

  上篇主要对 NSOperation 进行了拓展学习,下面开始着重学习 SDWebImageDownloaderOperation。

  通知

1 extern NSString * _Nonnull const SDWebImageDownloadStartNotification;
2 extern NSString * _Nonnull const SDWebImageDownloadReceiveResponseNotification;
3 extern NSString * _Nonnull const SDWebImageDownloadStopNotification;
4 extern NSString * _Nonnull const SDWebImageDownloadFinishNotification;

  这 4 个不可变字符串,分别表示:开始下载、接收到数据、停止、完成的通知。

  SDWebImageDownloaderOperationInterface 

 1 /**
 2  Describes a downloader operation. If one wants to use a custom downloader op, it needs to inherit from `NSOperation` and conform to this protocol
 3  */
 4 @protocol SDWebImageDownloaderOperationInterface<NSObject>
 5 
 6 - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
 7                               inSession:(nullable NSURLSession *)session
 8                                 options:(SDWebImageDownloaderOptions)options;
 9 
10 - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
11                             completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
12 
13 - (BOOL)shouldDecompressImages;
14 - (void)setShouldDecompressImages:(BOOL)value;
15 
16 - (nullable NSURLCredential *)credential;
17 - (void)setCredential:(nullable NSURLCredential *)value;
18 
19 @end

  描述一个下载操作,如果你想使用一个自定义的下载操作,就需要继承 NSOperation 并且遵守 SDWebImageDownloaderOperationInterface 协议。

  6 个代理方法:

  1.使用 NSURLRequest 、NSURLSession 、SDWebImageDownloaderOptions 初始化一个 SDWebImageDownloaderOperation 对象。

  2.添加下载进度和下载完成的 block。

  3.是否解压图像的 setter 和 getter 方法。

  4.是否需要设置凭证的 setter 和 getter 方法。

  SDWebImageDownloaderOperation

1 @interface SDWebImageDownloaderOperation : NSOperation <SDWebImageDownloaderOperationInterface, SDWebImageOperation, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>

  SDWebImageDownloaderOperation 遵守 SDWebImageDownloaderOperationInterface 、SDWebImageOperation 、NSURLSessionTaskDelegate 、NSURLSessionDataDelegate 协议,首先是上面的 6 个代理方法都要实现。在设计这个 .h 的时候,可以把协议中的方法再次写入这个 .h 中,这样在使用的时候会更加直观。

1 @property (assign, nonatomic) BOOL shouldDecompressImages;
2 
3 /**
4  * The credential used for authentication challenges in `-connection:didReceiveAuthenticationChallenge:`.
5  *
6  * This will be overridden by any shared credentials that exist for the username or password of the request URL, if present.
7  */
8 @property (nonatomic, strong, nullable) NSURLCredential *credential;

  通过声明这两个属性,就实现了 SDWebImageDownloaderOperationInterface 协议中的对应的 setter 和 getter 方法对应的代理方法。

  一般情况下,如果主动指明类的初始化方法,那么肯定会为初始化方法设定几个参数。

  那么这些参数就应该以只读的方式暴露出来。

  指定初始化方法:

 1 /**
 2  *  Initializes a `SDWebImageDownloaderOperation` object
 3  *
 4  *  @see SDWebImageDownloaderOperation
 5  *
 6  *  @param request        the URL request
 7  *  @param session        the URL session in which this operation will run
 8  *  @param options        downloader options
 9  *
10  *  @return the initialized instance
11  */
12 - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
13                               inSession:(nullable NSURLSession *)session
14                                 options:(SDWebImageDownloaderOptions)options NS_DESIGNATED_INITIALIZER;

  只读的属性:

 1 /**
 2  * The request used by the operation's task.
 3  */
 4 @property (strong, nonatomic, readonly, nullable) NSURLRequest *request;
 5 
 6 /**
 7  * The operation's task
 8  */
 9 @property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask;
10 
11 /**
12  * The SDWebImageDownloaderOptions for the receiver.
13  */
14 @property (assign, nonatomic, readonly) SDWebImageDownloaderOptions options;

  其它的属性:

1 /**
2  * The expected size of data.
3  */
4 @property (assign, nonatomic) NSInteger expectedSize;
5 
6 /**
7  * The response returned by the operation's connection.
8  */
9 @property (strong, nonatomic, nullable) NSURLResponse *response;

  表示数据预期的大小和操作连接返回的响应。

  返回用于取消此处理程序集的标记 和 取消操作

 1 /**
 2  *  Adds handlers for progress and completion. Returns a tokent that can be passed to -cancel: to cancel this set of
 3  *  callbacks.
 4  *
 5  *  @param progressBlock  the block executed when a new chunk of data arrives.
 6  *                        @note the progress block is executed on a background queue
 7  *  @param completedBlock the block executed when the download is done.
 8  *                        @note the completed block is executed on the main queue for success. If errors are found, there is a chance the block will be executed on a background queue
 9  *
10  *  @return the token to use to cancel this set of handlers
11  */
12 - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
13                             completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
14 
15 /**
16  *  Cancels a set of callbacks. Once all callbacks are canceled, the operation is cancelled.
17  *
18  *  @param token the token representing a set of callbacks to cancel
19  *
20  *  @return YES if the operation was stopped because this was the last token to be canceled. NO otherwise.
21  */
22 - (BOOL)cancel:(nullable id)token;

  这个方法不是取消任务的,而是取消任务中的响应,当任务中没有响应者的时候,任务会被取消。

  SDWebImageDownloaderOperation.m

1 NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
2 NSString *const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification";
3 NSString *const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification";
4 NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification";

  4 个不可变的通知的字符串的初始化。

1 static NSString *const kProgressCallbackKey = @"progress";
2 static NSString *const kCompletedCallbackKey = @"completed";

  定义两个静态不可变的字符串,表示下载进度回调的 key 和下载完成的回调的 key。

1 typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;

  定义一个 key 是字符串 value 是 id 的可变的字典类型。( SDCallbacksDictionary)

 1 @interface SDWebImageDownloaderOperation ()
 2 
 3 @property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;
 4 
 5 @property (assign, nonatomic, getter = isExecuting) BOOL executing;
 6 @property (assign, nonatomic, getter = isFinished) BOOL finished;
 7 @property (strong, nonatomic, nullable) NSMutableData *imageData;
 8 
 9 // This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run
10 // the task associated with this operation
11 @property (weak, nonatomic, nullable) NSURLSession *unownedSession;
12 // This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
13 @property (strong, nonatomic, nullable) NSURLSession *ownedSession;
14 
15 @property (strong, nonatomic, readwrite, nullable) NSURLSessionTask *dataTask;
16 
17 @property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t barrierQueue;
18 
19 #if SD_UIKIT
20 @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
21 #endif
22 
23 @end
24 
25 @implementation SDWebImageDownloaderOperation {
26     size_t width, height;
27 #if SD_UIKIT || SD_WATCH
28     UIImageOrientation orientation;
29 #endif
30     BOOL responseFromCached;
31 }
32 
33 @synthesize executing = _executing;
34 @synthesize finished = _finished;

   callbackBlocks 是一个可变数组,数组里面存放的是  SDCallbacksDictionary,这是上面定义的一个可变字典的类型。这个字典的 key 是字符串,这个字符串有种情况,就是上面定义的两个静态不可变字符串:kProgressCallbackKey 和 kCompletedCallbackKey,也就是说进度和完成的回调都放在一个数组里,字典的 value 就是回调的 block。

  unownedSession 这个属性是初始化时候传进来的参数,作者提到,这个参数不一定是可用的,也就是说是不安全的,当出现不可用的情况的时候,就需要使用 ownedSession

  responseFromCached  用于设置是否需要缓存响应,默认是 YES。 

  barrierQueue 是一个 GCD 队列。

  backgroundTaskId 是在 app 进入后台后申请的后台任务的身份。 

  初始化一个任务 

 1 - (nonnull instancetype)init {
 2     return [self initWithRequest:nil inSession:nil options:0];
 3 }
 4 
 5 - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
 6                               inSession:(nullable NSURLSession *)session
 7                                 options:(SDWebImageDownloaderOptions)options {
 8     if ((self = [super init])) {
 9         _request = [request copy];
10         _shouldDecompressImages = YES;
11         _options = options;
12         _callbackBlocks = [NSMutableArray new];
13         _executing = NO;
14         _finished = NO;
15         _expectedSize = 0;
16         _unownedSession = session;
17         responseFromCached = YES; // Initially wrong until `- URLSession:dataTask:willCacheResponse:completionHandler: is called or not called
18         _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
19     }
20     return self;
21 }
22 
23 - (void)dealloc {
24     SDDispatchQueueRelease(_barrierQueue);
25 }

  指定初始化方法实现,主要给属性赋值。

  添加响应者

 1 - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
 2                             completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
 3     SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
 4     if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
 5     if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
 6     dispatch_barrier_async(self.barrierQueue, ^{
 7         [self.callbackBlocks addObject:callbacks];
 8     });
 9     return callbacks;
10 }

  把指定 key 和 指定 value 的字典添加到 _callbackBlocks 数组。

  dispatch_barrier_async

  我们可以创建两种类型的队列,串行和并行,也就是:

  DISPATCH_QUEUE_SERIALDISPATCH_QUEUE_CONCURRENT。那么 dispatch_barrier_asyncdispatch_barrier_sync 有什么不同之处:

  barrier 这个词是栅栏的意思,也就是说是用来做拦截功能的,上边的这两种都能够拦截任务,换句话说,就是只有当前任务完成后,队列后边的任务才能完成。

  不同之处就是,dispatch_barrier_sync 控制了任务往队列添加这一过程,只有当前任务完成之后,才能往队列中添加任务。dispatch_barrier_async 不会控制队列添加任务,但是只有当前任务完成后,队列中后边的任务才会执行。

  那么在这里的任务是往数组中添加数据,对顺序没什么要求,采取 dispatch_barrier_async 就可以,已经能保证数据添加的安全性了。

 

1 - (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
2     __block NSMutableArray<id> *callbacks = nil;
3     dispatch_sync(self.barrierQueue, ^{
4         // We need to remove [NSNull null] because there might not always be a progress block for each callback
5         callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
6         [callbacks removeObjectIdenticalTo:[NSNull null]];
7     });
8     return [callbacks copy];    // strip mutability here
9 }

  我们需要删除 [NSNull null],因为每次回调可能不总是有一个回调 block。

  在 self.barrierQueue 队列里面同步执行。

  self.callbackBlocks 是一个数组,但是这里从数组里面取 block 采用了:

1 callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
1 @interface NSDictionary<KeyType, ObjectType>(NSKeyValueCoding)
2 
3 /* Return the result of sending -objectForKey: to the receiver.
4 */
5 - (nullable ObjectType)valueForKey:(NSString *)key;
6 
7 @end

  假定 self.callbackBlocks 的结构是这样的:

1 @[@{@"completed" : Block1}, 
2 @{@"progress" : Block2}, 
3 @{@"completed" : Block3}, 
4 @{@"progress" : Block4}, 
5 @{@"completed" : Block5}, 
6 @{@"progress" : Block6}]

   那么得到的如果 key 是 @"progress" 则 callbacks 就是 [Block2, Block4, Block6]。

1 - (void)removeObjectIdenticalTo:(ObjectType)anObject;

  这个方法会移除数组中和 anObject 相同地址的元素。

1 [callbacks removeObjectIdenticalTo:[NSNull null]];

  移除 callbacks 里面的 [NSNull null]。

 1 - (BOOL)cancel:(nullable id)token {
 2     __block BOOL shouldCancel = NO;
 3     dispatch_barrier_sync(self.barrierQueue, ^{
 4         [self.callbackBlocks removeObjectIdenticalTo:token];
 5         if (self.callbackBlocks.count == 0) {
 6             shouldCancel = YES;
 7         }
 8     });
 9     if (shouldCancel) {
10         [self cancel];
11     }
12     return shouldCancel;
13 }

   布尔 shouldCancel 表示是否取消操作。

  在 self.barrierQueue 同步删除 self.callbackBlocks 里面的指定回调。(typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;)

  当 self.callbackBlocks 里面的回调删除完的时候,取消操作。

  开启下载任务

 1 - (void)start {
 2     @synchronized (self) {
 3         if (self.isCancelled) {
 4             self.finished = YES;
 5             [self reset];
 6             return;
 7         }
 8 
 9 #if SD_UIKIT
10         Class UIApplicationClass = NSClassFromString(@"UIApplication");
11         BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
12         if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
13             __weak __typeof__ (self) wself = self;
14             UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
15             self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
16                 __strong __typeof (wself) sself = wself;
17 
18                 if (sself) {
19                     [sself cancel];
20 
21                     [app endBackgroundTask:sself.backgroundTaskId];
22                     sself.backgroundTaskId = UIBackgroundTaskInvalid;
23                 }
24             }];
25         }
26 #endif
27         NSURLSession *session = self.unownedSession;
28         if (!self.unownedSession) {
29             NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
30             sessionConfig.timeoutIntervalForRequest = 15;
31             
32             /**
33              *  Create the session for this task
34              *  We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
35              *  method calls and completion handler calls.
36              */
37             self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
38                                                               delegate:self
39                                                          delegateQueue:nil];
40             session = self.ownedSession;
41         }
42         
43         self.dataTask = [session dataTaskWithRequest:self.request];
44         self.executing = YES;
45     }
46     
47     [self.dataTask resume];
48 
49     if (self.dataTask) {
50         for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
51             progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
52         }
53         dispatch_async(dispatch_get_main_queue(), ^{
54             [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
55         });
56     } else {
57         [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
58     }
59 
60 #if SD_UIKIT
61     Class UIApplicationClass = NSClassFromString(@"UIApplication");
62     if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
63         return;
64     }
65     if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
66         UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
67         [app endBackgroundTask:self.backgroundTaskId];
68         self.backgroundTaskId = UIBackgroundTaskInvalid;
69     }
70 #endif
71 }

  开启下载任务,首先判断该任务是否已经被设置取消了,如果被设置取消了,那么就不需要开启下载任务了,并重置,并设置 finished 为 YES(通过 KVO 设置)。

1 - (void)setFinished:(BOOL)finished {
2     [self willChangeValueForKey:@"isFinished"];
3     _finished = finished;
4     [self didChangeValueForKey:@"isFinished"];
5 }

 

  重置操作:

 1 - (void)reset {
 2     dispatch_barrier_async(self.barrierQueue, ^{
 3         [self.callbackBlocks removeAllObjects];
 4     });
 5     self.dataTask = nil;
 6     self.imageData = nil;
 7     if (self.ownedSession) {
 8         [self.ownedSession invalidateAndCancel];
 9         self.ownedSession = nil;
10     }
11 }

  

  是否开启后台下载任务

  条件 1:

  判断 self.options (下载设置)是否在 app 进入后台后继续未完成的下载(如果后台任务过期,操作将被取消):

 1 - (BOOL)shouldContinueWhenAppEntersBackground {
 2     return self.options & SDWebImageDownloaderContinueInBackground;
 3 }
 4 
 5     /**
 6      * In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for
 7      * extra time in background to let the request finish. If the background task expires the operation will be cancelled.
 8      */
 9 
10     SDWebImageDownloaderContinueInBackground = 1 << 4,

  把 self.options 和 SDWebImageDownloaderContinueInBackground 做与操作。返回 YES 或 NO 判断是否需要在 app 进入后台后申请继续未完成的下载操作。 

   条件 2:

   如果当前是 iOS 或 TV 平台开发,先判断是否有 UIApplication 类 和 UIApplication 类是否有 sharedAppliction 方法实现。

  如果上述条件都成立,通过 UIApplication 类向系统申请更多的后台处理时间,去进行未完成的图片下载。

 

  下面进行当 app 进入后台的时候主动申请进行更长时间的后台操作的方法进行拓展学习:

  当 app 进入后台的时候(按 Home 键 app 退出,回到手机主界面的时候),会执行 UIApplicationDelegate 的进入后台的代理方法:

1 - (void)applicationDidEnterBackground:(UIApplication *)application {
2     // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
3     // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
4 
5     // 输入自定义代码
6     
7     }

  当应用程序进入后台后,它的主线程会被暂停。

  当应用程序从前台进入后台,应用程序会调用 applicationDidEnterBackground 代理方法,这个时候应用程序会给几秒的时间处理东西,然后把所有的线程挂起。

  在开发过程中,可能需要更长的时间完成应用程序进入后台的操作,例如把数据保存到本地或者保存到服务器等操作。系统给的时间太短,这个时候就需要向应用程序申请更长的时间进行后台操作。

  如果想在后台完成一个长期的任务,就必须调用 UIApplication 的 beginBackgroundTaskWithExpirationHandler: 实例方法,向 iOS 系统借些 app 的运行时间。

  向 iOS 系统借时间的步骤:

  在 AppDelegate.h 中声明一个 UIBackgroundTaskIdentifier 变量:

1 @property (nonatomic, unsafe_unretained) UIBackgroundTaskIdentifier backgroundTaskIdentifier;
2 @property (strong, nonatomic) NSTimer *myTimer;
3 
4 typedef NSUInteger UIBackgroundTaskIdentifier;

  在 AppDelegate.m 文件中:

  判断当前设备是否支持后台多任务:

1 - (BOOL)isMutiltaskingSupported {
2     BOOL result = NO;
3     if ([[UIDevice currentDevice] respondsToSelector:@selector(isMutiltaskingSupported)]) {
4         result = [[UIDevice currentDevice] isMultitaskingSupported];
5     }
6     return result;
7 }

  结束后台任务:

 1 - (void)endBackgroundTask {
 2     dispatch_queue_t mainQueue = dispatch_get_main_queue();
 3     __weak AppDelegate *weakSelf = self;
 4     dispatch_async(mainQueue, ^{
 5         __strong AppDelegate *strongSelf = weakSelf;
 6         if (strongSelf != nil) {
 7             [strongSelf.myTimer invalidate]; // 停止定时器
 8             
 9             // 每个对 beginBackgroundTaskWithExpirationHandler: 方法的调用,必须相应的调用 endBackgroundTask: 方法,这样来告诉应用程序,前面申请的后台任务已经执行完毕了
10             // 也就是说,向 iOS 系统要更多的时间来完成一个任务,那么必须告诉 iOS 系统什么时候能完成这个后台任务
11             // 标记指定的后台任务完成
12             [[UIApplication sharedApplication] endBackgroundTask:strongSelf.backgroundTaskIdentifier];
13             // 销毁后台任务标识符
14             strongSelf.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
15         }
16     });
17 }

  重点 applicationDidEnterBackground :

 1 - (void)applicationDidEnterBackground:(UIApplication *)application {
 2     // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
 3     // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
 4     
 5     // 当 app 进入后台的时候,释放公共的资源、存储用户数据、停止定义的定时器、存储程序终止前的相关信息
 6     // 如果应用程序提供了后台执行的方法,那么在程序退出时,这个方法将代替 applicationWillTerminate: 方法执行
 7     
 8     if ([self isMutiltaskingSupported] == NO) {
 9         NSLog(@"---> 不支持后台任务");
10         return;
11     }
12     
13     // 标记一个长时间运行的后台任务将开始
14     // 调试发现,iOS 系统给了额外的3分钟来执行这个任务,在 iOS 7 以前,后台可以用下面的的方式,去在后台存活 5-10 分钟,在 iOS 8 后,只能存活3分钟
15     self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
16         // 当应用程序留给后台的时间快要结束时(应用程序留给后台执行的时间是有限的),这个 block 就会被执行
17         // 需要在这个 block 中执行一些清理工作
18         
19         // 清理工作需要在主线程中用同步的方式进行
20         [self endBackgroundTask];
21     }];
22 
23     // 下面的定时器模拟一个需要长时间才能运行结束的后台任务
24     self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.f
25                                                     target:self
26                                                   selector:@selector(timerMethod:)
27                                                   userInfo:nil
28                                                    repeats:YES];
29     
30 }

  定时器调用方法:

1 - (void)timerMethod:(NSTimer *)paramSender {
2     // backgroundTimeRemaining 属性包含了程序留给后台执行任务的时间
3     NSTimeInterval backgroundTimeRemaining = [[UIApplication sharedApplication] backgroundTimeRemaining];
4     if (backgroundTimeRemaining == DBL_MAX) {
5         NSLog(@"Background Time Remaining = Undeterminded");
6     }
7     // 显示后台任务还剩余的时间
8     NSLog(@"Background Timer Remaining = %.2f seconds", backgroundTimeRemaining);
9 }

  控制台打印:

1 2017-06-05 07:28:41.652 NSOperation_DEMO[28232:941126] Background Timer Remaining = 179.00 seconds
2 2017-06-05 07:28:42.651 NSOperation_DEMO[28232:941126] Background Timer Remaining = 178.00 seconds
3 2017-06-05 07:28:43.652 NSOperation_DEMO[28232:941126] Background Timer Remaining = 177.00 seconds
4 2017-06-05 07:28:44.650 NSOperation_DEMO[28232:941126] Background Timer Remaining = 176.00 seconds
5 2017-06-05 07:28:45.651 NSOperation_DEMO[28232:941126] Background Timer Remaining = 175.00 seconds
6 2017-06-05 07:28:46.652 NSOperation_DEMO[28232:941126] Background Timer Remaining = 174.00 seconds
7 ......

  如果任务提前结束了,可以直接调用结束后台任务的方法。

  当应用程序回到了前台,如果后台任务还在执行中,可以在 applicationWillEnterForeground: 中终止它:

1 - (void)applicationWillEnterForeground:(UIApplication *)application {
2     // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
3     if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
4         [self endBackgroundTask];
5     }
6 }

  在 iOS 申请,在后台无限时间进行后台任务:

  在后台用 AVAudioPlayer 无限循环播放一个无声的音频文件。

  并对 Background Modes 进行设置:

  

 

  下面接着往下看开启下载任务的代码:

  当向 iOS 系统申请的后台运行时间结束的时候在:

1 - (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:(void(^ __nullable)(void))handler  NS_AVAILABLE_IOS(4_0) NS_REQUIRES_SUPER;

  的 block 里调用 cancel 方法:   

 1 - (void)cancel {
 2     @synchronized (self) {
 3         [self cancelInternal];
 4     }
 5 }
 6 
 7 - (void)cancelInternal {
 8     if (self.isFinished) return;
 9     [super cancel];
10 
11     if (self.dataTask) {
12         [self.dataTask cancel];
13         dispatch_async(dispatch_get_main_queue(), ^{
14             [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
15         });
16 
17         // As we cancelled the connection, its callback won't be called and thus won't
18         // maintain the isFinished and isExecuting flags.
19         if (self.isExecuting) self.executing = NO;
20         if (!self.isFinished) self.finished = YES;
21     }
22 
23     [self reset];
24 }

  如果 self.isFinished 为真,即下载任务已经完成,直接 return,如果下载任务没有完成,但是申请的后台任务时间(3 分钟)已经到了,则手动结束下载任务。

  @synchronized (self) { } 的作用是创建一个互斥锁,保证在同一个时间内没有其它线程对 self 对象进行修改,起到线程的保护作用,一般在公用变量的时候使用,如单例模式或者操作类的 static 变量中使用。

   如果 self.unownedSession  不存在,则创建一个超时为 15 秒的 NSURLSession 实例赋值给 self.ownedSession。

  self.request 做参数,创建一个 NSURLSessionDataTask 实例,并赋值给 self.dataTask。NSOperation 的 

 的 executing 设置为 YES(通过 KVO 设置)。

1 - (void)setExecuting:(BOOL)executing {
2     [self willChangeValueForKey:@"isExecuting"];
3     _executing = executing;
4     [self didChangeValueForKey:@"isExecuting"];
5 }

  开始执行 self.dataTask。

  如果 self.dataTask 存在,遍历 self.callbackBlocks 先执行第一个进度的 block。

1 - (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
2     __block NSMutableArray<id> *callbacks = nil;
3     dispatch_sync(self.barrierQueue, ^{
4         // We need to remove [NSNull null] because there might not always be a progress block for each callback
5         callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
6         [callbacks removeObjectIdenticalTo:[NSNull null]];
7     });
8     return [callbacks copy];    // strip mutability here
9 }

  在主线程发送一个名为 SDWebImageDownloadStartNotification 的通知。

  如果 self.dataTask 不存在:

 1         [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
 2 
 3 - (void)callCompletionBlocksWithError:(nullable NSError *)error {
 4     [self callCompletionBlocksWithImage:nil imageData:nil error:error finished:YES];
 5 }
 6 
 7 - (void)callCompletionBlocksWithImage:(nullable UIImage *)image
 8                             imageData:(nullable NSData *)imageData
 9                                 error:(nullable NSError *)error
10                              finished:(BOOL)finished {
11     NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
12     dispatch_main_async_safe(^{
13         for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
14             completedBlock(image, imageData, error, finished);
15         }
16     });
17 }

   做 block 回调。

  在往下是不等到申请的后台任务时间结束(3 分钟)就手动结束后台任务。

 

  NSURLSessionDataDelegate

 1 - (void)URLSession:(NSURLSession *)session
 2           dataTask:(NSURLSessionDataTask *)dataTask
 3 didReceiveResponse:(NSURLResponse *)response
 4  completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
 5     
 6     //'304 Not Modified' is an exceptional one
 7     if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode < 400 && ((NSHTTPURLResponse *)response).statusCode != 304)) {
 8         NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
 9         self.expectedSize = expected;
10         for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
11             progressBlock(0, expected, self.request.URL);
12         }
13         
14         self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
15         self.response = response;
16         dispatch_async(dispatch_get_main_queue(), ^{
17             [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
18         });
19     }
20     else {
21         NSUInteger code = ((NSHTTPURLResponse *)response).statusCode;
22         
23         //This is the case when server returns '304 Not Modified'. It means that remote image is not changed.
24         //In case of 304 we need just cancel the operation and return cached image from the cache.
25         if (code == 304) {
26             [self cancelInternal];
27         } else {
28             [self.dataTask cancel];
29         }
30         dispatch_async(dispatch_get_main_queue(), ^{
31             [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
32         });
33         
34         [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:((NSHTTPURLResponse *)response).statusCode userInfo:nil]];
35 
36         [self done];
37     }
38     
39     if (completionHandler) {
40         completionHandler(NSURLSessionResponseAllow);
41     }
42 }

  当收到响应的时候执行的代理方法。当没有收到响应码或者响应码小于 400 且响应码不是 304 的时候,认定为正常的响应。304 比较特殊,当响应码是 304 的时候,表示这个响应没有变化,可以在缓存中读取。响应码是其它的情况的时候就表示为错误的请求。

  当响应正常的时候,给 self.expectedSize 赋值,执行 self.callbackBlocks 里面表示进度的回调 block,给 self.imageData 根据 expectedSize 的大小初始化,把 response  赋值给 self.response,异步调取主线程发送 SDWebImageDownloadReceiveResponseNotification 通知。

  当以上情况都排除,响应不正常的时候,如果服务器返回的响应码是 "304未修改" 的情况,表示远程图像没有改变,对于 304 的情况只需要取消操作,并从缓存返回缓存的图像。

  如果状态码是 304 调用 cancelInternal 方法。

  如果不是 304,self.dataTask 调用 cancel。

  调取主线程发送 SDWebImageDownloadStopNotification 通知。

  调用 callCompletionBlocksWithError: 方法,携带错误信息回调。

  调用 done 方法。 

1 - (void)done {
2     self.finished = YES;
3     self.executing = NO;
4     [self reset];
5 }

  最后执行 completionHandler

 1 - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
 2     [self.imageData appendData:data];
 3 
 4     if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
 5         // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
 6         // Thanks to the author @Nyx0uf
 7 
 8         // Get the total bytes downloaded
 9         const NSInteger totalSize = self.imageData.length;
10 
11         // Update the data source, we must pass ALL the data, not just the new bytes
12         CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
13 
14         if (width + height == 0) {
15             CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
16             if (properties) {
17                 NSInteger orientationValue = -1;
18                 CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
19                 if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
20                 val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
21                 if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
22                 val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
23                 if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
24                 CFRelease(properties);
25 
26                 // When we draw to Core Graphics, we lose orientation information,
27                 // which means the image below born of initWithCGIImage will be
28                 // oriented incorrectly sometimes. (Unlike the image born of initWithData
29                 // in didCompleteWithError.) So save it here and pass it on later.
30 #if SD_UIKIT || SD_WATCH
31                 orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
32 #endif
33             }
34         }
35 
36         if (width + height > 0 && totalSize < self.expectedSize) {
37             // Create the image
38             CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
39 
40 #if SD_UIKIT || SD_WATCH
41             // Workaround for iOS anamorphic image
42             if (partialImageRef) {
43                 const size_t partialHeight = CGImageGetHeight(partialImageRef);
44                 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
45                 CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
46                 CGColorSpaceRelease(colorSpace);
47                 if (bmContext) {
48                     CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
49                     CGImageRelease(partialImageRef);
50                     partialImageRef = CGBitmapContextCreateImage(bmContext);
51                     CGContextRelease(bmContext);
52                 }
53                 else {
54                     CGImageRelease(partialImageRef);
55                     partialImageRef = nil;
56                 }
57             }
58 #endif
59 
60             if (partialImageRef) {
61 #if SD_UIKIT || SD_WATCH
62                 UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
63 #elif SD_MAC
64                 UIImage *image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
65 #endif
66                 NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
67                 UIImage *scaledImage = [self scaledImageForKey:key image:image];
68                 if (self.shouldDecompressImages) {
69                     image = [UIImage decodedImageWithImage:scaledImage];
70                 }
71                 else {
72                     image = scaledImage;
73                 }
74                 CGImageRelease(partialImageRef);
75                 
76                 [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
77             }
78         }
79 
80         CFRelease(imageSource);
81     }
82 
83     for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
84         progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
85     }
86 }

  当收到数据的时候执行的代理方法。这个代理方法会循环调用多次,直到整张图片被下载完毕。

  当 self.options 是 SDWebImageDownloaderProgressiveDownload (按下载的进度显示图片,可以看到图片从上到下一行一行的加载,直到整个图片加载完毕)的时候并且 self.expectedSize 大于 0 的时候会执行一大段操作。

  原本的按照正常的思路,可能是接收到数据的时候,只要把数据拼接起来,根据设置的下载选项,调用下载进度的回调就行了。

  当下载图片要按进度的方式显示下载的图片的时候,即使图片没有下载完,也能根据已经下载的部分图片数据来显示一张数据不完整的图片。

  当第一次回调接收到数据的代理方法的时候:

  self.imageData 已经在上面回调收到响应的代理方法里面进行了初始化(使用 - (nullable instancetype)initWithCapacity:(NSUInteger)capacity 和 预期的图片的大小来做初始化),在接收数据的回调方法里面,每次数据回来都会添加进 self.imageData。

  totalSize 纪录每次 self.imageData 的长度。

  每次使用 self.imageData 创建 imageSource (CGImageSourceRef)。

  imageSource 使用完毕要释放:

1 CFRelease(imageSource);
1 typedef struct IIO_BRIDGED_TYPE(id) CGImageSource * CGImageSourceRef;

  如果当前 width 加 height 等于 0 的时候,多半是第一次回调接收数据的代理方法。

  使用 imageSource  获取 properties (CFDictionaryRef),

1 typedef const struct CF_BRIDGED_TYPE(NSDictionary) __CFDictionary * CFDictionaryRef;

  properties 用来分别获取图片的 width、height、orientationValue 并赋值给各个对应的属性。

1                 NSInteger orientationValue = -1;
2                 CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
3                 if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
4                 val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
5                 if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
6                 val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
7                 if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);

  使用完毕要释放 properties。

1 CFRelease(properties);

  

  (其实完全看不懂上面的 ImageIO 框架的用法。)

 

  如果当前是 iOS 平台或者 TV 平台开发:

  根据 orientationValue 获取图片的 orientation 。

 1 #if SD_UIKIT || SD_WATCH
 2 + (UIImageOrientation)orientationFromPropertyValue:(NSInteger)value {
 3     switch (value) {
 4         case 1:
 5             return UIImageOrientationUp;
 6         case 3:
 7             return UIImageOrientationDown;
 8         case 8:
 9             return UIImageOrientationLeft;
10         case 6:
11             return UIImageOrientationRight;
12         case 2:
13             return UIImageOrientationUpMirrored;
14         case 4:
15             return UIImageOrientationDownMirrored;
16         case 5:
17             return UIImageOrientationLeftMirrored;
18         case 7:
19             return UIImageOrientationRightMirrored;
20         default:
21             return UIImageOrientationUp;
22     }
23 }
24 #endif

   综上所述,当 width + height == 0  的 if 语句里面,只是为了获取图像的宽高方向,三个属性。

  下面 (width + height > 0 && totalSize < self.expectedSize) 的 if 语句里面才是主要对图片数据进行的处理,当接收数据的回调不是第一次调用的时候,width 和 height 大于 0 的时候:

  使用 imageSource 创建当前下载的数据组成的部分图片 partialImageRef (CGImageRef)。

1 typedef struct CGImage *CGImageRef;

  如果当前是 iOS 平台或者 TV 平台或者 WATCH 平台:

  使用 partialImageRef 获取高度赋值给 partialHeight (size_t)。

  CGColorSpaceCreateDeviceRGB() 初始化一个 CGColorSpaceRef 实例 colorSpace。

  使用完毕需要释放:

1 CGColorSpaceRelease(colorSpace);

  使用上面的 width、height、colorSpace 以及 kCGBitmapByteOrderDefault、kCGImageAlphaPremultipliedFirst,做参数创建一个 CGContextRef  实例 bmContext。

   bmContext 使用完毕需要释放:

1 CGContextRelease(bmContext);

  如果 bmContext 存在,使用 CGContextDrawImage,把上面获取到的 width 和 partialHeight、partialImageRef 做参数,写入到 bmContext 里面。然后释放 partialImageRef :

1 CGImageRelease(partialImageRef);

  使用 bmContext 和 CGBitmapContextCreateImage 创建一个新的 CGImageRef,并赋值给 partialImageRef。释放 bmContext。

  如果 bmContext 不存在,则直接释放 partialImageRef 并把它置为 nil。

   

  然后再往下:

  如果 partialImageRef 存在,则使用 partialImageRef 做参数根据不同的开发平台创建 UIImage。

  最主要区别是 iOS 平台、TV 平台、WATCH 平台,使用:

1 + (UIImage *)imageWithCGImage:(CGImageRef)cgImage scale:(CGFloat)scale orientation:(UIImageOrientation)orientation NS_AVAILABLE_IOS(4_0);

  创建 UIImage,需要一个表示图片方向的参数 orientation。MAC 平台则没有。

  使用 self.request.URL 获取图片的 key,调整图片的大小。

  根据  self.shouldDecompressImage 属性判断是否需要解压图片。

  然后释放 partialImageRef。

1 CGImageRelease(partialImageRef);

  然后调用 finished 为 NO 的下载完成的 block:

 1 - (void)callCompletionBlocksWithImage:(nullable UIImage *)image
 2                             imageData:(nullable NSData *)imageData
 3                                 error:(nullable NSError *)error
 4                              finished:(BOOL)finished {
 5     NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
 6     dispatch_main_async_safe(^{
 7         for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
 8             completedBlock(image, imageData, error, finished);
 9         }
10     });
11 }

  

  如果在代理方法最开始的时候,self.options  不是 SDWebImageDownloaderProgressiveDownload 或者 self.expectedSize 不大于 0 的时候:

  遍历执行下载进度的 block。

 

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {

    responseFromCached = NO; // If this method is called, it means the response wasn't read from cache
    NSCachedURLResponse *cachedResponse = proposedResponse;

    if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
        // Prevents caching of responses
        cachedResponse = nil;
    }
    if (completionHandler) {
        completionHandler(cachedResponse);
    }
}

  该方法用于响应的缓存设置,如果把回调的参数设置为 nil,那么就不会缓存响应,真正缓存的数据就是回调中的参数。

  

  NSURLSessionTaskDelegate

 1 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
 2     @synchronized(self) {
 3         self.dataTask = nil;
 4         dispatch_async(dispatch_get_main_queue(), ^{
 5             [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
 6             if (!error) {
 7                 [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
 8             }
 9         });
10     }
11     
12     if (error) {
13         [self callCompletionBlocksWithError:error];
14     } else {
15         if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
16             /**
17              *  See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash
18              *  Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response
19              *    and images for which responseFromCached is YES (only the ones that cannot be cached).
20              *  Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication
21              */
22             if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
23                 // hack
24                 [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
25             } else if (self.imageData) {
26                 UIImage *image = [UIImage sd_imageWithData:self.imageData];
27                 NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
28                 image = [self scaledImageForKey:key image:image];
29                 
30                 // Do not force decoding animated GIFs
31                 if (!image.images) {
32                     if (self.shouldDecompressImages) {
33                         if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
34 #if SD_UIKIT || SD_WATCH
35                             image = [UIImage decodedAndScaledDownImageWithImage:image];
36                             [self.imageData setData:UIImagePNGRepresentation(image)];
37 #endif
38                         } else {
39                             image = [UIImage decodedImageWithImage:image];
40                         }
41                     }
42                 }
43                 if (CGSizeEqualToSize(image.size, CGSizeZero)) {
44                     [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
45                 } else {
46                     [self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
47                 }
48             } else {
49                 [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
50             }
51         }
52     }
53     [self done];
54 }

  当图片下载完成之后的代理方法。

  把  self.dataTask 置为 nil。

  异步调取主线程,先发送一个 SDWebImageDownloadStopNotification 的通知,如果没有 error 则再发送一个 SDWebImageDownloadFinishNotification 的通知。

  如果有 error,则回调 image 是 nil,imageData 是 nil,error  是 error,finished 是YES 的 完成的 block。

1 - (void)callCompletionBlocksWithError:(nullable NSError *)error {
2     [self callCompletionBlocksWithImage:nil imageData:nil error:error finished:YES];
3 }

  如果没有 error,则执行图片正常下载完毕的方法:

  如果 self.callbackBlocks 里面 completed 的 count 大于 0:

  如果 self.options 是 SDWebImageDownloaderIgnoreCachedResponse 并且 responseFromCached 为真、并且 [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request] 存在,则直接执行 image 是 nil、imageData 是 nil、error 是 nil、Finished 为 YES 的完成的 block。

  其它的,如果 self.imageData 存在:

  使用 self.imageData 转化为图片 image。

  使用 self.request.URL 获取图片的 key。

  对 image 根据 key 做 缩放处理。

  不要强迫解码动效图片。

  如果不是动图,如果 self.shouldDecompressImage 是 YES,如果 self.options 是 SDWebImageDownloaderScaleDownLargeImages,且当前是 iOS 平台或者 TV 平台或者 WATCH 平台,对 image 进行压缩,并刷新  self.imageData 的数据。

  否则只对 image 进行解压。

  如果 image.size 是 CGSizeZero,则携带错误信息回调完成的 block:

1 [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]

  否则正常携带图片信息回调完成的 block。

  

  如果上面的 self.imageData 不存在:

  携带错误信息回调完成的 block:

1 [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]

   最后调用 done 方法。

 

 1 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
 2     
 3     NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
 4     __block NSURLCredential *credential = nil;
 5     
 6     if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
 7         if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) {
 8             disposition = NSURLSessionAuthChallengePerformDefaultHandling;
 9         } else {
10             credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
11             disposition = NSURLSessionAuthChallengeUseCredential;
12         }
13     } else {
14         if (challenge.previousFailureCount == 0) {
15             if (self.credential) {
16                 credential = self.credential;
17                 disposition = NSURLSessionAuthChallengeUseCredential;
18             } else {
19                 disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
20             }
21         } else {
22             disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
23         }
24     }
25     
26     if (completionHandler) {
27         completionHandler(disposition, credential);
28     }
29 }

  这个方法跟HTTPS有点关系,要想说明白这个方法究竟干了什么事? 需要对验证有点了解才行。

  当我们发出了一个请求,这个请求到达服务器后,假定服务器设置了需要验证。那么这个方法就会被调用。服务器会返回去一个NSURLAuthenticationChallenge。通过NSURLAuthenticationChallengeprotectionSpace,获取授权method。如果这个metho是服务器信任的, 那么我们就可以直接使用服务器返回的证书,当然,我们也可以使用自己的证书,其他情况都会被认为验证失败,当前请求将会被取消。当有了证书后,客户端就可以使用证书中的公钥对数据进行加密了。

 

参考链接:http://www.jianshu.com/p/662c09582d30

http://blog.csdn.net/book_1992/article/details/52133818

http://blog.csdn.net/lmyuanhang/article/details/48442073

http://blog.sina.com.cn/s/blog_7b9d64af0101cjci.html

http://www.cnblogs.com/linzhengbo/p/6126755.html

http://www.jianshu.com/p/4dcd6e4bdbf0 

END

posted @ 2017-06-01 05:29  鳄鱼不怕牙医不怕  阅读(486)  评论(0编辑  收藏  举报