NSURLSession 是从 iOS 7.0 推出的,旨在取代 NSURLConnection。

NSURLSession 初识

NSURLSession 使用步骤也很简单,只需要使用 NSURLSession 对象创建 Task 对象,然后执行 Task 对象即可,其中 Task 的类型有以下几种:

NSURLSession Task Types

发送 HTTP 请求

简单使用 NSURLSession 发送 GET、POST 请求:

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
// GET 请求
- (void)getRequest {
// 获得 NSURLSession 单例对象
NSURLSession *session = NSURLSession.sharedSession;
// 创建任务(创建任务有几种方式,这里也可以用 dataTaskWithRequest 方法)
NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/login?username=angelen&pwd=123456"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%@", [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);
}];
// 启动任务 After you create the task, you must start it by calling its resume method.
[task resume];
}
// POST 请求
- (void)postRequest {
NSURLSession *session = [NSURLSession sharedSession];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/login"]];
request.HTTPMethod = @"POST";
request.HTTPBody = [@"username=angelen&pwd=123456" dataUsingEncoding:NSUTF8StringEncoding];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%@", [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);
}];
[task resume];
}

上面的发送 GET、POST 请求是通过 block 接收回调的,伟大的 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
// GET 请求(代理模式)
- (void)getRequestWithDelegate {
// 如果使用代理,那得获得 NSURLSession 时就要设置代理(不能通过 session.delegate = self,因为 delegate 是 readonly 的)
// 这里的 delegate 可以用 NSURLSessionDataDelegate,因为它继承于 NSURLSessionDelegate,类似 UITableViewDelegate 继承 UIScrollViewDelegate。
// 这里用 NSURLSessionDataDelegate 是因为我们即将要创建的任务是 NSURLSessionDataTask。
// 这里声明 delegate 是 id <NSURLSessionDelegate> 类型是因为创建的 Task 有可能是 NSURLSessionDataTask 也有可能是其他的 Task。
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/login?username=angelen&pwd=123456"]];
[task resume];
}
#pragma mark - <NSURLSessionDataDelegate>
// 1. 接收到服务器的响应
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
NSLog(@"didReceiveResponse -> %@", [NSThread currentThread]);
// 默认来说,如果没有调用这个 block,那么接收到服务器的响应之后,不会去接收数据,也不会回调 didCompleteWithError 这个方法,就是默认取消了这个 Task。
// 因此要告诉这个 Task 要继续执行还是取消还是其他。
// 这样的设计可以:我们可以判断接收到服务器的响应头是什么来决定是否要继续接收数据
completionHandler(NSURLSessionResponseAllow);
}
// 2. 接收到服务器的数据(可能会调用多次)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
NSLog(@"didReceiveData -> %@", [NSThread currentThread]);
}
// 3. 在 NSURLConnectionDataDelegate 中是有两个方法:一个是成功回调,一个是失败回调,在 NSURLSessionDataDelegate 中就合并成一个方法了,只要是请求完毕了就会回调这个方法,成功的话 error 就为 nil,失败的话 error 就是具体的失败信息
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSLog(@"didCompleteWithError -> %@", [NSThread currentThread]);
}

小 Tips:

  • 通过以上代码也是实现下载文件的功能,而且还能够知道下载进度(但是一般来说,下载任务还是直接使用 NSURLSessionDownloadTask,更加方便)。
  • NSURLSessionTask 有一些比较常见而且实用的方法可以了解一下:
1
2
3
4
5
- (void)suspend; // 暂停
- (void)resume; // 恢复
- (void)cancel; // 取消
@property (readonly, copy) NSError *error; // 错误
@property (readonly, copy) NSURLResponse *response; // 响应

文件下载

下载大文件也是很方便,NSURLSession 已经帮我们做好了边下载边缓存的功能了,先使用 block 回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)downloadFile {
NSURLSession *session = NSURLSession.sharedSession;
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_15.mp4"] completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// 文件下载:会将文件边下载边保存到 tmp 文件夹(location 路径,注意有可能随时删掉)
// 获取 Caches 目录
NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
NSFileManager *fileManager = [NSFileManager defaultManager];
// 剪切文件到 Caches 目录下
[fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:filePath] error:nil];
}];
[task resume];
}

文件会先保存到 tmp 目录下(注意随时会被清理掉,所以要及时转移文件),然后我们可以将下载到 tmp 的文件『剪切』到 Cahces 目录下:

下载到 tmp 目录

移动到 Caches 目录

