NSURLSession 详解

NSURLSession

NSURLSessioniOS7中推出,NSURLSession的推出旨在替换之前的NSURLConnectionNSURLSession的使用相对于之前的NSURLConnection更简单,而且不用处理Runloop相关的东西。

2015年RFC 7540标准发布了http 2.0版本,http 2.0版本中包含很多新的特性,在传输速度上也有很明显的提升。NSURLSessioniOS9.0开始,对http 2.0提供了支持。

NSURLSession由三部分构成:

  • NSURLSession:请求会话对象,可以用系统提供的单例对象,也可以自己创建。
  • NSURLSessionConfiguration:对session会话进行配置,一般都采用default
  • NSURLSessionTask:负责执行具体请求的task,由session创建。

NSURLSession有三种方式创建:

sharedSession

系统维护的一个单例对象,可以和其他使用这个sessiontask共享连接和请求信息。

sessionWithConfiguration:

在NSURLSession初始化时传入一个NSURLSessionConfiguration,这样可以自定义请求头、cookie等信息。

sessionWithConfiguration:delegate:delegateQueue:

如果想更好的控制请求过程以及回调线程,需要上面的方法进行初始化操作,并传入delegate来设置回调对象和回调的线程。

通过NSURLSession发起一个网络请求也比较简单。

  1. 创建一个NSURLSessionConfiguration配置请求。
  2. 通过Configuration创建NSURLSession对象。
  3. 通过session对象发起网络请求,并获取task对象。
  4. 调用[task resume]方法发起网络请求。
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
                                                      delegate:self
                                                 delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
[task resume];

NSURLSessionTask

通过NSURLSession发起的每个请求,都会被封装为一个NSURLSessionTask任务,但一般不会直接是NSURLSessionTask类,而是基于不同任务类型,被封装为其对应的子类。

  • NSURLSessionDataTask:处理普通的GetPost请求。
  • NSURLSessionUploadTask:处理上传请求,可以传入对应的上传文件或路径。
  • NSURLSessionDownloadTask:处理下载地址,提供断点续传功能的cancel方法。

主要方法都定义在父类NSURLSessionTask中,下面是一些关键方法或属性。

currentRequest 当前正在执行的任务,一般和originalRequest是一样的,除非发生重定向才会有所区别。originalRequest 主要用于重定向操作,用来记录重定向前的请求。 taskIdentifier 当前session下,task的唯一标示,多个session之间可能存在相同的标识。 priority task中可以设置优先级,但这个属性并不代表请求的优先级,而是一个标示。官方已经说明,NSURLSession并没有提供API可以改变请求的优先级。 state 当前任务的状态,可以通过KVO的方式监听状态的改变。 - resume 开始或继续请求,创建后的task默认是挂起的,需要手动调用resume才可以开始请求。 - suspend 挂起当前请求。主要是下载请求用的多一些,普通请求挂起后都会重新开始请求。下载请求挂起后,只要不超过NSURLRequest设置的timeout时间,调用resume就是继续请求。 - cancel 取消当前请求。任务会被标记为取消,并在未来某个时间调用URLSession:task:didCompleteWithError:方法。

NSURLSession提供有普通创建task的方式,创建后可以通过重写代理方法,获取对应的回调和参数。这种方式对于请求过程比较好控制。

- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request;
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request;

除此之外,NSURLSession也提供了block的方式创建task,创建方式简单如AFN,直接传入URLNSURLRequest,即可直接在block中接收返回数据。和普通创建方式一样,block的创建方式创建后默认也是suspend的状态,需要调用resume开始任务。

completionHandlerdelegate是互斥的,completionHandler的优先级大于delegate。相对于普通创建方法,block方式更偏向于面向结果的创建,可以直接在completionHandler中获取返回结果,但不能控制请求过程。

- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(nullable NSData *)bodyData completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;

可以通过下面的两个方法,获取当前session对应的所有task,方法区别在于回调的参数不同。以getTasksWithCompletionHandler为例,在AFN中的应用是用来获取当前sessiontask,并将AFURLSessionManagerTaskDelegate的回调都置为nil,以防止崩溃。

- (void)getTasksWithCompletionHandler:(void (^)(NSArray<NSURLSessionDataTask *> *dataTasks, NSArray<NSURLSessionUploadTask *> *uploadTasks, NSArray<NSURLSessionDownloadTask *> *downloadTasks))completionHandler;

