前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >iOS网络——NSURLSession详解及SDWebImage源码解析你要知道的NSURLSession都在这里

iOS网络——NSURLSession详解及SDWebImage源码解析你要知道的NSURLSession都在这里

作者头像
WWWWDotPNG
发布2018-04-10 11:51:57
2.8K0
发布2018-04-10 11:51:57
举报
文章被收录于专栏:iOS技术杂谈iOS技术杂谈

你要知道的NSURLSession都在这里

转载请注明出处 https://cloud.tencent.com/developer/user/1605429

本系列文章主要讲解iOS中网络请求类NSURLSession的使用方法进行详解,同时也会以此为扩展,讲解SDWebImage中图片下载功能的源码分析,讲解AFNetworking相关源码分析。本系列文章主要分为以下几篇进行讲解,读者可按需查阅。

  • iOS网络——NSURLSession详解及SDWebImage源码解析
  • iOS网络——SDWebImage SDImageDownloader源码解析
  • iOS网络——AFNetworking AFURLSessionManager源码解析
  • iOS网络——AFNetworking AFHttpSessionManager源码解析

NSURLSession的基础使用

NSURLSessioniOS7时就推出了,为了取代NSURLConnection,在iOS9NSURLConnection被废弃了,包括SDWebImageAFNetworking3也全面使用NSURLSession作为基础的网络请求类了。NSURLSession和服务端使用的session是完全不同的两个东西不要弄混淆了,NSURLSession工作在OSI 七层模型的会话层,会话层之下的所有工作,系统都已经帮我们做好了,所以这里的Session也可以理解为会话。NSURLSession相比于NSURLConnection来说提供的功能更加丰富,它支持HTTP2.0,提供了丰富的类来支持GET/POST请求、支持后台下载和上传,可将文件直接下载到磁盘的沙盒中。

NSURLSession的使用非常方便,先看一个最简单的栗子:

代码语言:javascript
复制
- (void)viewWillAppear:(BOOL)animated
{
    NSURLSession *session = [NSURLSession sharedSession];
    NSURL *url = [NSURL URLWithString:@"127.0.0.1:8080/login?username=cjm&password=cjmcjmcjm"];
    NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"%@ %@ %@ %@", data, response, error, [NSThread currentThread]);
    }];
    [task resume];
}

上述这个栗子发起了一次GET请求,为了方便使用,NSURLSession提供了一个单例的方法来获取一个全局共享的session对象,当然也可以自行创建,接下来通过这个session对象构造了一个请求任务的封装,即NSURLSessionDataTask类的对象,这个类是NSURLSessionTask的子类,主要用于进行一些比较简短数据的获取,通常用于发送GET/POST请求,默认发起GET请求,如果需要发起POST请求需要额外的操作,下面会讲。创建的任务封装默认是挂起状态的,所以为了启动网络请求,调用其resume方法即可开始执行请求,当任务完成时就会执行上述回调块,当然也可以使用代理的方式监听网络请求。

这样看来它的使用真的很方便,并且默认会自动开启多线程异步执行,上面栗子的回调块中输出了当前线程可以看出并不是主线程,所以在回调中如果要进行UI的更新操作需要放到主线程中执行,相比使用NSURLConnection的各种坑,使用NSURLSession更方便并且它是线程安全的。Foundation框架为我们提供了四种任务封装的类,每一种都提供了不同的功能,具体类图如下:

NSURLSessionTask类图

NSURLSessionTask类似抽象类不提供网络请求的功能,具体实现由其子类实现,上面的栗子使用的就是NSURLSessionDataTask主要用来获取一些简短的数据,如发起GET/POST请求,NSURLSessionDownloadTask用于下载文件,它提供了很多功能,默认支持将文件直接下载至磁盘沙盒中,就可以避免占用过多内存的问题,NSURLSessionUploadTask用于上传文件,NSURLSessionStreamTask提供了以流的形式读写TCP/IP流的功能,可以实现异步读写的功能。前面三个类使用的比较频繁,在SDWebImage中用于下载图片的具体任务是交由NSURLSessionDataTask完成,由于缓存策略的问题,图片一般都较小,可能不需要将图片保存至磁盘,所以也就不需要使用NSURLSessionDownloadTask,关于SDWebImage的缓存策略可以查阅本博客另一篇文章iOS缓存 NSCache详解及SDWebImage缓存策略源码分析

NSURLSession相关的类也提供了丰富的代理来监听具体请求的状态,相关代理协议的类图如下所示:

NSURLSessionDelegate类图

