文件下载

iOS 中实现文件下载有以下几种方式:

一.NSData

NSData 提供了类方法实现文件下载:

1
2
3
NSURL  *aUrl = [NSURL URLWithString:@"https://example.jpg"];
NSData *data = [NSData dataWithContentsOfURL:aUrl];
UIImage *image = [UIImage imageWithData:data];

使用这种方式会一次性返回整个下载到的文件,且在当前线程中同步下载文件。所以,在主线程中下载时,遇到网络状况不佳的情况时,会出现卡顿的现象。

解决方案之一:把下载任务放到异步线程中进行:

1
2
3
4
5
6
7
8
dispatch_queue_t downloadQueue = dispatch_queue_create(
"downloadQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(downloadQueue, ^{
NSURL *aUrl = [NSURL URLWithString:@"https://example.jpg"];
NSData *data = [NSData dataWithContentsOfURL:aUrl];
UIImage *image = [UIImage imageWithData:data];
});

此方法虽然解决了卡顿的问题,但还有另外的问题:返回的 data 是在内存中的。当目标文件过大时,内存会爆掉。所以,我们希望有这样一种方案:

  1. 文件数据能分批返回。
  2. 解决保存返回的数据时内存占用过大的情况。

对于第1点,我们可以使用系统提供的 NSURLConnection、NSURLSession 类来解决。它们都可以通过代理来不断接收返回的数据。

对于第2点,我们可以把接收到的数据保存到沙盒里。待全部数据接收并处理完成后,删除此沙盒文件即可。

二.NSURLConnection

通过 NSURLConnection 可以实现创建连接、自动发送请求、通过代理接收服务器返回的数据。同时,可以实现暂停和断点续传功能。

2.1.下载功能

  1. 创建一个 NSURL 对象,设置请求地址;
  2. 创建一个 NSURLRequest 对象,设置请求头和请求体;
  3. 使用 NSURLConnection 发送请求;
  4. 通过代理方法接收服务器的响应和数据,写入沙盒中;

2.2.暂停功能

NSURLConnection 本身并没有暂停的相关函数,但我们可以使用它提供的 cancel 方法来实现暂停功能。

2.3.断点续传功能

要实现断点续传,可借助 HTTP 请求头的 range 字段。它可以指定每次从网络上下载的数据包的大小。range 字段的格式如下:

1
2
3
4
bytes = 0-100,             从0100100个字节。
bytes = 200-, 从200字节以后的所有字节。
bytes = - 500, 最后500个字节。
bytes = 200-300500-800, 同时指定几个范围。

每次暂停时,在 connection:didReceiveData: 回调中记录已经接收到的数据大小,在恢复下载时,把此大小对应的 range 字段设置到请求头中,重新创建连接并发送请求即可。

具体可参考以下示例:

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
//DataTaskTool.h

#import <Foundation/Foundation.h>

@class DataTaskTool;

@protocol DataTaskToolDelegate

- (void)dataTaskTool:(DataTaskTool*)tool
onDownloadProgress:(double)progress;

- (void)dataTaskTool:(DataTaskTool*)tool
onDownloadFinishedWithInfo:(id)info;

- (void)dataTaskTool:(DataTaskTool*)tool
onDownloadFailedWithError:(NSError*)error;

@end

@interface DataTaskTool : NSObject

- (instancetype)initWithURL:(NSString*)URL
delegate:(id<DataTaskToolDelegate>)delegate;

- (void)start;
- (void)pause;
- (void)resume;
- (void)cancel;

@end
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
//DataTaskTool.m

#import "DataTaskTool.h"

@interface DataTaskTool ()
<
NSURLConnectionDataDelegate
>
{
NSMutableURLRequest *mRequest; //请求对象
NSURLConnection *mUrlConnection; //连接对象
NSFileHandle *mFileHandle; //文件管理
NSString *mFilePath; //沙盒文件路径
unsigned long long mTotalContentLength; //文件总长度
unsigned long long mCurrentContentLength; //当前接收到的数据总长度
double mProgressValue; //当前下载进度值
}

@property (nonatomic, weak) id <DataTaskToolDelegate> delegate;

@end

@implementation DataTaskTool

- (instancetype)initWithURL:(NSString*)URL
delegate:(id<DataTaskToolDelegate>)delegate
{
if (self = [super init])
{
if (0 == URL.length) {
return nil;
}
_delegate = delegate;

//设置请求
mRequest = [[NSMutableURLRequest alloc]
initWithURL:[NSURL URLWithString:URL]];

mRequest.HTTPMethod = @"GET";
mRequest.timeoutInterval = 60;

//创建连接对象
mUrlConnection = [[NSURLConnection alloc]
initWithRequest:mRequest
delegate:self];
}
return self;
}