- (void)getAllTasksWithCompletionHandler:(void (^)(NSArray<__kindof NSURLSessionTask *> *tasks))completionHandler);

delegateQueue

在初始化NSURLSession时可以指定线程,如果不指定线程,则completionHandlerdelegate的回调方法,都会在子线程中执行。

如果初始化NSURLSession时指定了delegateQueue,则回调会在指定的队列中执行,如果指定的是mainQueue,则回调在主线程中执行,这样就避免了切换线程的问题。

[NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];

delegate

对于NSURLSession的代理方法这里就不详细列举了,方法命名遵循苹果一贯见名知意的原则,用起来很简单。这里介绍一下NSURLSession的代理继承结构。

 

代理继承关系

 

 

NSURLSession中定义了一系列代理,并遵循上面的继承关系。根据继承关系和代理方法的声明,如果执行某项任务,只需要遵守其中的某个代理即可。

例如执行上传或普通Post请求,则遵守NSURLSessionDataDelegate,执行下载任务则遵循NSURLSessionDownloadDelegate,父级代理定义的都是公共方法。

请求重定向

HTTP协议中定义了例如301等重定向状态码,通过下面的代理方法,可以处理重定向任务。发生重定向时可以根据response创建一个新的request,也可以直接用系统生成的request,并在completionHandler回调中传入,如果想终止这次重定向,在completionHandler传入nil即可。

- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
        newRequest:(NSURLRequest *)request
 completionHandler:(void (^)(NSURLRequest *))completionHandler
{
    NSURLRequest *redirectRequest = request;

    if (self.taskWillPerformHTTPRedirection) {
        redirectRequest = self.taskWillPerformHTTPRedirection(session, task, response, request);
    }

    if (completionHandler) {
        completionHandler(redirectRequest);
    }
}

NSURLSessionConfiguration

创建方式

NSURLSessionConfiguration负责对NSURLSession初始化时进行配置,通过NSURLSessionConfiguration可以设置请求的Cookie、密钥、缓存、请求头等参数,将网络请求的一些配置参数从NSURLSession中分离出来。

NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
                                                      delegate:self
                                                 delegateQueue:[NSOperationQueue mainQueue]];

NSURLSessionConfiguration提供三种初始化方法,下面是请求的方法的一些解释。

@property (class, readonly, strong) NSURLSessionConfiguration *defaultSessionConfiguration;

NSURLSessionConfiguration提供defaultSessionConfiguration的方式创建,但这并不是单例方法,而是类方法,创建的是不同对象。通过这种方式创建的configuration,并不会共享cookiecache、密钥等,而是不同configuration都需要单独设置。

这块网上很多人理解都是错的,并没有真的在项目里使用或者没有留意过,如和其他人有出入,以我为准。

@property (class, readonly, strong) NSURLSessionConfiguration *ephemeralSessionConfiguration;

创建临时的configuration,通过这种方式创建的对象,和普通的对象主要区别在于URLCacheURLCredentialStorageHTTPCookieStorage上面。同样的,Ephemeral也不是单例方法,而只是类方法。

URLCredentialStorage
Ephemeral <__NSCFMemoryURLCredentialStorage: 0x600001bc8320>

HTTPCookieStorage
Ephemeral <NSHTTPCookieStorage cookies count:0>

如果对Ephemeral方式创建的config进行打印的话,可以看到变量类型明显区别于其他类型,并且在打印信息前面会有Ephemeral的标示。通过Ephemeral的方式创建的config,不会产生持久化信息,可以很好保护请求的数据安全性。

+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier;

identifier方式一般用于恢复之前的任务,主要用于下载。如果一个下载任务正在进行中,程序被kill调,可以在程序退出之前保存identifier。下次进入程序后通过identifier恢复之前的任务,系统会将NSURLSessionNSURLSessionConfiguration和之前的下载任务进行关联,并继续之前的任务。

timeout

timeoutIntervalForRequest

设置session请求间的超时时间,这个超时时间并不是请求从开始到结束的时间,而是两个数据包之间的时间间隔。当任意请求返回后这个值将会被重置,如果在超时时间内未返回则超时。单位为秒,默认为60秒。

timeoutIntervalForResource

资源超时时间,一般用于上传或下载任务,在上传或下载任务开始后计时,如果到达时间任务未结束,则删除资源文件。单位为秒,默认时间是七天。

资源共享