代理具体的回调方法可以自行查阅相关接口声明。

接下来再举一个发送POST请求的栗子:

代码语言:javascript
复制
- (void)viewWillAppear:(BOOL)animated
{
    //创建NSURL的请求路径URL
    NSURL *url = [NSURL URLWithString:@"http://127.0.0.1:8080/login"];
    //创建一个可变的request对象
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    //修改请求方式为POST方法,默认是GET方法
    [request setHTTPMethod:@"POST"];
    //设置请求体,即添加post请求数据
    [request setHTTPBody:[@"username=cjm&password=cjmcjmcjm" dataUsingEncoding:NSUTF8StringEncoding]];
    //使用单例的全局共享的session对象
    NSURLSession *session = [NSURLSession sharedSession];
    //使用上述request构造一个任务对象
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"%@ %@ %@ %@", data, response, error, [NSThread currentThread]);
    }];
    //启动任务
    [task resume];
}

上面的栗子就是一个发送POST请求的栗子,这里使用了可变的request请求对象,然后修改其请求方法,编码请求体加入参数,使用也很方便,请求完成后会执行回调块,可以根据服务端返回的数据转换为JSON数据或者HTML等格式。

举一个下载文件的栗子:

代码语言:javascript
复制
- (void)viewWillAppear:(BOOL)animated
{
    //创建文件地址URL
    NSURL *url = [NSURL URLWithString:@"http://mirrors.hust.edu.cn/apache/tomcat/tomcat-9/v9.0.1/bin/apache-tomcat-9.0.1.tar.gz"];
    //获取单例全局共享的session对象
    NSURLSession *session = [NSURLSession sharedSession];
    //创建一个下载任务
    NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
       //这个location就是下载到磁盘的位置,默认是在沙盒tmp文件夹中国
        NSLog(@"Location %@", location);
        //tmp文件夹在关闭app后会自动删除,有需要可以使用NSFileManager将该文件转移到沙盒其他目录下      
        //NSFileManager *fileManager = [NSFileManager defaultManager];
        //[fileManager copyItemAtPath:location.path toPath:@"" error:nil];  
    }];
    //启动任务
    [downloadTask resume];
}

前面讲过,我们可以使用代理的方式来监听网络请求的状态,也罗列的代理协议的继承关系,但是我们无法为全局共享的NSURLSession对象设置代理,也就不能监听其网络请求,原因很简单,委托对象只有一个,而全局共享的单例对象可能有很多类都在使用。所以只能自己创建一个NSURLSession对象并在初始化方法中指定其委托对象,具体栗子如下:

代码语言:javascript
复制
- (void)viewWillAppear:(BOOL)animated
{
    //创建一个代理方法执行的队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //设置队列的名称
    queue.name = @"MyDelegateQueue";
    //创建一个session,运行在默认模式下
    //设置代理和代理方法执行队列
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:queue];
    //创建一个任务
    NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
    //启动任务
    [task resume];
    
}

//一次请求只会执行一次,收到服务端响应
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
    NSLog(@"Receive Response %@ %@ %@", response, [NSThread currentThread], [NSOperationQueue currentQueue]);
    /*
    如果要实现这个代理方法一定要执行这个回调块
    如果不执行这个回调块默认就会取消任务,后面就不会从服务器获取数据了
    */
    if (completionHandler) {
        completionHandler(NSURLSessionResponseAllow);
    }
}

//从服务端收到数据,一次请求中可能执行多次
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data
{
    NSLog(@"Receive Data %@",  [NSOperationQueue currentQueue]);
}

//任务完成后的回调
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error
{
    NSLog(@"Complete %@ %@", error, [NSOperationQueue currentQueue]);
}

上面栗子的输出结果如下:

代码语言:javascript
复制
Receive Response <NSHTTPURLResponse: 0x1c4223260> { URL: http://www.baidu.com/ } { status code: 200, headers {
    BDPAGETYPE = 1;
    BDQID = 0x93412d1800009b30;
    BDUSERID = 0;
    "Cache-Control" = private;
    Connection = "Keep-Alive";
    "Content-Encoding" = gzip;
    "Content-Type" = "text/html; charset=utf-8";
    "Cxy_all" = "baidu+f27780376a7d65c99c3592e1ecb801b0";
    Date = "Wed, 01 Nov 2017 08:12:34 GMT";
    Expires = "Wed, 01 Nov 2017 08:12:00 GMT";
    Server = "BWS/1.1";
    "Set-Cookie" = "BDSVRTM=0; path=/, BD_HOME=0; path=/, H_PS_PSSID=1435_21081_17001_24879_22160; path=/; domain=.baidu.com";
    "Transfer-Encoding" = Identity;
    Vary = "Accept-Encoding";
    "X-Powered-By" = HPHP;
    "X-UA-Compatible" = "IE=Edge,chrome=1";
} } <NSThread: 0x1c0461980>{number = 4, name = (null)} <NSOperationQueue: 0x1c0223c60>{name = 'MyDelegateQueue'}
Receive Data <NSOperationQueue: 0x1c0223c60>{name = 'MyDelegateQueue'}
Receive Data <NSOperationQueue: 0x1c0223c60>{name = 'MyDelegateQueue'}
Receive Data <NSOperationQueue: 0x1c0223c60>{name = 'MyDelegateQueue'}
Receive Data <NSOperationQueue: 0x1c0223c60>{name = 'MyDelegateQueue'}
Complete (null) <NSOperationQueue: 0x1c0223c60>{name = 'MyDelegateQueue'

从输出结果看代理方法都是在子线程中执行,执行的队列也是我们创建的队列,如果需要在主线程中执行代理发现就将代理队列设置为主队列即可。

自定义创建NSURLSession对象是为了监听由该session发起的网络请求的执行状态,代理方法比较多,上述栗子只罗列了三个常用的方法,有兴趣的读者可自行实验。上面的栗子需要注意的就是在创建NSURLSession对象时传入的代理对象,NSURLSession会持有一个强引用,所以这里很有可能会产生引用循环的问题,为了打破循环需要在合适的地方调用其invalidateAndCancel方法或finishTasksAndInvalidate方法,如析构函数dealloc等。invalidateAndCancel方法会直接取消请求然后释放代理对象,finishTasksAndInvalidate方法等请求完成之后才会释放代理对象。

值得注意的就是didReceiveResponse:这个代理方法,如果实现这个方法在发现返回的响应没有问题的情况下一定要手动触发回调块,否则NSURLSession默认就会取消任务,也就不会再从服务端获取数据,后面的回调方法都不会再执行,我在第一次使用NSURLSession的时候没有仔细查看官方文档导致后面几个回调方法一直没有执行,所以在实现一个回调方法时一定要弄懂每一个参数的意义,就可以避免很多坑了。读者可以自行实验不触发回调块看看结果。

Foundation框架提供了三种NSURLSession的运行模式,即三种NSURLSessionConfiguration会话配置,defaultSessionConfiguration默认Session运行模式,使用该配置默认使用磁盘缓存网络请求相关数据如cookie等信息。ephemeralSessionConfiguration临时Session运行模式,不缓存网络请求的相关数据到磁盘,只会放到内存中使用。backgroundSessionConfiguration后台Session运行模式,如果需要实现在后台继续下载或上传文件时需要使用该会话配置,需要配置一个唯一的字符串作为区分。同时,NSURLSessionConfiguration还可以配置一些其他信息,如缓存策略、超时时间、是否允许蜂窝网络访问等信息。

SDWebImage SDWebImageDownloaderOperation源码解析

经过前文NSURLSession的讲解,我们已经掌握了NSURLSession的基础使用方法,接下来本文将讲解SDWebImage关于图片下载的部分,这部分需要读者掌握NSOpeartionGCD等知识,有疑问的读者可以阅读本博客相关文章iOS多线程——你要知道的NSOperation都在这里以及iOS多线程——你要知道的GCD都在这里

SDWebImage图片下载使用了NSURLSession来进行网络数据的处理,看一下官方SDWebImage的时序图:

SDWebImgae时序图

与图片下载相关的是第六步,调用SDImageDownloader的方法来进行图片的下载,SDImageDownloader负责管理所有的下载任务,具体的下载任务由SDImageDownloaderOperation类负责,本文首先讲解的内容正是SDWebImageDownloaderOperation类。

首先看一下这个类的头文件相关声明:

代码语言:javascript
复制
//声明一系列通知的名称
FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadStartNotification;
FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadReceiveResponseNotification;
FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadStopNotification;
FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadFinishNotification;

/*
SDWebImageDownloaderOperationInterface协议
开发者可以实现自己的下载操作只需要实现该协议即可
*/
@protocol SDWebImageDownloaderOperationInterface<NSObject>

//初始化函数,根据指定的request、session和下载选项创建一个下载任务
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options;

//添加进度和完成后的回调块
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

//是否压缩图片的setter和getter
- (BOOL)shouldDecompressImages;
- (void)setShouldDecompressImages:(BOOL)value;

//NSURLCredential的setter和getetr
- (nullable NSURLCredential *)credential;
- (void)setCredential:(nullable NSURLCredential *)value;

@end

/*
SDWebImageDownloaderOperation类继承自NSOperation
并遵守了四个协议
SDWebImageOperation协议只有一个cancel方法
*/
@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageDownloaderOperationInterface, SDWebImageOperation, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>

//下载任务的request
@property (strong, nonatomic, readonly, nullable) NSURLRequest *request;

//执行下载操作的下载任务
@property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask;

/*
是否压缩图片
上面的协议需要实现这个属性的getter和setter方法
只需要声明一个属性就可以遵守上面两个方法了
*/
@property (assign, nonatomic) BOOL shouldDecompressImages;

//废弃了的属性
@property (nonatomic, assign) BOOL shouldUseCredentialStorage __deprecated_msg("Property deprecated. Does nothing. Kept only for backwards compatibility");

//https需要使用的凭证
@property (nonatomic, strong, nullable) NSURLCredential *credential;

//下载时配置的相关内容
@property (assign, nonatomic, readonly) SDWebImageDownloaderOptions options;

//需要下载的文件的大小
@property (assign, nonatomic) NSInteger expectedSize;

//连接服务端后的收到的响应
@property (strong, nonatomic, nullable) NSURLResponse *response;

//初始化方法需要下载文件的request、session以及下载相关配置选项
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options NS_DESIGNATED_INITIALIZER;

/*
添加一个进度回调块和下载完成后的回调块
返回一个token,用于取消这个下载任务,这个token其实是一个字典,后文会讲
*/
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

/*
这个方法不是用来取消下载任务的,而是删除前一个方法添加的进度回调块和下载完成回调块
当所有的回调块都删除后,下载任务也会被取消,具体实现在.m文件中有讲解
需要传入上一个方法返回的token,即回调块字典
*/
- (BOOL)cancel:(nullable id)token;

@end

上述头文件声明中定义了一个协议,开发者就可以不使用SDWebImage提供的下载任务类,而可以自定义相关类,只需要遵守协议即可,SDWebImageDownloaderOperation类也遵守了该协议,该类继承自NSOperation主要是为了将任务加进并发队列里实现多线程下载多张图片,真正实现下载操作的是NSURLSessionTask类的子类,这里就可以看出SDWebImage使用NSURLSession实现下载图片的功能。

接下来看一下.m文件的具体源码:

代码语言:javascript
复制
//相关通知的名称
NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
NSString *const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification";
NSString *const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification";
NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification";

//进度回调块和下载完成回调块的字符串类型的key
static NSString *const kProgressCallbackKey = @"progress";
static NSString *const kCompletedCallbackKey = @"completed";

//定义了一个可变字典类型的回调块集合,这个字典key的取值就是上面两个字符串
typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;

上述先定义了一些全局变量和数据类型。

代码语言:javascript
复制
@interface SDWebImageDownloaderOperation ()

/*
回调块数组,数组内的元素即为前面自定义的数据类型
通过名称不难猜测,上述自定义字典的value就是回调块了
*/
@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;

/*
继承NSOperation需要定义executing和finished属性
并实现getter和setter,手动触发KVO通知
*/
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;

//可变NSData数据,存储下载的图片数据
@property (strong, nonatomic, nullable) NSMutableData *imageData;

//缓存的图片数据
@property (copy, nonatomic, nullable) NSData *cachedData;

/*
这里是weak修饰的NSURLSession属性
作者解释到unownedSession有可能不可用,因为这个session是外面传进来的,由其他类负责管理这个session,本类不负责管理
这个session有可能会被回收,当不可用时使用下面那个session
*/
@property (weak, nonatomic, nullable) NSURLSession *unownedSession;

/*
strong修饰的session,当上面weak的session不可用时,需要创建一个session
这个session需要由本类负责管理,需要在合适的地方调用*invalid*方法打破引用循环
*/
@property (strong, nonatomic, nullable) NSURLSession *ownedSession;

//NSURLSessionTask具体的下载任务
@property (strong, nonatomic, readwrite, nullable) NSURLSessionTask *dataTask;

//一个队列
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t barrierQueue;

//iOS上支持在后台下载时需要一个identifier
#if SD_UIKIT
@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
#endif

//这个解码器在图片没有完全下载完成时也可以解码展示部分图片
@property (strong, nonatomic, nullable) id<SDWebImageProgressiveCoder> progressiveCoder;

@end

上面的代码定义了一些属性,其中比较重要的就是unownedSessionownedSession需要读者理解,其次,关于NSOperation相关知识不再赘述了,不理解的读者可以阅读本博客相关文章。上面的代码还定义了一个队列,在前面分析SDWebImage缓存策略的源码时它也用到了一个串行队列,通过串行队列就可以避免竞争条件,可以不需要手动加锁和释放锁,简化编程。还可以发现它定义了一个NSURLSessionTask属性,所以具体的下载任务一定是交由其子类完成的。

继续看源码:

代码语言:javascript
复制
@implementation SDWebImageDownloaderOperation

@synthesize executing = _executing;
@synthesize finished = _finished;

//初始化函数,直接返回下面的初始化构造函数
- (nonnull instancetype)init {
    return [self initWithRequest:nil inSession:nil options:0];
}

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options {
    if ((self = [super init])) {
        _request = [request copy];
        _shouldDecompressImages = YES;
        _options = options;
        _callbackBlocks = [NSMutableArray new];
        _executing = NO;
        _finished = NO;
        _expectedSize = 0;
        _unownedSession = session;
        _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}

//析构函数
- (void)dealloc {
    SDDispatchQueueRelease(_barrierQueue);
}

合成存取了executingfinished属性,接下来就是两个初始化构造函数,进行了相关的初始化操作,注意看,在初始化方法中将传入的session赋给了unownedSession,所以这个session是外部传入的,本类就不需要负责管理它,但是它有可能会被释放,所以当这个session不可用时需要自己创建一个新的session并自行管理,上面还创建了一个并发队列,但这个队列都是以dispatch_barrier_(a)sync函数来执行,所以在这个并发队列上具体的执行方式还是串行,因为队列会被阻塞,在析构函数中释放这个队列,有不懂的读者可以阅读GCD相关文章。

代码语言:javascript
复制
//添加进度回调块和下载完成回调块
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    //创建一个<NSString,id>类型的可变字典,value为回调块
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    //如果进度回调块存在就加进字典里,key为@"progress"
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    //如果下载完成回调块存在就加进字典里,key为@"completed"
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    //使用dispatch_barrier_async方法异步方式不阻塞当前线程,但阻塞并发对列,串行执行添加进数组的操作
    dispatch_barrier_async(self.barrierQueue, ^{
        [self.callbackBlocks addObject:callbacks];
    });
    //返回的token其实就是这个字典
    return callbacks;
}

//通过key获取回调块数组中所有对应key的回调块
- (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
    __block NSMutableArray<id> *callbacks = nil;
    //同步方式执行,阻塞当前线程也阻塞队列
    dispatch_sync(self.barrierQueue, ^{
        callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
        //通过valueForKey方法如果字典中没有对应key会返回null所以需要删除为null的元素
        [callbacks removeObjectIdenticalTo:[NSNull null]];
    });
    return [callbacks copy];    // strip mutability here
}

//前文讲过的取消方法
- (BOOL)cancel:(nullable id)token {
    __block BOOL shouldCancel = NO;
    //同步方法阻塞队列阻塞当前线程也阻塞队列
    dispatch_barrier_sync(self.barrierQueue, ^{
        //根据token删除数组中的数据,token就是key为string,value为block的字典
        //删除的就是数组中的字典元素
        [self.callbackBlocks removeObjectIdenticalTo:token];
        //如果回调块数组长度为0就真的要取消下载任务了,因为已经没有人来接收下载完成和下载进度的信息,下载完成也没有任何意义
        if (self.callbackBlocks.count == 0) {
            shouldCancel = YES;
        }
    });
    //如果要真的要取消任务就调用cancel方法
    if (shouldCancel) {
        [self cancel];
    }
    return shouldCancel;
}

上面三个方法主要就是往一个字典类型的数组中添加回调块,这个字典最多只有两个key-value键值对,数组中可以有多个这样的字典,每添加一个进度回调块和下载完成回调块就会把这个字典返回作为token,在取消任务方法中就会从数组中删除掉这个字典,但是只有当数组中的回调块字典全部被删除完了才会真正取消任务。

代码语言:javascript
复制
//重写NSOperation类的start方法,任务添加到NSOperationQueue后会执行该方法,启动下载任务
- (void)start {
    /*
    同步代码块,防止产生竞争条件?
    其实这里我并不懂为什么要加这个同步代码块
    NSOperation子类加进NSOperationQueue后会自行调用start方法,并且只会执行一次,不太理解为什么需要加这个,懂的读者希望不吝赐教
    */
    @synchronized (self) {
        
        //判断是否取消了下载任务
        if (self.isCancelled) {
            //如果取消了就设置finished为YES,调用reset方法
            self.finished = YES;
            [self reset];
            return;
        }

        //iOS里支持可以在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
        //根据配置的下载选项获取网络请求的缓存数据
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
            // Grab the cached data for later check
            NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request];
            if (cachedResponse) {
                self.cachedData = cachedResponse.data;
            }
        }
        
        NSURLSession *session = self.unownedSession;
        //判断unownedSession是否为nil
        if (!self.unownedSession) {
            //为空则自行创建一个NSURLSession对象
            //相关知识前文也讲解过了,session运行在默认模式下
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            //超时时间15s
            sessionConfig.timeoutIntervalForRequest = 15;
            //delegateQueue为nil,所以回调方法默认在一个子线程的串行队列中执行
            self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            //局部变量赋值
            session = self.ownedSession;
        }
        //使用可用的session来创建一个NSURLSessionDataTask类型的下载任务
        self.dataTask = [session dataTaskWithRequest:self.request];
        //设置NSOperation子类的executing属性,标识开始下载任务
        self.executing = YES;
    }
    //NSURLSessionDataTask任务开始执行
    [self.dataTask resume];
    
    //如果这个NSURLSessionDataTask不为空即开启成功
    if (self.dataTask) {
        //遍历所有的进度回调块并执行
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        /*
        在主线程中发送通知,并将self传出去
        在什么线程发送通知,就会在什么线程接收通知
        为了防止其他监听通知的对象在回调方法中修改UI,这里就需要在主线程中发送通知
        */
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
        });
    } else {
        //如果创建NSURLSessionDataTask失败就执行失败的回调块
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
    }