#pragma mark -NSURLConnectionDataDelegate
//接收到服务器的响应
-(void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response
{
mTotalContentLength = response.expectedContentLength;

//设置文件路径
NSString *fileName = response.suggestedFilename;
NSString* caches = [NSSearchPathForDirectoriesInDomains(
NSCachesDirectory, NSUserDomainMask, YES)
lastObject];

mFilePath = [caches stringByAppendingPathComponent:fileName];

if (![[NSFileManager defaultManager] fileExistsAtPath:mFilePath])
{
[[NSFileManager defaultManager]
createFileAtPath:mFilePath
contents:nil attributes:nil];
}

mFileHandle = [NSFileHandle fileHandleForWritingAtPath:mFilePath];
}

//接收到服务器返回的数据(此方法可能会回调多次)
-(void)connection:(NSURLConnection *)connection
didReceiveData:(NSData *)data
{
//移动到文件的结尾
[mFileHandle seekToEndOfFile];
//新数据追加到文件中
[mFileHandle writeData:data];

//计算当前下载进度
mCurrentContentLength += data.length;
mProgressValue = (double)mCurrentContentLength / mTotalContentLength;

//回调进度
[_delegate dataTaskTool:self onDownloadProgress:mProgressValue];
}

//下载完成
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//结束写入
[mFileHandle closeFile];
NSError *error = [NSError new];
[[NSFileManager defaultManager] removeItemAtPath:mFilePath error:&error];

//完成回调
[_delegate dataTaskTool:self onDownloadFinishedWithInfo:nil];
}