不过这个方法有个缺点就是:无法得知下载进度。如果想要监测文件下载了多少,可以使用代理方式。

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
- (void)downloadLargeFileWithDelegate {
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_13.mp4"]];
[task resume];
}
#pragma mark - <NSURLSessionDownloadDelegate>
/* Sent periodically(周期性地) to notify the delegate of download progress.
* 周期性的告诉代理,下载进度是多少
*
* bytesWritten: The number of bytes transferred since the last time this delegate method was called. 从上次这个方法被回调到这次这个方法被回调要写入的数据大小。
* totalBytesWritten: The total number of bytes transferred so far. 至今为止总共写入的数据大小。
* totalBytesExpectedToWrite:The expected length of the file, as provided by the Content-Length header. If this header was not provided, the value is NSURLSessionTransferSizeUnknown. 期望总大小
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
NSLog(@"download progress -> %.2f%%", 1.0 * totalBytesWritten / totalBytesExpectedToWrite * 100);
}
/* Sent when a download task that has completed a download. The delegate should
* copy or move the file at the given location to a new location as it will be
* removed when the delegate message returns. URLSession:task:didCompleteWithError: will
* still be called.
* 当下载任务完成时回调,应该在这里给定一个新位置『拷贝』或者『剪切』文件。
* URLSession:task:didCompleteWithError: 依然会被回调。
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
NSLog(@"didFinishDownloadingToURL -> %@", location);
NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:downloadTask.response.suggestedFilename];
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:filePath] error:nil];
}
// 请求完毕(父代理的方法)
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSLog(@"didCompleteWithError -> %@", error);
}
// 打印:
省略打印进度...
download progress -> 99.75%
download progress -> 100.00%
didFinishDownloadingToURL -> file:///Users/apple/Library/Developer/CoreSimulator/Devices/1DF0CC85-C256-48B4-8569-99C5C0D14C9D/data/Containers/Data/Application/8CA9EC56-3B12-43FB-9E1D-476361EBE39A/tmp/CFNetworkDownload_PDKQWi.tmp
didCompleteWithError -> (null)

有时候,我们希望下载文件时能够断点下载,用代理方式最简单的方式就是:

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
@interface ViewController()<NSURLSessionDownloadDelegate>
/** 下载 Task */
@property (strong, nonatomic) NSURLSessionDownloadTask *task;
@end
@implementation ViewController
// 开始下载
- (IBAction)beginDownload {
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
self.task = [session downloadTaskWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_11.mp4"]];
[self.task resume];
}
// 暂停下载
- (IBAction)suspendDownload {
[self.task suspend];
}
// 恢复下载
- (IBAction)resumeSuspend {
[self.task resume];
}
#pragma mark - <NSURLSessionDownloadDelegate>
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
NSLog(@"download progress -> %.2f%%", 1.0 * totalBytesWritten / totalBytesExpectedToWrite * 100);
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:downloadTask.response.suggestedFilename];
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:filePath] error:nil];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSLog(@"didCompleteWithError -> %@", error);
}
@end

不过,这种实现仅限于这个 App 没有回到后台或者退出。