//iOS后台下载相关
#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
}

上面这个函数就是重写了NSOperation类的start方法,当NSOperation类的子类添加进NSOperationQueue队列中线程调度后就会执行上述方法,上面这个方法也比较简单,主要就是判断session是否可用然后决定是否要自行管理一个NSURLSession对象,接下来就使用这个session创建一个NSURLSessionDataTask对象,这个对象是真正执行下载和服务端交互的对象,接下来就开启这个下载任务然后进行通知和回调块的触发工作,很简单的逻辑。

代码语言:javascript
复制
//SDWebImageOperation协议的cancel方法,取消任务,调用cancelInternal方法
- (void)cancel {
    @synchronized (self) {
        [self cancelInternal];
    }
}

//真的取消下载任务的方法
- (void)cancelInternal {
    //如果下载任务已经结束了直接返回
    if (self.isFinished) return;
    //调用NSOperation类的cancel方法,即,将isCancelled属性置为YES
    [super cancel];
    
    //如果NSURLSessionDataTask下载图片的任务存在
    if (self.dataTask) {
        //调用其cancel方法取消下载任务
        [self.dataTask cancel];
        //在主线程中发出下载停止的通知
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:weakSelf];
        });

        //设置两个属性的值
        if (self.isExecuting) self.executing = NO;
        if (!self.isFinished) self.finished = YES;
    }
    //调用reset方法
    [self reset];
}