-(void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
{
[_delegate dataTaskTool:self onDownloadFailedWithError:error];
}

#pragma mark -BUSINESS API
//开始请求
- (void)start
{
[mUrlConnection start];
}

//暂停
- (void)pause
{
[mUrlConnection cancel];
mUrlConnection = nil;
}

//恢复
- (void)resume
{
//设置断点下载的区间
NSString *range = [NSString stringWithFormat:
@"bytes=%lld-", mCurrentContentLength];

[mRequest setValue:range forHTTPHeaderField:@"Range"];

mUrlConnection = [[NSURLConnection alloc]
initWithRequest:mRequest delegate:self];

[mUrlConnection start];
}

//取消请求
- (void)cancel
{
[mUrlConnection cancel];
}
@end

调用示例

1
2
3
4
5
6
7
8
9
10
/*Windows10下载地址
URL: https://software-download.microsoft.com/
db/Win10_1709_Chinese(Simplified)_x32.iso?
t=e56bdb47-6973-4da2-9e94-5a6a458c9192&e
=1513964531&h=d9ba28ed1645810206cd89e7c4675a5b
*/
DataTaskTool *tool = [[DataTaskTool alloc]
initWithURL:url delegate:self];

[tool start];

需要注意的是,iOS9之后,NSURLConnection 已被废弃,苹果转而推荐使用 NSURLSession。

三.NSURLSession

NSURLSession 是苹果在 iOS7 后为数据传输提供的一系列接口,它比 NSURLConnection 更强大。

  • 任务都是异步进行的,没有同步执行的单独接口;
  • 支持后台数据上传和下载;
  • 有单独接口支持断点续传;
  • 数据下载会保存到缓存目录,解决了下载时的内存问题;
  • 任务创建后不会自动发送请求,需要手动开始执行任务;

3.1.配置

NSURLSessionConfiguration,这是一个定义URLSession的行为和策略的配置,是实例化URLSession对象的必备参数;一旦配置完成则当前会话会使用该配置的一个备份,并忽略你对原配置对象的任何修改;如果想修改配置信息,你需要更新对应的字段并且用更新后的配置创建一个新的URLSession对象。

1、会话配置分类:

1
2
3
@property (class, readonly, strong) NSURLSessionConfiguration *defaultSessionConfiguration;
@property (class, readonly, strong) NSURLSessionConfiguration *ephemeralSessionConfiguration;
+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier;
  • defaultSessionConfiguration

默认的会话配置,它会使用硬盘空间做缓存;用钥匙串保存用户的授权信息;会将cookie保存到shared cookie store中。

  • ephemeralSessionConfiguration

临时会话配置,不做硬盘缓存,而是保存到内存RAM中;不保存用户的cookie和证书;invalidate后所有配置信息会被抹去。

  • backgroundSessionConfiguration

后台会话配置,需要指定一个标识符。它允许在后台执行数据的上传或下载任务,使用这个配置时session会将数据的传输控制权交给系统,系统会在一个单独的进程中处理这个任务,即使应用被挂起甚至被杀掉时,仍能继续数据的上传或下载。注意这里的被杀掉是指被系统杀掉,如果是用户通过多任务界面主动杀掉了应用,则系统会自动清空当前session的所有后台传输任务,任务会暂停。用户需要主动重启应用,之前的任务才能继续。如果应用是被系统杀掉并重启的,那么应用内可以使用与之前后台会话配置中相同的identifier标识符重新创建配置和URLSession对象,此session对象可以返回后台任务被杀掉时所处的状态。关于后台任务的具体实现,后面章节中会详细介绍~

2、可配置属性

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
/* identifier for the background session configuration */
@property (nullable, readonly, copy) NSString *identifier; // 后台任务的标识符

/* default cache policy for requests */
@property NSURLRequestCachePolicy requestCachePolicy; // 请求的缓存策略

/* default timeout for requests. This will cause a timeout if no data is transmitted
*for the given timeout value, and is reset whenever data is transmitted. */
@property NSTimeInterval timeoutIntervalForRequest; // 超时时长

/* allow request to route over cellular. */
@property BOOL allowsCellularAccess; // 是否允许蜂窝网路

/* allows background tasks to be scheduled at the discretion of the system for optimal performance. */
// 数据量大时,让系统自主优化后台任务(比如连上WiFi再开始传输)
@property (getter=isDiscretionary) BOOL discretionary;

/* The maximum number of simultanous persistent connections per host */
@property NSInteger HTTPMaximumConnectionsPerHost; // 单个主机允许的最大同时并发连接数

/* The cookie storage object to use, or nil to indicate that no cookies should be handled */
@property (nullable, retain) NSHTTPCookieStorage *HTTPCookieStorage;

/* The credential storage object, or nil to indicate that no credential storage is to be used */
@property (nullable, retain) NSURLCredentialStorage *URLCredentialStorage;

/* The URL resource cache, or nil to indicate that no caching is to be performed */
@property (nullable, retain) NSURLCache *URLCache;

3.2.创建会话

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

配置信息,包括cookie、缓存策略、代理、超时、证书配置等;

  • 参数2:delegate

会话的代理对象,处理鉴权、缓存策略等事宜,在AFN中是AFURLSessionManager类;

The session object keeps a strong reference to the delegate until your app exits or explicitly invalidates the session. If you do not invalidate the session by calling the invalidateAndCancel or finishTasksAndInvalidate method, your app leaks memory until it exits.

注意:session会强引用 delegate 对象直到通过invalidateAndCancelfinishTasksAndInvalidate将session置为无效,如果不将session作废就会造成内存泄露。

  • 参数3:queue

代理所处的队列,可以是主队列或我们创建的私有队列。

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.

注意:queue参数必须是串行队列,这是为了保证回调的顺序。如果为nil则session会自动创建一个串行队列执行代理。

3.3.创建任务

URLSession提供了数据下载和上传相关的API,通过不同接口可以创建不同类型的任务:

1
2
3
4
5
6
7
8
9
10
11
// 普通任务,返回的数据保存在内存中,不支持后台任务
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url;

// 上传任务,支持后台上传
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;

// 下载任务,保存到本地文件中,支持后台下载
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url;

// 数据流任务,从指定的服务器和端口建立一个 TCP/IP 连接
- (NSURLSessionStreamTask *)streamTaskWithHostName:(NSString *)hostname port:(NSInteger)port;

每个NSURLSession对象可以包含多个任务,类似于浏览器中多个窗口可以分别处理网页浏览和数据下载等任务。

3.4.会话作废

1
2
3
4
5
6
7
8
9
10
11
12
13
/* -finishTasksAndInvalidate returns immediately and existing tasks will be allowed
* to run to completion. New tasks may not be created. The session
* will continue to make delegate callbacks until URLSession:didBecomeInvalidWithError:
* has been issued.
*
* -finishTasksAndInvalidate and -invalidateAndCancel do not
* have any effect on the shared session singleton.
*
* When invalidating a background session, it is not safe to create another background
* session with the same identifier until URLSession:didBecomeInvalidWithError: has
* been issued.
*/
- (void)finishTasksAndInvalidate;

作用:将session作废,但允许未完成的task继续执行完。

此方法不会等待任务完成而立刻返回,URLSession会话对象调用此方法后将不能再重用,也不再接收新的task,已存在或在执行的task则会继续执行直到全部完成。

1
2
3
4
5
6
/* -invalidateAndCancel acts as -finishTasksAndInvalidate, but issues
* -cancel to all outstanding tasks for this session. Note task
* cancellation is subject to the state of the task, and some tasks may
* have already have completed at the time they are sent -cancel.
*/
- (void)invalidateAndCancel;

作用:将未完成的task取消,同时将session作废。

与第一个方法的区别在于,本方法会取消未完成的任务。注意,二者对于NSURLSession.sharedSession会话无效。

3.5.会话协议

NSURLSessionDelegate,用来处理session-level事件的协议,比如 session 生命周期的变化。

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
@protocol NSURLSessionDelegate <NSObject>
@optional

/* The last message a session receives. A session will only become
* invalid because of a systemic error or when it has been
* explicitly invalidated, in which case the error parameter will be nil.
*/
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error;

/* If implemented, when a connection level authentication challenge
* has occurred, this delegate will be given the opportunity to
* provide authentication credentials to the underlying
* connection. Some types of authentication will apply to more than
* one request on a given connection to a server (SSL Server Trust
* challenges). If this delegate message is not implemented, the
* behavior will be to use the default handling, which may involve user
* interaction.
*/
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
NSURLCredential *credential))completionHandler;