实现真正实用的断点下载器(类似于 UC浏览器,由于考虑到 App 有可能被电话、FaceTime 等中断,也有可能会被用户无意中在没有暂停的情况强制关掉等状况,因此不建议使用 NSURLSessionDownloadTask 的 cancelByProducingResumeData: 方法,因此使用 NSURLSessionDataTask 比较合适),这里没有做特殊处理,只适合单文件下载。

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#import "ViewController.h"
#import "NSString+NSHash.h"
#define MP4URL @"http://120.25.226.186:32812/resources/videos/minion_01.mp4"
#define MP4FileName MP4URL.MD5
#define MP4FilePath [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:MP4FileName]
// 用于存放文件的长度
#define FileLengthPath [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"fileLength.plist"]
#define DownloadedFileSize [[[NSFileManager defaultManager] attributesOfItemAtPath:MP4FilePath error:nil][NSFileSize] integerValue]
@interface ViewController()<NSURLSessionDataDelegate>
/** NSURLSession */
@property (strong, nonatomic) NSURLSession *session;
/** 下载 Task */
@property (strong, nonatomic) NSURLSessionDataTask *task;
/** 输出流 */
@property (strong, nonatomic) NSOutputStream *outputStream;
/** 文件总长度 */
@property (assign, nonatomic) NSInteger totalLength;
@end
@implementation ViewController
- (NSURLSession *)session {
if (!_session) {
_session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
}
return _session;
}
- (NSURLSessionDataTask *)task {
if (!_task) {
// 通过存储的 fileLength.plist 获取文件的总长度{MP4FileName: 文件大小}
NSInteger totalLength = [[NSDictionary dictionaryWithContentsOfFile:FileLengthPath][MP4FileName] integerValue];
if (totalLength && totalLength == DownloadedFileSize) {
// 如果有值 && 已经下载的文件的大小和记录在字典的大小一直,说明文件已经下载过了
NSLog(@"文件已经下载过了");
return nil;
}
// 因为要修改请求头,因此要使用 NSMutableURLRequest
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:MP4URL]];
// bytes=xxx-yyy: 从 xxx 到 yyy 的长度
// bytes=111-222: 从 111 到 222 的长度
// bytes=111-: 从 111 开始的总长度
[request setValue:[NSString stringWithFormat:@"bytes=%zd-", DownloadedFileSize] forHTTPHeaderField:@"Range"];
_task = [self.session dataTaskWithRequest:request];
}
return _task;
}
- (NSOutputStream *)outputStream {
if (!_outputStream) {
_outputStream = [[NSOutputStream alloc] initToFileAtPath:MP4FilePath append:YES];
}
return _outputStream;
}
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@", MP4FilePath);
}
// 开始(恢复)下载
- (IBAction)beginDownload {
[self.task resume];
}
// 暂停下载
- (IBAction)suspendDownload {
[self.task suspend];
}
#pragma mark - <NSURLSessionDownloadDelegate>
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
// 要在使用的时候打开流,不使用的时候记得关闭流
[self.outputStream open];
// 获取文件总大小,由于 response 的真实类型是 NSHTTPURLResponse,所以可以直接改成 NSHTTPURLResponse
/* response.allHeaderFields:
{
"Accept-Ranges" = bytes;
"Content-Length" = 10655752;
"Content-Type" = "video/mp4";
Date = "Mon, 21 Nov 2016 02:19:36 GMT";
Etag = "W/\"10655752-1409456092000\"";
"Last-Modified" = "Sun, 31 Aug 2014 03:34:52 GMT";
Server = "Apache-Coyote/1.1";
}
*/
// 这里的 Content-Length 代表的是服务器能给你的文件大小,如果你设置了请求头说从第 5M 开始拿,那服务器只会给你 3M(总文件大小为 8M)
self.totalLength = [response.allHeaderFields[@"Content-Length"] integerValue] + DownloadedFileSize;
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:FileLengthPath];
if (dict == nil) {
dict = [NSMutableDictionary dictionary];
}
// 存储文件大小
dict[MP4FileName] = @(self.totalLength);
[dict writeToFile:FileLengthPath atomically:YES];
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
// 写入数据
[self.outputStream write:data.bytes maxLength:data.length];
// 进度百分比
/* DownloadedFileSize:
{
NSFileCreationDate = "2016-11-21 02:31:31 +0000";
NSFileExtensionHidden = 0;
NSFileGroupOwnerAccountID = 20;
NSFileGroupOwnerAccountName = staff;
NSFileModificationDate = "2016-11-21 02:32:38 +0000";
NSFileOwnerAccountID = 501;
NSFilePosixPermissions = 420;
NSFileReferenceCount = 1;
NSFileSize = 11912825;
NSFileSystemFileNumber = 36725189;
NSFileSystemNumber = 16777224;
NSFileType = NSFileTypeRegular;
}
*/
NSLog(@"progress -> %.2f%%", 1.0 * DownloadedFileSize / self.totalLength * 100);
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
[self.outputStream close];
self.outputStream = nil;
self.task = nil;
}
@end

文件上传

在 NSURLSession 中实现文件上传与在 NSURLConnection 中实现文件上传相比,并没有多少便捷,需要做文件上传的时候再好好深入理解:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#define XMGBoundary @"520it"
#define XMGEncode(string) [string dataUsingEncoding:NSUTF8StringEncoding]
#define XMGNewLine [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]
#import "ViewController.h"
@interface ViewController ()
/** session */
@property (nonatomic, strong) NSURLSession *session;
@end
@implementation ViewController
- (NSURLSession *)session {
if (!_session) {
NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration defaultSessionConfiguration];
cfg.timeoutIntervalForRequest = 10;
// 是否允许使用蜂窝网络(手机自带网络)
cfg.allowsCellularAccess = YES;
_session = [NSURLSession sessionWithConfiguration:cfg];
}
return _session;
}
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/upload"]];
request.HTTPMethod = @"POST";
// 设置请求头(告诉服务器,这是一个文件上传的请求)
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", XMGBoundary] forHTTPHeaderField:@"Content-Type"];
// 设置请求体
NSMutableData *body = [NSMutableData data];
// 文件参数
// 分割线
[body appendData:XMGEncode(@"--")];
[body appendData:XMGEncode(XMGBoundary)];
[body appendData:XMGNewLine];
// 文件参数名
[body appendData:XMGEncode([NSString stringWithFormat:@"Content-Disposition: form-data; name=\"file\"; filename=\"test.png\""])];
[body appendData:XMGNewLine];
// 文件的类型
[body appendData:XMGEncode([NSString stringWithFormat:@"Content-Type: image/png"])];
[body appendData:XMGNewLine];
// 文件数据
[body appendData:XMGNewLine];
[body appendData:[NSData dataWithContentsOfFile:@"/Users/xiaomage/Desktop/test.png"]];
[body appendData:XMGNewLine];
// 结束标记
/*
--分割线--\r\n
*/
[body appendData:XMGEncode(@"--")];
[body appendData:XMGEncode(XMGBoundary)];
[body appendData:XMGEncode(@"--")];
[body appendData:XMGNewLine];
// 文件上传的时候,把请求体放到这个方法的 body 里,直接放到 request.HTTPBody 无效
[[self.session uploadTaskWithRequest:request fromData:body completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(@"-------%@", [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);
}] resume];
}
@end

需要注意的是,如果想要实现断点续传,则需要使用 Socket 实现,同时服务器也要同时支持才行。