//下载完成后调用的方法
- (void)done {
    //设置finished为YES executing为NO
    self.finished = YES;
    self.executing = NO;
    //调用reset方法
    [self reset];
}

- (void)reset {
    //删除回调块字典数组的所有元素
    __weak typeof(self) weakSelf = self;
    dispatch_barrier_async(self.barrierQueue, ^{
        [weakSelf.callbackBlocks removeAllObjects];
    });
    //NSURLSessionDataTask对象置为nil,等待回收
    self.dataTask = nil;
    
    //获取代理方法执行的队列
    NSOperationQueue *delegateQueue;
    //如果unownedSession可用就从它里面获取
    if (self.unownedSession) {
        delegateQueue = self.unownedSession.delegateQueue;
    } else {
        //不可用就从ownedSession中拿
        delegateQueue = self.ownedSession.delegateQueue;
    }
    //如果有代理方法执行队列
    if (delegateQueue) {
        //assert一下,这个队列必须为串行队列
        NSAssert(delegateQueue.maxConcurrentOperationCount == 1, @"NSURLSession delegate queue should be a serial queue");
        //将image数据置为nil
        [delegateQueue addOperationWithBlock:^{
            weakSelf.imageData = nil;
        }];
    }
    //如果ownedSession存在,就需要我们手动调用invalidateAndCancel方法打破引用循环
    if (self.ownedSession) {
        [self.ownedSession invalidateAndCancel];
        self.ownedSession = nil;
    }
}