/* If an application has received an
* -application:handleEventsForBackgroundURLSession:completionHandler:
* message, the session delegate will receive this message to indicate
* that all messages previously enqueued for this session have been
* delivered. At this time it is safe to invoke the previously stored
* completion handler, or to begin any internal updates that will
* result in invoking the completion handler.
*/
// 后台任务完成时 触发此回调
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session;
@end

3.6.Task

数据请求可抽象为任务,即NSURLSessionTask,这是网络任务的基类,一般不直接使用此类,创建数据任务可使用其子类:

1
2
3
4
5
6
NSObject
└── NSURLSessionTask
├── NSURLSessionDataTask
│   └── NSURLSessionUploadTask
├── NSURLSessionDownloadTask
└── NSURLSessionStreamTask

每个类的具体作用下面章节中会继续介绍~此基类提供了任务状态和进度相关的属性,以及开始、暂停、取消等接口:

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
@interface NSURLSessionTask : NSObject <NSCopying, NSProgressReporting>

@property (readonly) NSUInteger taskIdentifier; /* an identifier for this task, assigned by and unique to the owning session */
@property (nullable, readonly, copy) NSURLRequest *originalRequest; /* may be nil if this is a stream task */
@property (nullable, readonly, copy) NSURLRequest *currentRequest; /* may differ from originalRequest due to http server redirection */
@property (nullable, readonly, copy) NSURLResponse *response; /* may be nil if no response has been received */

/*
* NSProgress object which represents the task progress.
* It can be used for task progress tracking.
*/
@property (readonly, strong) NSProgress *progress API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));

/*
* Start the network load for this task no earlier than the specified date. If
* not specified, no start delay is used.
*
* Only applies to tasks created from background NSURLSession instances; has no
* effect for tasks created from other session types.
*/
@property (nullable, copy) NSDate *earliestBeginDate API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));

/*
* The number of bytes that the client expects (a best-guess upper-bound) will
* be sent and received by this task. These values are used by system scheduling
* policy. If unspecified, NSURLSessionTransferSizeUnknown is used.
*/
@property int64_t countOfBytesClientExpectsToSend API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));
@property int64_t countOfBytesClientExpectsToReceive API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));


/* Byte count properties may be zero if no body is expected,
* or NSURLSessionTransferSizeUnknown if it is not possible
* to know how many bytes will be transferred.
*/

/* number of body bytes already received */
@property (readonly) int64_t countOfBytesReceived;

/* number of body bytes already sent */
@property (readonly) int64_t countOfBytesSent;

/* number of body bytes we expect to send, derived from the Content-Length of the HTTP request */
@property (readonly) int64_t countOfBytesExpectedToSend;

/* number of byte bytes we expect to receive, usually derived from the Content-Length header of an HTTP response. */
@property (readonly) int64_t countOfBytesExpectedToReceive;

/*
* The taskDescription property is available for the developer to
* provide a descriptive label for the task.
*/
@property (nullable, copy) NSString *taskDescription;

/*
* The current state of the task within the session.
*/
@property (readonly) NSURLSessionTaskState state;

/*
* The error, if any, delivered via -URLSession:task:didCompleteWithError:
* This property will be nil in the event that no error occured.
*/
@property (nullable, readonly, copy) NSError *error;

/* -cancel returns immediately, but marks a task as being canceled.
* The task will signal -URLSession:task:didCompleteWithError: with an
* error value of { NSURLErrorDomain, NSURLErrorCancelled }. In some
* cases, the task may signal other work before it acknowledges the
* cancelation. -cancel may be sent to a task that has been suspended.
*/
- (void)cancel;