如果是相同的NSURLSessionConfiguration对象,会共享请求头、缓存、cookieCredential,通过Configuration创建的NSURLSession,也会拥有对应的请求信息。

@property (nullable, copy) NSDictionary *HTTPAdditionalHeaders;

公共请求头,默认是空的,设置后所有经Confuguration配置的NSURLSession,请求头都会带有设置的信息。

@property (nullable, retain) NSHTTPCookieStorage *HTTPCookieStorage;

HTTP请求的Cookie管理器。如果是通过sharedSessionbackgroundConfiguration创建的NSURLSession,默认使用sharedHTTPCookieStorageCookie数据。如果不想使用Cookie,则直接设置为nil即可,也可以手动设置为自己的CookieStorage

@property (nullable, retain) NSURLCredentialStorage *URLCredentialStorage;

证书管理器。如果是通过sharedSessionbackgroundConfiguration创建的NSURLSession,默认使用sharedCredentialStorage的证书。如果不想使用证书,可以直接设置为nil,也可以自己创建证书管理器。

@property (nullable, retain) NSURLCache *URLCache;

请求缓存,如果不手动设置的话为nil,对于NSURLCache这个类我没有研究过,不太了解。

缓存处理

NSURLRequest中可以设置cachePolicy请求缓存策略,这里不对具体值做详细描述,默认值为NSURLRequestUseProtocolCachePolicy使用缓存。

NSURLSessionConfiguration可以设置处理缓存的对象,我们可以手动设置自定义的缓存对象,如果不设置的话,默认使用系统的sharedURLCache单例缓存对象。经过configuration创建的NSURLSession发出的请求,NSURLRequest都会使用这个NSURLCache来处理缓存。

@property (nullable, retain) NSURLCache *URLCache;

NSURLCache提供了MemoryDisk的缓存,在创建时需要为其分别指定MemoryDisk的大小,以及存储的文件位置。使用NSURLCache不用考虑磁盘空间不够,或手动管理内存空间的问题,如果发生内存警告系统会自动清理内存空间。但是NSURLCache提供的功能非常有限,项目中一般很少直接使用它来处理缓存数据,还是用数据库比较多。

[[NSURLCache alloc] initWithMemoryCapacity:30 * 1024 * 1024 
                              diskCapacity:30 * 1024 * 1024 
                              directoryURL:[NSURL URLWithString:filePath]];

使用NSURLCache还有一个好处,就是可以由服务端来设置资源过期时间,在请求服务端后,服务端会返回Cache-Control来说明文件的过期时间。NSURLCache会根据NSURLResponse来自动完成过期时间的设置。

最大连接数

限制NSURLSession的最大连接数,通过此方法创建的NSURLSession和服务端的最大连接数量不会超出这里设置的数量。苹果为我们设置的iOS端默认为4,Mac端默认为6。

@property NSInteger HTTPMaximumConnectionsPerHost;

连接复用

HTTP是基于传输层协议TCP的,通过TCP发送网络请求都需要先进行三次握手,建立网络请求后再发送数据,请求结束时再经历四次挥手。HTTP1.0开始支持keep-alivekeep-alive可以保持已经建立的链接,如果是相同的域名,在请求连接建立后,后面的请求不会立刻断开,而是复用现有的连接。从HTTP1.1开始默认开启keep-alive

请求是在请求头中设置下面的参数,服务器如果支持keep-alive的话,响应客户端请求时,也会在响应头中加上相同的字段。

Connection: Keep-Alive

如果想断开keep-alive,可以在请求头中加上下面的字段,但一般不推荐这么做。

Connection: Close

如果通过NSURLSession来进行网络请求的话,需要使用同一个NSURLSession对象,如果创建新的session对象则不能复用之前的链接。keep-alive可以保持请求的连接,苹果允许在iOS上最大保持有4个连接,Mac则是6个连接。

pipeline

 

pipeline

 

 

HTTP1.1中,基于keep-alive,还可以将请求进行管线化。和相同后端服务,TCP层建立的链接,一般都需要前一个请求返回后,后面的请求再发出。但pipeline就可以不依赖之前请求的响应,而发出后面的请求。

pipeline依赖客户端和服务器都有实现,服务端收到客户端的请求后,要按照先进先出的顺序进行任务处理和响应。pipeline依然存在之前非pipeline的问题,就是前面的请求如果出现问题,会阻塞当前连接影响后面的请求。

