第一篇第二篇大概是把下载图片缓存图片的这个逻辑走完了,里面涉及好多类。
罗列一下
UIView+WebCache
UIView+WebCacheOperation
UIImageView+WebCache
UIImage+MultiFormat
UIImage+GIF
SDWebImageManager
SDWebImageOperation 是个协议
SDWebImageDownloaderOperation
SDWebImageDownloader
SDWebImageDecoder
SDImageCacheConfig
SDImageCache
哇撒,几乎把所有的类都给包含进去了。
1.SDImageCache
我们先从数据缓存看起。
看一个类,先看数据结构(我认为的哈)
这就是数据结构,比较简单。public property 三个,private 四个,还有个成员变量
了解了结构,下面就看看初始化方法。
1.+ (nonnull instancetype)sharedImageCache;
2.- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns;
3.- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;
初始化方法有三个
第一个初始化
+ (nonnull instancetype)sharedImageCache {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
是个单例
- (instancetype)init {
return [self initWithNamespace:@"default"];
}
默认初始化调用 - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
最后都调用到了- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory 方法进行真正的初始化
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory {
if ((self = [super init])) {
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
// Create IO serial queue
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
_config = [[SDImageCacheConfig alloc] init];
// Init the memory cache
_memCache = [[AutoPurgeCache alloc] init];
_memCache.name = fullNamespace;
// Init the disk cache
if (directory != nil) {
_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
} else {
NSString *path = [self makeDiskCachePath:ns];
_diskCachePath = path;
}
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
#if SD_UIKIT
// Subscribe to app events
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
}
return self;
}
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace { NSArray*paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
return [paths[0] stringByAppendingPathComponent:fullNamespace];
}
初始化都干了啥事情呢
初始化 _ioQueue _config _memCache (这里是AutoPurgeCache)_diskCachePath 和 _fileManager 还有些通知。 _ioQueue 是串行队列
接着往下看其他的不是初始化的方法。
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace;
这个方法实现很简单,就是获取缓存默认路径
- (void)addReadOnlyCachePath:(nonnull NSString *)path
这个函数 就是初始话_customPaths 给self.customPaths 增加一个路径值。(初始化的时候是没有初始化_customPaths成员变量的)
1- (void)storeImage:(nullable UIImage *)image
forKey:(nullable NSString *)key
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
2- (void)storeImage:(nullable UIImage *)image
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
3.- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
这三个函数最终都是调用到了最后这个函数里面
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
if (!image || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
// if memory cache is enabled
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
if (toDisk) {
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
NSData *data = imageData;
if (!data && image) {
SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];
data = [image sd_imageDataAsFormat:imageFormatFromData];
}
[self storeImageDataToDisk:data forKey:key];
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
}
看这个函数实现,其实很简单,要是配置缓存到内存就缓存到内存,要是要写入disk中,切换到self.ioQueue 异步写入 到磁盘。写入成功执行回调block 要是参数error 直接调用block
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key;
这个函数是最终写入磁盘的函数,不过这个函数要求在 self.ioQueue 中操作,同步写入。
看到这里我们知道,SDWebImage 写入到磁盘的数据都是在self.ioQueue 中,单个队列管理。
- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;
查询操作
查询也需要切换到self.ioQueue 队列中执行。异步查询,好到回到主线程回调
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;
这个函数其实就是先去内存中找数据,要是没找到,那就生成一个NSOperation 并且返回,异步去self.ioQueue 队列去查询图片,要是找到了对图片处理缓存到内存中,在self.ioQueue队列回调完成block。(这里为什么要返回一个NSOpertion呢?估计是下载和在磁盘上查找同时进行)
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key
简单从缓存中查找key数据
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key
从磁盘是找找key ,找到如果配置到缓存,就缓存到内存中
- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key;
这个从缓存中找,先冲内存缓存找,找不到再到磁盘找
1- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion
2- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;
折两个函数一起看,都是缓存清除
要是配置缓存在内存,就从内存中移除图片,接着异步切换到self.ioQueue 队列中,对disk的数据进行删除。
- (void)clearMemory;
清理记忆内存的缓存图片
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion
异步切换到 self.ioQueue 队列中,对所有图片进行删除操作 删除成功执行回调block
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;
这个函数 也是异步切换到self.ioQueue 队列中执行 删除过期的文件 并且检查缓存的图片是否超过我们设定的硬盘图片大小,要是超过就按照时间排序,删除最老时候的图片,直到缓存在硬盘的图片大小比我们设定的小。我对这个函数比较感兴趣,搜索了下调用 。该函数在- (void)backgroundDeleteOldFiles 调用,而- (void)backgroundDeleteOldFiles 调用的时机是[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil]; 当应用进入后台的时候就会清理过期的图片,清理硬盘图片到我们需要的大小为止。
这里可以修改自己想要的磁盘图片管理。
- (NSUInteger)getSize;
这个是同步操作切换到self.ioQueue 获取磁盘图片大小。
- (NSUInteger)getDiskCount;
同步操作切换到self.ioQueue 获取磁盘图片数量
- (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock;
这个是异步获取图片的大小
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path
这个就是生成一个缓存路径
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key
默认缓存路径
这里就把SDImageCache 缓存给分析完毕
总结下
1:SDImageCache 管理者memoryCache 和 磁盘 上的图片
2:memoryCache 读取或者写入都是在当前线程执行
3:diskCache 都是在self.ioQueue队列执行
4:self.ioQueue 是串行队列,一个一个任务执行的。
5:清理缓存是在进入后台的时候进行的。
这里面涉及到两个类我们也瞅瞅AutoPurgeCache SDImageCacheConfig
2.SDImageCacheConfig
这个类全是属性。方法没有
@property (assign, nonatomic) BOOL shouldDecompressImages;
默认图片压缩
@property (assign, nonatomic) BOOL shouldDisableiCloud;
是否保存iCould
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
是否缓存到记忆内存
@property (assign, nonatomic) NSInteger maxCacheAge;
最大的缓存日期,默认七天
@property (assign, nonatomic) NSUInteger maxCacheSize;
最大缓存大小。默认是0 。0 代表不清理缓存。
3.AutoPurgeCache
AutoPurgeCache 继承NSCache
这个类就是NSCache 的功能,不过只是给这个类增加了一个接受内存警告的通知罢了。
4.SDWebImageDownloaderOperation
接下来我们看看网络请求部分
SDWebImageDownloaderOperation 继承NSOperation 是NSOperation 的子类。
继承NSOperation 的子类简单实用方式
官方文档:
For non-concurrent operations, you typically override only one method:
对于不是concurrent操作,你就覆盖Main 函数就行了。
If you are creating a concurrent operation, you need to override the following methods and properties at a minimum:
而对于concurrent操作,那么你需要覆盖这四个方法。
那什么是concurrent 操作呢?我认为哈,请高手指正对不对,这里指的是同步操作或者异步操作。同步操作比如是同步读取数据库,写入成功结束。而异步操作相当于异步写入数据库,等待回调,写入成功,接收回调操作结束。
那么测试试试吧。
测试代码
定义一个子类NSOperationConcurrentSub
#importtypedef void(^CompleteSubBlock)(void);
#define __weakSelf __weak typeof(self) weakSelf = self;
@interface NSOperationConcurrentSub : NSOperation
- (instancetype)initWithBlock:(CompleteSubBlock)block;
@end
#import "NSOperationConcurrentSub.h"
@interface NSOperationConcurrentSub()
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
@property (copy,nonatomic) CompleteSubBlock block;
@end
@implementation NSOperationConcurrentSub
@synthesize executing = _executing;
@synthesize finished = _finished;
- (instancetype)initWithBlock:(CompleteSubBlock)block
{
self = [super init];
if (self) {
self.block = block;
}
return self;
}
-(void)start{
if (self.isCancelled) {
self.finished = YES;
return;
}
__weakSelf
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue
, ^{
sleep(1);
if (weakSelf==nil) {
weakSelf.finished = YES;
return ;
}
weakSelf.block();
weakSelf.finished = YES;
});
}
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}
- (BOOL)isConcurrent {
return YES;
}
-(BOOL)isAsynchronous{
return YES;
}
@end
定义NSOperationNoConcurrentSub
#import#import "NSOperationConcurrentSub.h"
@interface NSOperationNoConcurrentSub : NSOperation
- (instancetype)initWithBlock:(CompleteSubBlock)block;
@end
#import "NSOperationNoConcurrentSub.h"
@interface NSOperationNoConcurrentSub()
@property (copy,nonatomic) CompleteSubBlock block;
@end
@implementation NSOperationNoConcurrentSub
- (instancetype)initWithBlock:(CompleteSubBlock)block
{
self = [super init];
if (self) {
self.block = block;
}
return self;
}
-(void)main{
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
__weakSelf
dispatch_async(aQueue
, ^{
sleep(1);
if (weakSelf==nil) {
return ;
}
weakSelf.block();
});
}
测试代码viewController
#import "ViewController.h"
#import "NSOperationConcurrentSub.h"
#import "NSOperationNoConcurrentSub.h"
@interface ViewController ()
@property (nonatomic ,strong)NSOperationQueue * queue;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSOperationQueue * queue = [[NSOperationQueue alloc]init];
self.queue = queue;
NSOperationConcurrentSub *sub = [[NSOperationConcurrentSub alloc]initWithBlock:^{
NSLog(@"NSOperationConcurrentSub");
}];
NSOperationNoConcurrentSub * nosub = [[NSOperationNoConcurrentSub alloc]initWithBlock:^{
NSLog(@"NSOperationNoConcurrentSub");
}];
[queue addOperation:sub];
[queue addOperation:nosub];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
输出结果是
2017-10-30 11:57:46.100736+0800 NSOpertionTest[47985:822761] NSOperationConcurrentSub
没有执行NSOperationNoConcurrentSub 中的block。说明no-concurrentSub 只能执行同步操作,操作中不能有分支。
而,NSOperationConcurrentSub 中的block执行了。说明,可以分叉执行。
而SDWebImage中的SDWebImageDownloaderOperation 因为里面有异步操作请求图片的过程,所以用的是concurrent方式。
网络请求是用NSURLSessionTask 任务形式的,网上讲这个怎么用的方式一大把,这里就不啰嗦了。
我们下面看这个类的结构
属性不少。接着我们看方法
先看初始化操作
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options NS_DESIGNATED_INITIALIZER;
- (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;
}
这里生成一个对象,一个初始化了九个成员变量。没啥可看的。
- (nonnull instancetype)init {
return [self initWithRequest:nil inSession:nil options:0];
}
这个是调用默认初始化。request 和 session 都是nil 。意思是没请求。不过这干嘛不来个log打印呢。
第二个public 方法是
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
- (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];
});
return callbacks;
}
这个就是给进度条加入到数组中。
第三个public 方法
- (BOOL)cancel:(nullable id)token
- (BOOL)cancel:(nullable id)token {
__block BOOL shouldCancel = NO;
dispatch_barrier_sync(self.barrierQueue, ^{
[self.callbackBlocks removeObjectIdenticalTo:token];
if (self.callbackBlocks.count == 0) {
shouldCancel = YES;
}
});
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 connection, 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 {
dispatch_barrier_async(self.barrierQueue, ^{
[self.callbackBlocks removeAllObjects];
});
self.dataTask = nil;
self.imageData = nil;
if (self.ownedSession) {
[self.ownedSession invalidateAndCancel];
self.ownedSession = nil;
}
}
取消做的事情比较多,
1。第一步是移除callbackBlocks 数组 相关数据
2.要是callbackBlocks 数组中的数量为0,调用cancel函数
3.cancel 函数加锁接着掉用cancelInternal 函数
4.检测operation 是否结束。没结束super调用 cancel
5.要是有dataTask 那么dataTask 调用cancel,并且发送通知
6.复位
public方法说完了。
当NSOperation 加入到NSOperationQueue 中,上面说到concurrent 会调用到start 方法。
所以我们这里有个默认的public方法。那就是 start
分析start方法
分段分析
第一段
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
就是要说operation 取消了,就reset下
第二段
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;
}
}];
}
要是配置后台运行下载,那么等后台下载结束后结束后台下载进程。
第三段
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
/**
* Create the session for this task
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
* method calls and completion handler calls.
*/
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
要说外界没有传入session 。那么我们就生成ownedSession ,别的就用unownedSession
第四段
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
用session生成task任务。 并且让operation 进入executing 状态
第五段
[self.dataTask resume];
启动任务下载
第六段
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"}]];
}
要说有任务就开始启动进度条。没有就回调error
第七段
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;
}
要是self.backgroundTaskId 不是 UIBackgroundTaskInvalid 那么就结束后台执行
这里有点疑问,假设开启了backgoundTask 任务,怎么在start 结束后直接将backgroundTask任务给关闭了。那不就是没有后台任务了。难道只是为了保证在任何没有杀死app的其他任何情况下都启动任务?
好了这就是网络请求发出去的过程。网络请求发出去肯定就要接收呀。
数据接收在 NSURLSessionDataDelegate, NSURLSessionTaskDelegate
SDwebImage 中
NSURLSessionTaskDelegate代理实现了两个
1.- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
2.- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
NSURLSessionDataDelegate 代理实现了三个
1.- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
2.- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
3.- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
这两个的代理还有很多,不过于研究。
这里这些代理的执行先后顺序是
1.- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
2.- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
3.- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
4.- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
5.- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
我们先看第一个执行的代理方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
} else {
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
disposition = NSURLSessionAuthChallengeUseCredential;
}
} else {
if (challenge.previousFailureCount == 0) {
if (self.credential) {
credential = self.credential;
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
}
if (completionHandler) {
completionHandler(disposition, credential);
}
}
这里是接受到网络连接后的第一个回调函数。好多新的类,妈呀。看着头晕。这都是什么鬼。没办法只能一个一个看下去了。NSURLSession 和 NSURLSessionTask 讲解的很多,这里不做说明,看NSURLAuthenticationChallenge 这是啥呢?
Most apps do not create authentication challenges themselves. However, you might need to create authentication challenge objects when adding support for custom networking protocols, as part of your customNSURLProtocolsubclasses.
这句话就是一般不需要鉴定,但是如果自己实现的协议,那么可能就有用了。
Instead, your app receives authentication challenges in variousNSURLSession,NSURLConnection, andNSURLDownloaddelegate methods, such asURLSession:task:didReceiveChallenge:completionHandler:. These objects provide the information you’ll need when deciding how to handle a server’s request for authentication. At the core of that authentication challenge is aprotection spacethat defines the type of authentication being requested, the host and port number, the networking protocol, and (where applicable) the authentication realm (a group of related URLs on the same server that share a single set of credentials).
鉴定的核心有个space 这个包含请求的host port 网络协议等等。
Your app responds to authentication challenges by providing anNSURLCredentialobject. The details depend on the API you are using and on the type of challenge.
我们响应应该通过NSURLCredential 对象respond。具体依赖api
At a high level, if you’re providing the user’s credentials to a server or proxy, theproposedCredentialmethod provides a credential that matches the criteria specified in the protection space, retrieved from theNSURLCredentialStorageclass handling the request (assuming such a credential exists).
If thepreviousFailureCountmethod returns 0 and the proposed credential exists, the proposed credential has not yet been tried, which means you should try it. If it returns a nonzero result, then the server has rejected the proposed credential, and you should use that credential to populate a password or certificate chooser dialog, then provide a new credential. You can create password-based credentials by calling thecredentialWithUser:password:persistence:method or create certificate-based credentials with thecredentialWithIdentity:certificates:persistence:.
If the authentication’s protection space uses theNSURLAuthenticationMethodServerTrustauthentication method, the request is asking you to verify the server’s authenticity. In this case, theproposedCredentialmethod provides a credential based on the certificates that the server provided as part of its initial TLS handshake. Most apps should request default handling for authentication challenges based on a server trust protection space, but if you need to override the default TLS validation behavior, you can do so as described inOverriding TLS Chain Validation Correctly.
previousFailureCount 为0 代表credential存在。不是0 ,服务器已经拒绝了。那就只能新建credential。
For more information about how URL sessions handle the different types of authentication challenges, seeNSURLSessionandURL Session Programming Guide.
官方文档原话,就是上面这样的。在哪呢? cmd + shift +0 ,搜索 NSURLAuthenticationChallenge。
上面只是大概翻译下。明白就好。返回来看代码。 讲解证书信任
这个方法是使用https 才会调用的,调用http是不会调用到这里的,这个只是证书验证过程。默认使用NSURLSessionAuthChallengePerformDefaultHandling 默认配置,要是配置可以允许无效证书,那么就创建一个NSURLCredential 并且信任他。
要是不信任 的证书,判断previousFailureCount =0 ,0代表服务器信任了。检查有没有self.credential ,没有就取消这个操作。
2.- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler{
//'304 Not Modified' is an exceptional one
if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode < 400 && ((NSHTTPURLResponse *)response).statusCode != 304)) {
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);
}
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;
//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.
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];
}
if (completionHandler) {
completionHandler(NSURLSessionResponseAllow);
}
}
第二个回调是接受响应体。
其实是分三部分处理的。code = 304的, code<=400 一下的,再就是剩下code其他的
。这里不对http请求code 码做单独解读,可以自行百度。
为啥code 304 单独处理呢?
304(未修改)自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容。
如果网页自请求者上次请求后再也没有更改过,您应将服务器配置为返回此响应(称为 If-Modified-Since HTTP 标头)。服务器可以告诉 Googlebot 自从上次抓取后网页没有变更,进而节省带宽和开销。
先看code 400以下,并且不是304的部分
首先从NSURLResponse读取接收数据长度,保存到_expectedSize 变量中,并且开始进度条从0开始调用。这里初始化_imageData 变量,保存response到变量_response中,再发一个通知。结束。
code =304 的直接 调用cancelInternal
其他的直接取消下载任务。
3- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{
[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);
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.
#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
// 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
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);
}
}
这个函数算是真正的下载了。要是有数据,这个函数至少要调用一次,把所有数据拼接到一起就是本次下载的图片数据。
那我们分析这个这个函数。
第一步,给self.imageData 追加数据
第二步,如果数据长度大于0 并且配置了进度条 。执行下面操作。
1. 获取当前数据的长度。
2 将当前数据转换成CGImageSourceRef。
3 检测变量width 和 height 都是0 的话,那么就读取图片的长和宽给变量赋值。也获取下图片的方向。
4 获取图片的长款还有当前数据长度小于接受数据总长度。执行下面操作。将CGImageSourceRef 转换成image,如果生成了CGimage,将图片处理成bitmap,接着生成UIimage。对图片进行一系列处理,然后返回。
第三步 :就是回调进度条。
- (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;
__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.
* 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
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];
}
}
}
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];
}
这个类我认为哈,关键点barrierQueue ,这个是干嘛用的。不用barrierQueue 行不行,搜索下这个类,与barrierQueue 关联在一起的是callbackBlocks 这个数组,所有的callbackBlocks 数组操作都是切换到barrierQueue 中的,有时候用dispatch_barrier_async 有时候用 dispatch_sync 还有时候用 dispatch_barrier_sync。
虽然讲解这三个 GCD 函数的demo博客很多,但是具体到这个线程中是什么状态呢?
我们需要做个demo 试验下。
测试代码
#import "NSOperationConcurrentSub.h"
@interface NSOperationConcurrentSub()
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
@property (copy,nonatomic) CompleteSubBlock block;
@end
@implementation NSOperationConcurrentSub
@synthesize executing = _executing;
@synthesize finished = _finished;
- (instancetype)initWithBlock:(CompleteSubBlock)block
{
self = [super init];
if (self) {
self.block = block;
}
return self;
}
-(void)start{
if (self.isCancelled) {
self.finished = YES;
return;
}
// [self ddd];
[self test];
}
-(void)test{
NSLog(@" queue begin");
dispatch_queue_t aQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t newQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(newQueue, ^{
sleep(8);
NSLog(@"new_dispatch_async");
});
dispatch_async(aQueue, ^{
sleep(3);
NSLog(@"dispatch_async");
});
dispatch_barrier_async(aQueue, ^{
sleep(2);
NSLog(@"dispatch_barrier_async");
});
dispatch_async(aQueue, ^{
sleep(1);
NSLog(@"dispatch_async");
});
dispatch_async(newQueue, ^{
NSLog(@"new_dispatch_async");
});
NSLog(@" queue end");
}
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}
- (BOOL)isConcurrent {
return YES;
}
-(BOOL)isAsynchronous{
return YES;
}
@end
#importtypedef void(^CompleteSubBlock)(void);
#define __weakSelf __weak typeof(self) weakSelf = self;
@interface NSOperationConcurrentSub : NSOperation
- (instancetype)initWithBlock:(CompleteSubBlock)block;
@end
#import "ViewController.h"
#import "NSOperationConcurrentSub.h"
@interface ViewController ()
@property (nonatomic ,strong)NSOperationQueue * queue;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSOperationQueue * queue = [[NSOperationQueue alloc]init];
self.queue = queue;
NSOperationConcurrentSub *sub = [[NSOperationConcurrentSub alloc]initWithBlock:^{
NSLog(@"NSOperationConcurrentSub");
}];
NSLog(@"begin");
[queue addOperation:sub];
sleep(1);
NSLog(@"end");
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
测试结果
2017-10-31 18:23:07.439753+0800 NSOpertionTest[38787:452080] begin
2017-10-31 18:23:07.440018+0800 NSOpertionTest[38787:452132] queue begin
2017-10-31 18:23:07.440162+0800 NSOpertionTest[38787:452132] queue end
2017-10-31 18:23:07.440198+0800 NSOpertionTest[38787:452124] new_dispatch_async
2017-10-31 18:23:08.440141+0800 NSOpertionTest[38787:452080] end
2017-10-31 18:23:10.444277+0800 NSOpertionTest[38787:452126] dispatch_async
2017-10-31 18:23:12.447935+0800 NSOpertionTest[38787:452126] dispatch_barrier_async
2017-10-31 18:23:13.451442+0800 NSOpertionTest[38787:452126] dispatch_async
2017-10-31 18:23:15.441778+0800 NSOpertionTest[38787:452125] new_dispatch_async
可以看出dispatch_barrier_async 管理的是自己的队列,不会影响其他的队列和当前线程。
将dispatch_barrier_async 改成dispatch_barrier_sync
结果是
2017-10-31 18:20:47.819449+0800 NSOpertionTest[38756:450698] begin
2017-10-31 18:20:47.819702+0800 NSOpertionTest[38756:450789] queue begin
2017-10-31 18:20:48.819873+0800 NSOpertionTest[38756:450698] end
2017-10-31 18:20:50.823113+0800 NSOpertionTest[38756:450787] dispatch_async
2017-10-31 18:20:52.824665+0800 NSOpertionTest[38756:450789] dispatch_barrier_async
2017-10-31 18:20:52.824860+0800 NSOpertionTest[38756:450789] queue end
2017-10-31 18:20:52.824875+0800 NSOpertionTest[38756:450788] new_dispatch_async
2017-10-31 18:20:53.827384+0800 NSOpertionTest[38756:450787] dispatch_async
2017-10-31 18:20:55.820566+0800 NSOpertionTest[38756:450786] new_dispatch_async
dispatch_barrier_sync 影响的是当前线程和传入的队列,对其他队列别影响。
那么返回sdwebimage 看源码这个队列啥意思。一共有四处使用这个队列
- (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];
});
return callbacks;
}
- (nullable NSArray*)callbacksForKey:(NSString *)key { __block NSMutableArray*callbacks = nil;
dispatch_sync(self.barrierQueue, ^{
// We need to remove [NSNull null] because there might not always be a progress block for each callback
callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
[callbacks removeObjectIdenticalTo:[NSNull null]];
});
return [callbacks copy]; // strip mutability here
}
- (BOOL)cancel:(nullable id)token {
__block BOOL shouldCancel = NO;
dispatch_barrier_sync(self.barrierQueue, ^{
[self.callbackBlocks removeObjectIdenticalTo:token];
if (self.callbackBlocks.count == 0) {
shouldCancel = YES;
}
});
if (shouldCancel) {
[self cancel];
}
return shouldCancel;
}
- (void)reset {
dispatch_barrier_async(self.barrierQueue, ^{
[self.callbackBlocks removeAllObjects];
});
self.dataTask = nil;
self.imageData = nil;
if (self.ownedSession) {
[self.ownedSession invalidateAndCancel];
self.ownedSession = nil;
}
}
而这四处操作的都是callbackBlocks 这个变量
接下来分析分析这四处地方的使用
在- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock方法中
使用的是 下列的方式
dispatch_barrier_async(self.barrierQueue, ^{
[self.callbackBlocks addObject:callbacks];
});
给数组增加一个对象。这说明数组中可以随时增加对象。
在- (nullable NSArray*)callbacksForKey:(NSString *)key方法中
dispatch_sync(self.barrierQueue, ^{
// We need to remove [NSNull null] because there might not always be a progress block for each callback
callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
[callbacks removeObjectIdenticalTo:[NSNull null]];
});
获取数据 是用的是dispatch_sync ,相当于一条线执行,执行完我在回到当前线程执行,获取数据。
第三个方法- (BOOL)cancel:(nullable id)token
dispatch_barrier_sync(self.barrierQueue, ^{
[self.callbackBlocks removeObjectIdenticalTo:token];
if (self.callbackBlocks.count == 0) {
shouldCancel = YES;
}
});
要是self.barrierQueue 队列中有任务那么就等待所有任务结束,结束后再执行我。并且当前线程也暂停。
而reset方法呢
dispatch_barrier_async(self.barrierQueue, ^{
[self.callbackBlocks removeAllObjects];
});
删除所有对象
在操作self.callbackBlocks 数组的时候,只有在增加数据或者删除所有数据的时候调用dispatch_barrier_async 而在获取数据的时候用的dispatch_sync 而在删除某个数据的时候用的是 dispatch_barrier_sync 。
为啥这样呢?我认为是dispatch_barrier_sync 是cancel 方法,要取消当前的操作,所以当前线程就没有必要执行其他操作了。执行到这里行了。取消线程,并且依赖dispatch_barrier_sync block中执行过程中的数据。
而增加或者删除数组元素操作,必须要用dispatch_barrier_async 而不是dispatch_async ,保证数据的加入和删除是有顺序的。