/*
* Suspending a task will prevent the NSURLSession from continuing to
* load data. There may still be delegate calls made on behalf of
* this task (for instance, to report data received while suspending)
* but no further transmissions will be made on behalf of the task
* until -resume is sent. The timeout timer associated with the task
* will be disabled while a task is suspended. -suspend and -resume are
* nestable.
*/
- (void)suspend;
- (void)resume;

/*
* Sets a scaling factor for the priority of the task. The scaling factor is a
* value between 0.0 and 1.0 (inclusive), where 0.0 is considered the lowest
* priority and 1.0 is considered the highest.
*
* The priority is a hint and not a hard requirement of task performance. The
* priority of a task may be changed using this API at any time, but not all
* protocols support this; in these cases, the last priority that took effect
* will be used.
*
* If no priority is specified, the task will operate with the default priority
* as defined by the constant NSURLSessionTaskPriorityDefault. Two additional
* priority levels are provided: NSURLSessionTaskPriorityLow and
* NSURLSessionTaskPriorityHigh, but use is not restricted to these.
*/
@property float priority API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));

@end
  • NSURLSessionTaskDelegate
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
/*
* Messages related to the operation of a specific task.
*/
@protocol NSURLSessionTaskDelegate <NSURLSessionDelegate>
@optional

/*
* Sent when the system is ready to begin work for a task with a delayed start
* time set (using the earliestBeginDate property). The completionHandler must
* be invoked in order for loading to proceed. The disposition provided to the
* completion handler continues the load with the original request provided to
* the task, replaces the request with the specified task, or cancels the task.
* If this delegate is not implemented, loading will proceed with the original
* request.
*
* Recommendation: only implement this delegate if tasks that have the
* earliestBeginDate property set may become stale and require alteration prior
* to starting the network load.
*
* If a new request is specified, the allowsCellularAccess property from the
* new request will not be used; the allowsCellularAccess property from the
* original request will continue to be used.
*
* Canceling the task is equivalent to calling the task's cancel method; the
* URLSession:task:didCompleteWithError: task delegate will be called with error
* NSURLErrorCancelled.
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
willBeginDelayedRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLSessionDelayedRequestDisposition disposition, NSURLRequest * _Nullable newRequest))completionHandler
API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));

/*
* Sent when a task cannot start the network loading process because the current
* network connectivity is not available or sufficient for the task's request.
*
* This delegate will be called at most one time per task, and is only called if
* the waitsForConnectivity property in the NSURLSessionConfiguration has been
* set to YES.
*
* This delegate callback will never be called for background sessions, because
* the waitForConnectivity property is ignored by those sessions.
*/
- (void)URLSession:(NSURLSession *)session taskIsWaitingForConnectivity:(NSURLSessionTask *)task
API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0));

/* An HTTP request is attempting to perform a redirection to a different
* URL. You must invoke the completion routine to allow the
* redirection, allow the redirection with a modified request, or
* pass nil to the completionHandler to cause the body of the redirection
* response to be delivered as the payload of this request. The default
* is to follow redirections.
*
* For tasks in background sessions, redirections will always be followed and this method will not be called.
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler;

/* The task has received a request specific authentication challenge.
* If this delegate is not implemented, the session specific authentication challenge
* will *NOT* be called and the behavior will be the same as using the default handling
* disposition.
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;

/* Sent if a task requires a new, unopened body stream. This may be
* necessary when authentication has failed for any request that
* involves a body stream.
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
needNewBodyStream:(void (^)(NSInputStream * _Nullable bodyStream))completionHandler;

/* Sent periodically to notify the delegate of upload progress. This
* information is also available as properties of the task.
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend;

/*
* Sent when complete statistics information has been collected for the task.
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

/* Sent as the last message related to a specific task. Error may be
* nil, which implies that no error occurred and this task is complete.
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error;

@end

3.7.DataTask

NSURLSessionDataTask,上传数据并接收返回的数据,返回的数据被保存到内存中。

#GET请求示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask =[session dataTaskWithURL:
[NSURL URLWithString:url]
completionHandler:^(NSData *data,
NSURLResponse *response, NSError *error)
{
//data为服务器返回的数据
if (!error) {
UIImage *image = [UIImage imageWithData:data];
NSLog(@"+++文件大小:%lld",response.expectedContentLength);
}
}];
//启动任务
[dataTask resume];

dataTask也可以胜任 downloadTask 和 uploadTask 的工作。区别在于dataTask 不支持后台下载。

例如,dataTask一般用来上传表单数据,比如将用户名、密码等信息以GET的方式追加到请求的RUL.query部分;当然也可以将数据以POST请求的方式追加到http.body中;这时 datatask 的功能有点类似 uploadTask。

而服务器收到datatask上传的信息后,会返回一个状态码和对应的json数据,这时 dataTask 的功能又有点像 downloadTask;

尽管dataTask可以做这些事,URLSession还是对请求进行了详细分类,提供了专门的类单独负责上传、下载及数据流任务。

  • NSURLSessionDataDelegate
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
/*
* Messages related to the operation of a task that delivers data
* directly to the delegate.
*/
@protocol NSURLSessionDataDelegate <NSURLSessionTaskDelegate>
@optional
/* The task has received a response and no further messages will be
* received until the completion block is called. The disposition
* allows you to cancel a request or to turn a data task into a
* download task. This delegate message is optional - if you do not
* implement it, you can get the response as a property of the task.
*
* This method will not be called for background upload tasks (which cannot be converted to download tasks).
*/
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;

