0%

NSURLSession简单介绍

NSURLSession介绍

支持的协议

1
2
3
4
5
The URLSession class natively supports the data, file, ftp, http, and https URL schemes, with transparent support for proxy servers and SOCKS gateways, as configured in the user’s system preferences.

URLSession supports the HTTP/1.1, HTTP/2, and HTTP/3 protocols. HTTP/2 support, as described by RFC 7540, requires a server that supports Application-Layer Protocol Negotiation (ALPN).

You can also add support for your own custom networking protocols and URL schemes (for your app’s private use) by subclassing URLProtocol.

URLSession原生支持data, file, ftp, http, 和 https协议,当然你也可以子类化URLProtocol来支持自己的私有网络协议。URLSession支持的HTTP版本为HTTP/1.1, HTTP/2, 和 HTTP/3,当然要想使用到HTTP/2, HTTP/3服务器端也必须得支持才行。

URLSessionConfiguration

NSURLSession的基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
  - (void)viewDidLoad {
[super viewDidLoad];

NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 3; //设置代理回调队列的最大并发数。

self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:queue];
NSLog(@"%@", self.session);

NSURL *url = [NSURL URLWithString:landscapeUrl];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:60 * 5];
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:req];
[dataTask resume];
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
NSLog(@"收到响应:%@ \ndataTask:%@", response, dataTask);

self.mData = [NSMutableData data];

NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;
completionHandler(disposition);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data
{
NSLog(@"%@收到data:%ld",[NSThread currentThread] ,data.length);

[self.mData appendData:data];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error
{
NSLog(@"完成, error:%@", error);
if (!error) {
//在完成的时候,之前收到的data怎么取到?不借助其他的变量,在该方法里取不到?
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:self.mData options:NSJSONReadingAllowFragments error:nil];
NSLog(@"%@", dict);
}
//不把本次session Invalidate,那么session持有的delegate不会被释放.
[session finishTasksAndInvalidate];
}

对于方法

1
2
3
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration 
delegate:(nullable id <NSURLSessionDelegate>)delegate
delegateQueue:(nullable NSOperationQueue *)queue;

delegate和delegateQueue会被session强引用。只有当session被 invalidate后,delegate在URLSession:didBecomeInvalidWithError结束后才会被释放.对于delegateQueue,实际使用时delegateQueue最好不要设置为主队列(会增加主线程负担).当delegateQueue不是主队列时,didReceiveData:方法将随机在某个线程执行.

基本上一个APP,生成一个urlSession就够了.没必要一次请求,创建一个session,请求结束后又将session Invalidate.因此也就没必要去管delegate和delegateQueue的内存释放问题,这三个对象基本上是等到APP结束才会销毁的.一般的做法是常规的API请求使用一个session,上传下载的请求使用另一个session。

对于代理方法:

1
2
3
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler

在该方法中,为什么收到响应后,还要调用completionHandler?
因为在该方法中,通过disposition参数,调用completionHandler后,可以更细粒度的控制本次请求是继续还是取消还是转为下载任务.如果是取消,则后面请求的响应体不会接收.如果是转为下载任务,那么通过调用completionHandler,NSURLSession将调用Delegate的 URLSession:dataTask:didBecomeDownloadTask:方法并将新生成的Download task对象作为参数传入。在此调用之后,Delegate将不再接收来自Data task的回调消息,并开始接收Download task的回调消息。
注意:如果不调用

1
2
NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;
completionHandler(disposition);

后面的didReceiveData:代理方法将不会执行.

delegateQueue

回调的派发队列,方法参数说明:

1
An operation queue for scheduling the delegate calls and completion handlers. The queue should be a serial queue, in order to ensure the correct ordering of callbacks. If nil, the session creates a serial operation queue for performing all delegate method calls and completion handler calls.

用于执行delegate回调和完成处理的派发队列,delegateQueue最好是一个串行队列,这样可以确保正确的回调顺序。默认是一个串行队列。注意这个只是回调方法的派发队列,和网络请求的线程不是同一个。