pipeline对于请求大文件并没有提升作用,只是对于普通请求速度有提升。在NSURLSessionConfiguration中可以设置HTTPShouldUsePipeliningYES,开启管线化,此属性默认为NO

NSURLSessionTaskMetrics

在日常开发过程中,经常遇到页面加载太慢的问题,这很大一部分原因都是因为网络导致的。所以,查找网络耗时的原因并解决,就是一个很重要的任务了。苹果对于网络检查提供了NSURLSessionTaskMetrics类来进行检查,NSURLSessionTaskMetrics是对应NSURLSessionTaskDelegate的,每个task结束时都会回调下面的方法,并且可以获得一个metrics对象。

- (void)URLSession:(NSURLSession *)session 
              task:(NSURLSessionTask *)task 
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics;

NSURLSessionTaskMetrics可以很好的帮助我们分析网络请求的过程,以找到耗时原因。除了这个类之外,NSURLSessionTaskTransactionMetrics类中承载了更详细的数据。

@property (copy, readonly) NSArray<NSURLSessionTaskTransactionMetrics *> *transactionMetrics;

transactionMetrics数组中每一个元素都对应着当前task的一个请求,一般数组中只会有一个元素,如果发生重定向等情况,可能会存在多个元素。

@property (copy, readonly) NSDateInterval *taskInterval;

taskInterval记录了当前task从开始请求到最后完成的总耗时,NSDateInterval中包含了startDateendDateduration耗时时间。

@property (assign, readonly) NSUInteger redirectCount;

redirectCount记录了重定向次数,在进行下载请求时一般都会进行重定向,来保证下载任务能由后端最合适的节点来处理。

NSURLSessionTaskTransactionMetrics

NSURLSessionTaskTransactionMetrics中的属性都是用来做统计的,功能都是记录某个值,并没有逻辑上的意义。所以这里就对一些主要的属性做一下解释,基本涵盖了大部分属性,其他就不管了。

这张图是我从网上扒下来的,标示了NSURLSessionTaskTransactionMetrics的属性在请求过程中处于什么位置。

 

请求耗时细节

 

 

// 请求对象
@property (copy, readonly) NSURLRequest *request;
// 响应对象,请求失败可能会为nil
@property (nullable, copy, readonly) NSURLResponse *response;
// 请求开始时间
@property (nullable, copy, readonly) NSDate *fetchStartDate;
// DNS解析开始时间
@property (nullable, copy, readonly) NSDate *domainLookupStartDate;
// DNS解析结束时间,如果解析失败可能为nil
@property (nullable, copy, readonly) NSDate *domainLookupEndDate;
// 开始建立TCP连接时间
@property (nullable, copy, readonly) NSDate *connectStartDate;
// 结束建立TCP连接时间
@property (nullable, copy, readonly) NSDate *connectEndDate;
// 开始TLS握手时间
@property (nullable, copy, readonly) NSDate *secureConnectionStartDate;
// 结束TLS握手时间
@property (nullable, copy, readonly) NSDate *secureConnectionEndDate;
// 开始传输请求数据时间
@property (nullable, copy, readonly) NSDate *requestStartDate;
// 结束传输请求数据时间
@property (nullable, copy, readonly) NSDate *requestEndDate;
// 接收到服务端响应数据时间
@property (nullable, copy, readonly) NSDate *responseStartDate;
// 服务端响应数据传输完成时间
@property (nullable, copy, readonly) NSDate *responseEndDate;
// 网络协议,例如http/1.1
@property (nullable, copy, readonly) NSString *networkProtocolName;
// 请求是否使用代理
@property (assign, readonly, getter=isProxyConnection) BOOL proxyConnection;
// 是否复用已有连接
@property (assign, readonly, getter=isReusedConnection) BOOL reusedConnection;
// 资源标识符,表示请求是从Cache、Push、Network哪种类型加载的
@property (assign, readonly) NSURLSessionTaskMetricsResourceFetchType resourceFetchType;
// 本地IP
@property (nullable, copy, readonly) NSString *localAddress;
// 本地端口号
@property (nullable, copy, readonly) NSNumber *localPort;
// 远端IP
@property (nullable, copy, readonly) NSString *remoteAddress;
// 远端端口号
@property (nullable, copy, readonly) NSNumber *remotePort;
// TLS协议版本,如果是http则是0x0000
@property (nullable, copy, readonly) NSNumber *negotiatedTLSProtocolVersion;
// 是否使用蜂窝数据
@property (readonly, getter=isCellular) BOOL cellular;