//NSOperation子类finished属性的stter
- (void)setFinished:(BOOL)finished {
    //手动触发KVO通知
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

//NSOperation子类finished属性的stter
- (void)setExecuting:(BOOL)executing {
    //手动触发KVO通知
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

//重写NSOperation方法,标识这是一个并发任务
- (BOOL)isConcurrent {
    return YES;
}

上面几个方法就是与NSOperation有关了,用于取消下载任务和设置相关属性值,具体作用就不再赘述了。

代码语言:javascript
复制
#pragma mark NSURLSessionDataDelegate
//NSURLSessionDataDelegate代理方法

//收到服务端响应,在一次请求中只会执行一次
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    //根据http状态码判断是否成功响应,需要注意的是304认为是异常响应
    //如果响应正常
    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);
        }
        //创建imageData可变数据类型,大小就为响应返回的文件大小
        self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
        //将response赋值到成员变量
        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;
        
        //如果是304则直接取消下载任务
        if (code == 304) {
            [self cancelInternal];
        } else {
            //其他则取消NSURLSessionDataTask任务,并触发URLSession:task:didCompleteWithError:代理方法
            [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]];
        //执行done方法
        [self done];
    }
    //如果有回调块就执行
    if (completionHandler) {
        completionHandler(NSURLSessionResponseAllow);
    }
}