/* Notification that a data task has become a download task. No
* future messages will be sent to the data task.
*/
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask;

/*
* Notification that a data task has become a bidirectional stream
* task. No future messages will be sent to the data task. The newly
* created streamTask will carry the original request and response as
* properties.
*
* For requests that were pipelined, the stream object will only allow
* reading, and the object will immediately issue a
* -URLSession:writeClosedForStream:. Pipelining can be disabled for
* all requests in a session, or by the NSURLRequest
* HTTPShouldUsePipelining property.
*
* The underlying connection is no longer considered part of the HTTP
* connection cache and won't count against the total number of
* connections per host.
*/
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didBecomeStreamTask:(NSURLSessionStreamTask *)streamTask;

/* Sent when data is available for the delegate to consume. It is
* assumed that the delegate will retain and not copy the data. As
* the data may be discontiguous, you should use
* [NSData enumerateByteRangesUsingBlock:] to access it.
*/
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data;

/* Invoke the completion routine with a valid NSCachedURLResponse to
* allow the resulting data to be cached, or pass nil to prevent
* caching. Note that there is no guarantee that caching will be
* attempted for a given resource, and you should not rely on this
* message to receive the resource data.
*/
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse * _Nullable cachedResponse))completionHandler;

@end

如果既设置了completionHandler,又实现了NSURLSessionTaskDelegate或其子类的协议方法,则优先执行 block,代理方法不再执行。

3.8.DownloadTask

NSURLSessionDownloadTask,将数据下载并保存到文件中,且下载文件时不需要考虑边下载边写入沙盒的问题,苹果都帮我们做好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDownloadTask *dataTask = [session downloadTaskWithURL:[NSURL URLWithString:url]
completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
if (!error) {
NSLog(@"++++文件位置:%@ \n文件大小:%lld",
location,response.expectedContentLength);

NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *filePath = [caches stringByAppendingPathComponent:
response.suggestedFilename];
//移动文件
[[NSFileManager defaultManager] moveItemAtPath:location.path
toPath:filePath error:nil];
}
}];
//开始任务
[dataTask resume];

输出日志:

1
2
3
4
5
++++文件位置:Users/xxx/Library/Developer/CoreSimulator/
Devices/2EDED966-0D34-4965-A946-F547BBCE33DA/data/
Containers/Data/Application/5A2FAF81-62D5-485A-B059-22AD6DECA518
/tmp/CFNetworkDownload_JVNOrU.tmp
文件大小:100626

从回调的参数可以看到,并没有 NSData 字段传回来。回调中包括了一个 location 参数,它就是下载好的文件在沙盒中的地址。下载好的文件被放到tmp目录下。由于tmp目录下的文件随时可能被系统自动删除,我们在回调中把文件移动指定的目录下即可。

  • NSURLSessionDownloadDelegate

上面的示例中,NSURLSession 通过 block 的方式返回数据,这种方式有个不好的地方是无法监听下载进度。如果想要在接收数据过程中做进一步的处理,可以通过 NSURLSessionDownloadDelegate 协议来实现。(更新:iOS11后可以使用新增的 NSProgress * progress 属性来监听进度了)

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
- (void)sendRequest
{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration
defaultSessionConfiguration];

config.timeoutIntervalForRequest = 60;
config.allowsCellularAccess = YES;

NSURLSession *session = [NSURLSession sessionWithConfiguration:config
delegate:self delegateQueue:nil];

NSURLSessionDownloadTask *dataTask = [session downloadTaskWithURL:
[NSURL URLWithString:url]];

//开始任务
[dataTask resume];
}