1
2
3
4
5
6
7
8
网络请求线程        回调队列
| |
| callback1 |
|-----------> |
| |
| callback2 |
|-----------> |
| |

这里文档是should,如果用个并发队列会怎样?我们项目里用的就是并发队列,用了这么久好像也没什么影响。

场景:点击按钮,发起1个请求,故意阻塞didReceiveData回调。

1
2
3
4
5
6
7
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
[self.mData appendData:data];
unsigned int time = MAX(1, arc4random() % 4);
sleep(time*10);
NSLog(@"本次接收:%ld,总共接收:%ld,线程:%@", data.length, self.mData.length, [NSThread currentThread]);
}

即使delegateQueue的最大并发数设置为3,但是didReceiveData的回调行为还是类似串行(虽然每次线程确实会不一样),不会出现两个线程同时回调didReceiveData。

通过实验可以得出:

1.delegateQueue的最大并发数设置为1,那么回调就是串行的,由于多个请求公用这一个回调派发队列,如果其中一个请求的代理方法比较耗时,那么其他请求的回调将会等待前面的回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
NSURL *url = dataTask.originalRequest.URL;
NSMutableData *dataContainer = [self dataWithURL:url];
[dataContainer appendData:data];

NSLog(@"本次接收1:%ld,线程:%@, url:%@", data.length, [NSThread currentThread], url.lastPathComponent);

if (url.absoluteString == landscapeUrl) { //故意让landscapeUrl的请求回调阻塞一下,其他url不阻塞。
sleep(10);
}
NSLog(@"本次接收2:%ld,总共接收:%ld,线程:%@, url:%@", data.length, dataContainer.length, [NSThread currentThread], url.lastPathComponent);
}

不过一般情况下didReceiveData不可能出现这种故意阻塞的写法,所以delegateQueue的并发数设置为1几乎没有什么影响,如果没有什么特别的理由还是按文档上来设置为1吧。

2.delegateQueue的最大并发数设置为3(大于1的情况),那么回调就是并发的,目前没看到有什么异常影响。因为是并发队列,其他请求的回调不用等待前面的回调完成因而可以快速被回调。

3.即使delegateQueue的最大并发数设置为3,对于同一个请求的回调,didReceiveData的回调行为还是类似串行(虽然每次线程确实会不一样),不会出现两个线程同时回调didReceiveData。

defaultSession 和 ephemeralSession

ephemeralSession,不会将响应缓存到磁盘,也不会使用磁盘缓存的响应。最多将一些session-related的数据保存在内存。APP结束,所有会话信息都会随着内存回收而被清除。

defaultSession,默认session,如果响应能够缓存则会将响应持久化到磁盘。

问答

1.NSURLSession对象是被谁强引用了?如何释放?

NSURLSession对象应该是被系统的runloop强引用了,就类似于定时器一样,需要invalid后,才会被释放销毁.
题外话:如果timer属性是strong,那么invalidate后最好将其置为nil,否则invalid后timer因为还有人持有它,而不能销毁.strong情况下,timer的释放: [self.timer invalidate];self.timer = nil;定时器对象是注册到runloop里的,应该通过invalidate来告诉runloop释放它.所以self不应该持有该对象,因此timer属性最好为weak.

2. 在didCompleteWithError:完成的时候,之前收到的data怎么取到?不借助其他的变量,在该方法里取不到?

完成回调里是取不到data的。需要在相应的代理方法里保存数据。

3. 请求转为下载任务时,为什么会转移到其他的代理方法比如didBecomeDownloadTask?

这是因为下载任务的资源一般较大,接收到的数据不能像普通请求那样直接缓存在内存,否则会导致APP OOM。所以必须转移到其他代理方法边接收边缓存到磁盘。

4.TCP接收到数据后是怎么传给NSURLSession的,NSURLSession又是怎么传给delegateQueue执行各个代理方法的,线程关系?TD

不知道。

觉得文章有帮助可以打赏一下哦!