//收到数据的回调方法,可能执行多次
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    //向可变数据中添加接收到的数据
    [self.imageData appendData:data];
    
    //如果下载选项需要支持progressive下载,即展示已经下载的部分,并且响应中返回的图片大小大于0
    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
        //复制data数据
        NSData *imageData = [self.imageData copy];
        //获取已经下载了多大的数据
        const NSInteger totalSize = imageData.length;
        //判断是否已经下载完成
        BOOL finished = (totalSize >= self.expectedSize);
        //如果这个解码器不存在就创建一个
        if (!self.progressiveCoder) {
            // We need to create a new instance for progressive decoding to avoid conflicts
            for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) {
                if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
                    [((id<SDWebImageProgressiveCoder>)coder) canIncrementallyDecodeFromData:imageData]) {
                    self.progressiveCoder = [[[coder class] alloc] init];
                }
            }
        }
        //将数据交给解码器返回一个图片
        UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
        //如果图片存在
        if (image) {
            //通过URL获取缓存的key
            NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
            //缩放图片,不同平台图片大小计算方法不同,所以需要处理与喜爱
            image = [self scaledImageForKey:key image:image];
            //是否需要压缩
            if (self.shouldDecompressImages) {
                //压缩
                image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
            }
            //触发回调块回传这个图片
            [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
        }
    }
    //调用进度回调块并触发进度回调块
    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
    }
}

//如果要缓存响应时回调该方法
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
    
    NSCachedURLResponse *cachedResponse = proposedResponse;
    //如果request的缓存策略是不缓存本地数据就设置为nil
    if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
        // Prevents caching of responses
        cachedResponse = nil;
    }
    //调用回调块
    if (completionHandler) {
        completionHandler(cachedResponse);
    }
}

上面几个方法就是在接收到服务端响应后进行一个处理,判断是否是正常响应,如果是正常响应就进行各种赋值和初始化操作,并触发回调块,进行通知等操作,如果不是正常响应就结束下载任务。接下来的一个比较重要的方法就是接收到图片数据的处理,接收到数据后就追加到可变数据中,如果需要在图片没有下载完成时就展示部分图片,需要进行一个解码的操作然后调用回调块将图片数据回传,接着就会调用存储的进度回调块来通知现在的下载进度,回传图片的总长度和已经下载长度的信息。