#pragma mark -NSURLSessionDownloadDelegate

//下载完成
-(void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
}

//每次写入沙盒完成后
-(void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
NSLog(@"+++下载进度:%f",(double)totalBytesWritten/totalBytesExpectedToWrite);
}

//恢复下载后
-(void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{
}

3.9.后台任务

对于一些耗时或者优先级不高的数据传输任务,可以在后台创建一个任务进行传输。URLSession中支持后台传输,并且可以在应用进入后台或者被系统杀掉后,继续执行传输任务。

  • 创建后台会话、发起任务

这里需要给Configuration指定一个标识符,方便后台任务完成后回调函数中继续使用此标识符处理任务。同时建议打开其中两个选项,以便系统对任务进行优化。

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
//HelNetHelper.m

#import "HelNetHelper.h"

NSString * const k_BackSessionID = @"k_URLSession_001";

@implementation HelNetHelper

+ (instancetype)shareInstance{
static HelNetHelper *instance;
static dispatch_once_t token;
_dispatch_once(&token, ^{
instance = [[HelNetHelper alloc] init];
});
return instance;
}

- (NSURLSession *)backgroundSession {
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 1.配置后台session,指定唯一标识符。
NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:k_BackSessionID];
config.discretionary = YES; //允许系统采取最优时机执行传输任务
config.sessionSendsLaunchEvents = YES; // 当session中有任务完成时允许应用恢复或启动
// 2.创建session 设置代理和代理所在队列
session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
});
return session;
}

// 发起后台下载任务
- (void)startBackgroundTask{
NSLog(@"+++start session~");
NSURLSession *session = [[HelNetHelper shareInstance] backgroundSession];
NSURLSessionDownloadTask * task = [session downloadTaskWithURL:[NSURL URLWithString:@"https://dl.360safe.com/pclianmeng/n/1__6000322__00__7777772e68616f373339392e636f6d__079b.exe"]];
[task resume];
}

// 取消任务
- (void)stop{
NSLog(@"+++task canceled~");
[[[HelNetHelper shareInstance] backgroundSession] invalidateAndCancel];
}

#pragma mark -URLSession Delegate

// session中所有task都已完成时 回调此方法
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
NSLog(@"++++thread on finishEvent:%@",[NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
self.completeBlock();
});
}

// 文件下载完成
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location{
NSLog(@"+++downloadtask finished, file in :%@~",location.path);
//移动文件
//[[NSFileManager defaultManager] moveItemAtURL:location toURL:targetPath error:nil];
//删除文件
NSError *error = nil;
[[NSFileManager defaultManager] removeItemAtURL:location error:&error];
}

//下载进行中 跟踪进度
-(void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
NSLog(@"+++下载进度:%f",(double)totalBytesWritten/totalBytesExpectedToWrite);
}

// 下载成功或失败 都会回调此方法,只是失败时error不为空,可处理错误信息
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error
{
if (error) {
NSLog(@"++download failed:%@",error.description);
}else{
NSLog(@"+++download succeed~");
}
}
@end
  • AppDelegate收到下载完成回调

在AppDelegate.m中将回调中的completionHandler保存起来,以便后面继续使用。

1
2
3
4
5
6
7
8
9
10
11
12
// AppDelegate.m

- (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)(void))completionHandler
{
// 使用与挂起前后台任务相同的标识符 重新创建session并设置代理,系统会自动将之前的任务与当前session关联起来
[[HelNetHelper shareInstance] backgroundSession];

[HelNetHelper shareInstance].completeBlock = completionHandler;
NSLog(@"++++handleEventsForBackgroundURLSession:%@~",identifier);
}
  • HelNetHelper中回调completeBlock

上面AppDelegate收到回调后,系统会在 HelNetHelper 中回调 NSURLSessionDelegate 协议的以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//HelNetHelper.m

/* If an application has received an
* -application:handleEventsForBackgroundURLSession:completionHandler:
* message, the session delegate will receive this message to indicate
* that all messages previously enqueued for this session have been
* delivered. At this time it is safe to invoke the previously stored
* completion handler, or to begin any internal updates that will
* result in invoking the completion handler.
*/
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
NSLog(@"++++thread on finishEvent:%@",[NSThread currentThread]);
// 当前回调可能在私有队列中 需要回到主线程
dispatch_async(dispatch_get_main_queue(), ^{
self.completeBlock();
});
}

回调中我们调用之前保存起来的completeBlock()即可。

注意,我们创建session时delegateQueue参数传的是nil,所以任务的回调 NSURLSessionDelegate 会在一个私有串行队列中执行,苹果要求我们在执行completeBlock()回调时必须在主线程中。