下面是我发起一个http的下载请求,统计得到的数据。设备是Xcode模拟器,网络环境是WiFi

(Request) <NSURLRequest: 0x600000c80380> { URL: http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4 }
(Response) <NSHTTPURLResponse: 0x600000ed9420> { URL: http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4 } { Status Code: 200, Headers {
    "Accept-Ranges" =     (
        bytes
    );
    Age =     (
        1063663
    );
    "Ali-Swift-Global-Savetime" =     (
        1575358696
    );
    Connection =     (
        "keep-alive"
    );
    "Content-Length" =     (
        20472584
    );
    "Content-Md5" =     (
        "YM+JxIH9oLH6l1+jHN9pmQ=="
    );
    "Content-Type" =     (
        "video/mp4"
    );
    Date =     (
        "Tue, 03 Dec 2019 07:38:16 GMT"
    );
    EagleId =     (
        dbee142415764223598843838e
    );
    Etag =     (
        "\"60CF89C481FDA0B1FA975FA31CDF6999\""
    );
    "Last-Modified" =     (
        "Fri, 31 Mar 2017 01:41:36 GMT"
    );
    Server =     (
        Tengine
    );
    "Timing-Allow-Origin" =     (
        "*"
    );
    Via =     (
        "cache39.l2et2[0,200-0,H], cache6.l2et2[3,0], cache16.cn548[0,200-0,H], cache16.cn548[1,0]"
    );
    "X-Cache" =     (
        "HIT TCP_MEM_HIT dirn:-2:-2"
    );
    "X-M-Log" =     (
        "QNM:xs451;QNM3:71"
    );
    "X-M-Reqid" =     (
        "m0AAAP__UChjzNwV"
    );
    "X-Oss-Hash-Crc64ecma" =     (
        12355898484621380721
    );
    "X-Oss-Object-Type" =     (
        Normal
    );
    "X-Oss-Request-Id" =     (
        5DE20106F3150D38305CE159
    );
    "X-Oss-Server-Time" =     (
        130
    );
    "X-Oss-Storage-Class" =     (
        Standard
    );
    "X-Qnm-Cache" =     (
        Hit
    );
    "X-Swift-CacheTime" =     (
        2592000
    );
    "X-Swift-SaveTime" =     (
        "Sun, 15 Dec 2019 15:05:37 GMT"
    );
} }
(Fetch Start) 2019-12-15 15:05:59 +0000
(Domain Lookup Start) 2019-12-15 15:05:59 +0000
(Domain Lookup End) 2019-12-15 15:05:59 +0000
(Connect Start) 2019-12-15 15:05:59 +0000
(Secure Connection Start) (null)
(Secure Connection End) (null)
(Connect End) 2019-12-15 15:05:59 +0000
(Request Start) 2019-12-15 15:05:59 +0000
(Request End) 2019-12-15 15:05:59 +0000
(Response Start) 2019-12-15 15:05:59 +0000
(Response End) 2019-12-15 15:06:04 +0000
(Protocol Name) http/1.1
(Proxy Connection) NO
(Reused Connection) NO
(Fetch Type) Network Load
(Request Header Bytes) 235
(Request Body Transfer Bytes) 0
(Request Body Bytes) 0
(Response Header Bytes) 866
(Response Body Transfer Bytes) 20472584
(Response Body Bytes) 20472584
(Local Address) 192.168.1.105
(Local Port) 63379
(Remote Address) 219.238.20.101
(Remote Port) 80
(TLS Protocol Version) 0x0000
(TLS Cipher Suite) 0x0000
(Cellular) NO
(Expensive) NO
(Constrained) NO
(Multipath) NO

FAQ

NSURLSession的delegate为什么是强引用?

在初始化NSURLSession对象并设置代理后,代理对象将会被强引用。根据苹果官方的注释来看,这个强持有并不会一直存在,而是在调用URLSession:didBecomeInvalidWithError:方法后,会将delegate释放。

通过调用NSURLSessioninvalidateAndCancelfinishTasksAndInvalidate方法,即可将强引用断开并执行didBecomeInvalidWithError:代理方法,执行完成后session就会无效不可以使用。也就是只有在session无效时,才可以解除强引用的关系。

有时候为了保证连接复用等问题,一般不会轻易将session会话invalid,所以最好不要直接使用NSURLSession,而是要对其进行一次二次封装,使用AFN3.0的原因之一也在于此。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值