代码语言:javascript
复制
#pragma mark NSURLSessionTaskDelegate
//下载完成或下载失败时的回调方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    /*
    又是一个同步代码块...有点不解,望理解的读者周知
    SDWebImage下载的逻辑也挺简单的,本类SDWebImageDownloaderOperation是NSOperation的子类
    所以可以使用NSOperationQueue来实现多线程下载
    但是每一个Operation类对应一个NSURLSessionTask的下载任务
    也就是说,SDWebImageDownloader类在需要下载图片的时候就创建一个Operation,
    然后将这个Operation加入到OperationQueue中,就会执行start方法
    start方法会创建一个Task来实现下载
    所以整个下载任务有两个子线程,一个是Operation执行start方法的线程来开启Task的下载任务
    一个是Task的线程来执行下载任务
    Operation和Task是一对一的关系,应该不会有竞争条件产生呀?
    */
    @synchronized(self) {
        //置空
        self.dataTask = nil;
        //主线程根据error是否为空发送对应通知
        __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];
            }
        });
    }
    
    //如果error存在,即下载过程中有我entity
    if (error) {
        //触发对应回调块
        [self callCompletionBlocksWithError:error];
    } else {
        //下载成功
        //判断下载完成回调块个数是否大于0
        if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
            //获取不可变data图片数据
            NSData *imageData = [self.imageData copy];
            //如果下载的图片存在
            if (imageData) {
                //如果下载设置只使用缓存数据就会判断缓存数据与当前获取的数据是否一致,一致就触发完成回调块
                if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
                    [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
                } else {
                    //解码图片
                    UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
                    //获取缓存图片的唯一key
                    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                    //缩放图片,不同平台图片大小计算方法不同,需要设置一下
                    image = [self scaledImageForKey:key image:image];
                    //下面是GIF WebP格式数据的解码工作
                    BOOL shouldDecode = YES;
                    // Do not force decoding animated GIFs and WebPs
                    if (image.images) {
                        shouldDecode = NO;
                    } else {
#ifdef SD_WEBP
                        SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
                        if (imageFormat == SDImageFormatWebP) {
                            shouldDecode = NO;
                        }
#endif
                    }
                    
                    if (shouldDecode) {
                        if (self.shouldDecompressImages) {
                            BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
                            image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
                        }
                    }
                    if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                        [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
                    } else {
                        [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
                    }
                }
            } else {
                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
            }
        }
    }
    [self done];
}

//如果是https访问就需要设置SSL证书相关
- (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);
    }
}

#pragma mark Helper methods
//这几个方法都是一些辅助的方法,比较好理解

//不同平台计算图片大小方式不同,图片需要缩放一下,读者可以自行查阅源码,很好理解
- (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
    return SDScaledImageForKey(key, image);
}

//是否支持后台下载
- (BOOL)shouldContinueWhenAppEntersBackground {
    return self.options & SDWebImageDownloaderContinueInBackground;
}

//调用完成回调块
- (void)callCompletionBlocksWithError:(nullable NSError *)error {
    [self callCompletionBlocksWithImage:nil imageData:nil error:error finished:YES];
}

//遍历所有的完成回调块,在主线程中触发
- (void)callCompletionBlocksWithImage:(nullable UIImage *)image
                            imageData:(nullable NSData *)imageData
                                error:(nullable NSError *)error
                             finished:(BOOL)finished {
    NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
    dispatch_main_async_safe(^{
        for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
            completedBlock(image, imageData, error, finished);
        }
    });
}

整个SDWebImageDownloaderOperation的源码就结束了,该类继承自NSOperation,实现了相关的自定义操作,所以上层类在使用时就可以很轻松的用NSOperationQueue来实现多线程下载多张图片,该类逻辑也很简单,在加入到NSOperationQueue以后,执行start方法时就会通过一个可用的NSURLSession对象来创建一个NSURLSessionDataTask的下载任务,并设置回调,在回调方法中接收数据并进行一系列通知和触发回调块。

补充

前面讲的源码很多地方都用到了SDWebImage自己的编解码技术,所以又去了解了一下相关知识。

在展示一张图片的时候常使用imageNamed:这样的类方法去获取并展示这张图片,但是图片是以二进制的格式保存在磁盘或内存中的,如果要展示一张图片需要根据图片的不同格式去解码为正确的位图交由系统控件来展示,而解码的操作默认是放在主线程执行,凡是放在主线程执行的任务都务必需要考虑清楚,如果有大量图片要展示,就会在主线程中执行大量的解码任务,势必会阻塞主线程造成卡顿,所以SDWebImage自己实现相关的编解码操作,并在子线程中处理,就不会影响主线程的相关操作。

具体的图片格式、图片编解码技术就不再讲解了,有需要的读者可以自行查阅,感觉还是挺有趣的。

备注

由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017.11.01 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 你要知道的NSURLSession都在这里
    • NSURLSession的基础使用
      • SDWebImage SDWebImageDownloaderOperation源码解析
        • 补充
          • 备注
          相关产品与服务
          SSL 证书
          腾讯云 SSL 证书(SSL Certificates)为您提供 SSL 证书的申请、管理、部署等服务,为您提供一站式 HTTPS 解决方案。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档