The URL session API itself is fully thread-safe. You can freely create sessions and tasks in any thread context. When your delegate methods call the provided completion handlers, the work is automatically scheduled on the correct delegate queue.

The system may call the URLSessionDidFinishEventsForBackgroundURLSession: session delegate method on a secondary thread. However, in iOS, your implementation of that method may need to call a completion handler provided to you in your application:handleEventsForBackgroundURLSession:completionHandler: app delegate method. You must call that completion handler on the main thread.

  • 处理下载的文件

当应用回调完completeBlock()后,下载任务就自动完成,随后回调:

1
2
3
4
5
6
7
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
//移动文件
[[NSFileManager defaultManager] moveItemAtURL:location toURL:targetPath error:nil];
}

将下载的文件从缓存目录中移到你指定的目录中即可。

  • 应用挂起时被系统杀掉情况下任务的恢复

应用被挂起时有可能被系统杀掉,后台任务在单独的进程中继续执行,当任务完成时系统会自动在后台重启你的应用。应用启动阶段你可以使用与之前的后台任务相同的标识符重新创建URLSession会话,系统会自动将原来的后台任务与你的新session关联起来。这样不论应用是由用户启动还是由系统启动,后台任务都会继续回调各种事件。

1
2
3
4
5
6
7
8
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// 重用后台任务标识符 重建session 设置回调代理
[[HelNetHelper shareInstance] backgroundSession];

return YES;
}
  • 测试

在ViewController中 点击按钮开始下载任务:

1
2
3
- (IBAction)onClick:(id)sender {
[[HelNetHelper shareInstance] backgroundTask];
}

在模拟器中调试,控制台会不断打印当前下载的进度。此时可先直接command + .停止模拟器并记住控制台中最后一条进度的数值;

1
2
3
4
5
+++start session~
+++下载进度:0.000021
+++下载进度:0.000025
...
+++下载进度:0.012226

随后重新运行模拟器,可以看到日志继续打印,并且进度并不是从0开始,而是以比之前数值更大的进度继续进行。

1
2
3
+++下载进度:0.089701
+++下载进度:0.089829
...

这说明虽然应用退出但是后台任务仍在单独的进程中运行,重新启动应用后任务继续并执行回调。

如果开始下载后,直接进入后台,直到文件下载完成,则完整的日志如下:

1
2
3
4
5
6
7
8
9
+++start session~
+++下载进度:0.000381
+++下载进度:0.000573
+++下载进度:0.000765
+++下载进度:0.001149
++++handleEventsForBackgroundURLSession:k_URLSession_001~
+++downloadtask finished, file in :/Users/xxx/Library/Developer/CoreSimulator/Devices/xxx/Library/Caches/com.apple.nsurlsessiond/Downloads/Helko/CFNetworkDownload_MaGyNe.tmp~
+++download succeed~
++++thread on finishEvent:<NSThread: 0x6000020a56c0>{number = 3, name = (null)}

即任务会在后台自动进行,直到最终完成。任务完成后,自动调用AppDelegate和我们工具类中的URLSession各项回调。

  • 后台任务的限制条件

后台session中的数据传输任务是在一个单独的进程中执行的,重启应用是比较昂贵的操作,所以后台session会有一些限制条件:

  1. 后台session必须提供一个delegate处理事件;
  2. 只支持 HTTP 和 HTTPS 协议,不支持私有协议;
  3. 允许重定向且会直接执行,不会回调 URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:方法;
  4. 只支持上传和下载,不支持dataTask;且上传的文件必须在文件夹中,上传内存中的数据或者数据流在应用退出时会失败。

3.10.断点下载

NSURLSessionDownloadTask 提供了断点下载的相关方法。

1
2
3
[dataTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
_mResumeData = resumeData;
}];

此方法用来取消下载任务,取消下载后的回调中,参数 resumeData 包含了继续下载文件的位置信息。resumeData 只包含了url和已经下载了多少数据,不会很大,不用担心内存问题。

恢复下载时,可使用下面的方法:

1
2
mDataTask = [mURLSession downloadTaskWithResumeData:_mResumeData];
[mDataTask resume];

另外,由于下载失败导致的下载中断会进入此协议方法,也可以得到用来恢复的数据:

1
2
3
4
5
6
7
- (void)URLSession:(NSURLSession *)session 
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
// 保存恢复数据
_mResumeData = error.userInfo[NSURLSessionDownloadTaskResumeData];
}

完结,撒花~


文件下载
https://davidlii.cn/2017/12/22/download.html
作者
Davidli
发布于
2017年12月22日
许可协议