一、概述 NSURLSession 在iOS7中推出,旨在替换之前的NSURLConnection
NSURLSession的使用相对于之前的NSURLConnection更简单,而且不用处理Runloop相关的东西;
暂停、停止、重启网络任务,不再需要 NSOperation
封装;
支持后台运行的网络任务;
NSURLSession支持http2.0协议;
提供了全局的session并且可以统一配置,使用更加方便;
同一个session发送多次请求,只需要建立一次连接(复用了TCP)。
2015年 RFC 7540标准发布了http 2.0版本,http 2.0 版本中包含很多新的特性,在传输速度上也有很明显的提升。NSURLSession 从 iOS9.0 开始,对 http 2.0 提供了支持。
NSURLSession API 由三部分构成:
NSURLSession:请求会话对象,可以用系统提供的单例对象,也可以自己创建。
NSURLSessionConfiguration:对session会话进行配置,一般都采用default。
NSURLSessionTask:负责执行具体请求的task,由session创建。使用同一个session的task可以共享连接和请求信息。
协议支持:
NSURLSession 类支持data、file、ftp、http 和 https URL schemes,透明支持代理服务器和 SOCKS 网关,如用户系统首选项中配置的那样。
NSURLSession 支持 HTTP/1.1、HTTP/2 和 HTTP/3 协议。 如 RFC 7540 所述,HTTP/2 支持需要支持应用层协议协商 (ALPN) 的服务器。
还可以通过继承 NSURLProtocol 来添加对开发者自定义的网络协议和 URL 方案的支持(供您的应用程序私人使用)。
线程安全:
URL Session API 是线程安全的。 可以在任何线程上下文中自由创建 sessions 和 tasks。 当调用提供的 completion handlers 时,工作会自动安排在正确的 delegate queue 中。
二、NSURLSession 2.1 Session的创建 您的应用程序创建一个或多个 NSURLSession 实例,每个实例协调一组相关的数据传输任务。例如,如果您正在创建一个 Web 浏览器,您的应用程序可能会为每个选项卡或窗口创建一个会话,或者一个会话用于交互使用,另一个会话用于后台下载。在每个 session 中,可以添加一系列task,每个 task 都代表对特定 URL 的请求。
因为NSURLSession的TCP连接复用特性,所以尽量共用Session,以提升请求性能。
NSURLSession有三种方式创建。
1. sharedSession 系统维护的一个单例对象。
1 2 @property (class , readonly , strong ) NSURLSession *sharedSession;
2. NSURLSessionConfiguration 在NSURLSession初始化时传入一个NSURLSessionConfiguration,这样可以自定义身份验证,超时时长,缓存策略,Cookie等配置。
1 + (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
3. delegate与delegate Queue 如果想更好的控制请求过程以及回调线程,可以使用下面的方法进行初始化操作,传入delegate、delegateQueue来设置回调对象和回调线程。
1 2 3 4 5 6 7 8 + (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id <NSURLSessionDelegate >)delegate delegateQueue:(nullable NSOperationQueue *)queue;
如果你指定了一个delegate,session对象保持对 delegate 的强引用 ,直到应用退出或调用 invalidateAndCancel 或 finishTasksAndInvalidate 方法显式使会话无效,此时delegate被发送 URLSession:didBecomeInvalidWithError:
消息。
2.2 NSURLSessionConfigration NSURLSessionConfiguration配置NSURLSession
在网络交互过程中的各种属性。包括:身份验证,超时时长,缓存策略,Cookie等。
2.2.1 创建方式 NSURLSessionConfiguration的创建,有三种方式:(注意:前两种方式不是单例方法,而是类方法,创建的是不同对象。各自的配置不会相互影响。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @property (class , readonly , strong ) NSURLSessionConfiguration *defaultSessionConfiguration;@property (class , readonly , strong ) NSURLSessionConfiguration *ephemeralSessionConfiguration;+ (NSURLSessionConfiguration *)backgroundSessionConfiguration:(NSString *)identifier;
第三种方式创建的Task的应用:
在iOS中,当后台传输完成或需要凭据时,如果您的应用程序不再运行,您的应用程序会在后台自动重新启动,并且应用程序的 UIApplicationDelegate 会收到 application:handleEventsForBackgroundURLSession:completionHandler:
消息。此调用包含导致应用程序启动的session的identifier。
如果一个下载任务正在进行中,程序被kill,可以在程序退出之前保存identifier。下次进入程序后通过identifier恢复之前的任务,系统会将NSURLSession及NSURLSessionConfiguration和之前的下载任务进行关联,并继续之前的任务。(后台能下载,手动kill可是不能继续下载的 )。详见文件下载一节
一般基本上都是使用默认设置。
2.2.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 @property NSString * identifier; @property NSURLRequestCachePolicy requestCachePolicy; @property NSTimeInterval timeoutIntervalForRequest; @property NSTimeInterval timeoutIntervalForResource; @property NSURLRequestNetworkServiceType networkServiceType;@property BOOL allowsCellularAccess; @property BOOL allowsExpensiveNetworkAccess;@property BOOL allowsConstrainedNetworkAccess;@property BOOL waitsForConnectivity;@property (getter =isDiscretionary) BOOL discretionary; @property NSString * sharedContainerIdentifier;@property BOOL sessionSendsLaunchEvents; @property NSDictionary * connectionProxyDictionary;@property tls_protocol_version_t TLSMinimumSupportedProtocolVersion;@property tls_protocol_version_t TLSMaximumSupportedProtocolVersion;@property BOOL HTTPShouldUsePipelining; @property BOOL HTTPShouldSetCookies;@property NSHTTPCookieAcceptPolicy HTTPCookieAcceptPolicy;@property NSDictionary * HTTPAdditionalHeaders; @property NSHTTPCookieStorage * HTTPCookieStorage; @property NSURLCredentialStorage * URLCredentialStorage; @property NSInteger HTTPMaximumConnectionsPerHost; @property NSURLCache * URLCache; @property BOOL shouldUseExtendedBackgroundIdleMode;@property NSArray <Class> * protocolClasses;@property NSURLSessionMultipathServiceType multipathServiceType;
附加说明:
HTTPCookieStorage
要禁用 cookie 存储,请将此属性设置为 nil。
对于 default和 background sessions,默认值为 sharedHTTPCookieStorage cookie 存储对象。
对于 ephemeralSessionConfiguration 会话,默认值是一个私有的 cookie 存储对象,它只将数据存储在内存中,并在您使会话无效时被销毁。
URLCredentialStorage(基本同HTTPCookieStorage)
URLCache
要禁用缓存,请将此属性设置为 nil。
对于默认会话,默认值为sharedURLCache
。
对于后台会话,默认值为 nil。
对于临时会话,默认值是仅将数据存储在内存中的私有缓存对象,并在您使会话无效时被销毁。
2.2.3 URLCache NSURLCache
提供了Memory
和Disk
的缓存,在创建时需要为其分别指定Memory
和Disk
的大小,以及存储的文件位置。使用NSURLCache
不用考虑磁盘空间不够,或手动管理内存空间的问题,如果发生内存警告系统会自动清理内存空间。但是NSURLCache
提供的功能非常有限,项目中一般很少直接使用它来处理缓存数据,还是用数据库比较多。
1 2 3 [[NSURLCache alloc] initWithMemoryCapacity:30 * 1024 * 1024 diskCapacity:30 * 1024 * 1024 directoryURL:[NSURL URLWithString:filePath]];
使用NSURLCache
还有一个好处,就是可以由服务端来设置资源过期时间,在请求服务端后,服务端会返回Cache-Control
来说明文件的过期时间。NSURLCache
会根据NSURLResponse
来自动完成过期时间的设置。
2.2.4 HTTPMaximumConnectionsPerHost 此属性决定了根据本configuration创建的 sessions 中的任务与每个主机建立的最大同时连接数。
此限制是针对每个 session 的,因此如果使用了多个 session,那应用程序作为一个整体可能会超过此限制。 此外,根据与 Internet 的连接,session 使用的限制可能低于指定的限制。
但最好不要为了增加并发而创建多个Session ,创建多个Session的目的应该是为了对不同的Task使用不同的策略,来实现更符合我们需求的交互。
macOS 中的默认值为 6,iOS 中的默认值为 4。
2.3 特性 — 连接复用 HTTP
是基于传输层协议TCP
的,通过TCP
发送网络请求都需要先进行三次握手,建立网络请求后再发送数据,请求结束时再经历四次挥手。HTTP1.0
开始支持keep-alive
,keep-alive
可以保持已经建立的链接,如果是相同的域名,在请求连接建立后,后面的请求不会立刻断开,而是复用现有的连接。从HTTP1.1
开始默认开启keep-alive
。
请求是在请求头中设置下面的参数,服务器如果支持keep-alive
的话,响应客户端请求时,也会在响应头中加上相同的字段。
如果想断开keep-alive
,可以在请求头中加上下面的字段,但一般不推荐这么做。
如果通过NSURLSession
来进行网络请求的话,需要使用同一个 NSURLSession 对象,以复用 TCP 连接 (很容易地就能通过抓包证明 )。如果创建新的session
对象则不能复用之前的链接。keep-alive
可以保持请求的连接,苹果允许在iOS
上最大保持有4个连接,Mac
则是6个连接。
2.4 特性 — pipeline
在HTTP1.1
中,基于keep-alive
,还可以将请求进行管线化。和相同后端服务,TCP
层建立的链接,一般都需要前一个请求返回后,后面的请求再发出。但pipeline
就可以不依赖之前请求的响应,而发出后面的请求。
pipeline
依赖客户端和服务器都有实现,服务端收到客户端的请求后,要按照先进先出的顺序进行任务处理和响应。pipeline
依然存在之前非pipeline
的问题,就是前面的请求如果出现问题,会阻塞当前连接影响后面的请求。
pipeline
对于请求大文件并没有提升作用,只是对于普通请求速度有提升。在NSURLSessionConfiguration
中可以设置HTTPShouldUsePipelining
为YES
,开启管线化,此属性默认为NO
。
三、NSURLSessionTask 3.1 继承体系 通过NSURLSession发起的每个请求,都会被封装为一个NSURLSessionTask任务,但一般不会直接是NSURLSessionTask类,而是基于不同任务类型,被封装为其对应的子类。
1 2 3 4 5 6 7 8 NSURLSessionTask | \ \ \ | \ \ NSURLSessionWebSocketTask:通过 WebSockets 协议标准进行通信。 | \ NSURLSessionStreamTask :允许通过TCP/IP,可选的安全握手以及代理导航直接连接到给定的主机和端口。 | NSURLSessionDownloadTask :处理下载任务,获取下载进度,支持断点续传(暂停、取消、恢复。前提是服务器支持) NSURLSessionDataTask :处理普通的Get、Post请求。 | NSURLSessionUploadTask:处理上传请求,可以传入对应的上传文件或路径。
主要方法都定义在父类NSURLSessionTask中。下面是一些关键方法或属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @interface NSURLSessionTask : NSObject <NSCopying , NSProgressReporting > currentRequest originalRequest taskIdentifier priority state -(void )resume; -(void )suspend; -(void )cancel; @end
3.2 Task的创建 所有的 task 创建出来默认都是suspended状态,必须调用 resume 方法开始任务。
3.2.1 代理回调方式 NSURLSession 提供有普通创建 task的方式,创建后可以通过重写代理方法,获取对应的回调和参数。这种方式对于请求过程比较好控制。
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 @interface NSURLSession : NSObject - (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request; - (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url; - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL; - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData; - (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request; - (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request; - (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url; - (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData; - (NSURLSessionStreamTask *)streamTaskWithHostName:(NSString *)hostname port:(NSInteger )port; - (NSURLSessionWebSocketTask *)webSocketTaskWithURL:(NSURL *)url; - (NSURLSessionWebSocketTask *)webSocketTaskWithURL:(NSURL *)url protocols:(NSArray <NSString *>*)protocols; - (NSURLSessionWebSocketTask *)webSocketTaskWithRequest:(NSURLRequest *)request; @end
3.2.2 Block回调方式 除此之外,NSURLSession也提供了block的方式创建task,直接传入URL或NSURLRequest,即可直接在block中接收返回数据。
completionHandler和delegate是互斥的,completionHandler的优先级大于delegate 。相对于普通创建方法,block方式更偏向于面向结果的创建,可以直接在completionHandler中获取返回结果,但不能控制请求过程。
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 @interface NSURLSession (NSURLSessionAsynchronousConvenience )- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *, NSURLResponse *, NSError *))completionHandler; - (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData *, NSURLResponse *, NSError *))completionHandler; - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL completionHandler:(void (^)(NSData *, NSURLResponse *, NSError *))completionHandler; - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(nullable NSData *)bodyData completionHandler:(void (^)(NSData *, NSURLResponse *, NSError *))completionHandler; - (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURL *, NSURLResponse *, NSError *))completionHandler; - (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSURL *, NSURLResponse *, NSError *))completionHandler; - (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData completionHandler:(void (^)(NSURL *, NSURLResponse *, NSError *))completionHandler; @end
3.3 Task的获取 可以通过下面的两个方法,获取当前session
对应的所有task
:
1 2 3 4 5 6 7 8 9 @interface NSURLSession : NSObject - (void )getTasksWithCompletionHandler:(void (^)(NSArray <NSURLSessionDataTask *> *dataTasks, NSArray <NSURLSessionUploadTask *> *uploadTasks, NSArray <NSURLSessionDownloadTask *> *downloadTasks))completionHandler; - (void )getAllTasksWithCompletionHandler:(void (^)(NSArray <__kindof NSURLSessionTask *> *tasks))completionHandler; @end
AFN
中,使用getTasksWithCompletionHandler
来获取当前session
的task
,并将AFURLSessionManagerTaskDelegate
的回调都置为nil
,以防止崩溃。
3.4 示例: 发起网络请求 通过NSURLSession发起一个网络请求:
1 2 3 4 5 6 7 8 9 10 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];
四、Session、Task相关的代理方法 4.1 继承体系 NSURLSession
中定义了一系列代理,并遵循上面的继承关系。父级代理定义的都是公共方法。
如果想执行某类任务、精细地控制请求和响应过程,只需要遵守Task对应的Delegate即可。例如:
执行普通Post
、上传(upload
)请求,则遵守NSURLSessionDataDelegate
;
执行下载任务则遵循NSURLSessionDownloadDelegate
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 @protocol NSURLSessionDelegate <NSObject >@optional - (void )URLSession: didBecomeInvalidWithError: - (void )URLSession: didReceiveChallenge: completionHandler: - (void )URLSessionDidFinishEventsForBackgroundURLSession: @end @protocol NSURLSessionTaskDelegate <NSURLSessionDelegate >@optional - (void )URLSession: task: didCompleteWithError: - (void )URLSession: task: willPerformHTTPRedirection: newRequest: completionHandler: - (void )URLSession: task: didSendBodyData: totalBytesSent: totalBytesExpectedToSend: - (void )URLSession: task: needNewBodyStream: - (void )URLSession: task: didReceiveChallenge: completionHandler: - (void )URLSession: task: willBeginDelayedRequest: completionHandler: - (void )URLSession: taskIsWaitingForConnectivity: - (void )URLSession: task: didFinishCollectingMetrics: @end @protocol NSURLSessionDataDelegate <NSURLSessionTaskDelegate >@optional - (void )URLSession: dataTask: didReceiveResponse: completionHandler: - (void )URLSession: dataTask: didBecomeDownloadTask: - (void )URLSession: dataTask: didBecomeStreamTask: - (void )URLSession: dataTask: didReceiveData: - (void )URLSession: dataTask: willCacheResponse: completionHandler: @end @protocol NSURLSessionDownloadDelegate <NSURLSessionTaskDelegate >- (void )URLSession: downloadTask: didFinishDownloadingToURL:(NSURL *)location; @optional - (void )URLSession: downloadTask: didResumeAtOffset: expectedTotalBytes: - (void )URLSession: downloadTask: didWriteData: totalBytesWritten: totalBytesExpectedToWrite: @end @protocol NSURLSessionStreamDelegate <NSURLSessionTaskDelegate >@optional - (void )URLSession: betterRouteDiscoveredForStreamTask: - (void )URLSession: streamTask: didBecomeInputStream: outputStream: - (void )URLSession: readClosedForStreamTask: - (void )URLSession: writeClosedForStreamTask: @end @protocol NSURLSessionWebSocketDelegate <NSURLSessionTaskDelegate >@optional - (void )URLSession: webSocketTask: didOpenWithProtocol: - (void )URLSession: webSocketTask: didCloseWithCode: reason: @end
4.2 示例: 请求重定向 HTTP
协议中定义了例如301等重定向状态码,通过下面的代理方法,可以处理重定向任务。
可以根据response
创建一个新的request
,也可以直接用系统生成的request
,并在completionHandler
回调中传入,完成这次重定向。
如果想终止这次重定向,在completionHandler
传入nil
即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 - (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); } }
五、NSURLSessionTaskMetrics 在日常开发过程中,经常遇到页面加载太慢的问题,这很大一部分原因都是因为网络导致的。所以,查找网络耗时的原因并解决,就是一个很重要的任务了。苹果对于网络检查提供了NSURLSessionTaskMetrics
类来进行检查,NSURLSessionTaskMetrics
是对应NSURLSessionTaskDelegate
的,每个task结束时都会回调下面的方法,并且可以获得一个metrics
对象。
1 2 3 - (void )URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics;
NSURLSessionTaskMetrics
可以很好的帮助我们分析网络请求的过程,以找到耗时原因。除了这个类之外,NSURLSessionTaskTransactionMetrics
类中承载了更详细的数据。
1 2 3 4 5 6 7 8 @interface NSURLSessionTaskMetrics : NSObject @property NSArray <NSURLSessionTaskTransactionMetrics *> *transactionMetrics;@property NSDateInterval *taskInterval;@property NSUInteger redirectCount; @end
5.1 TransactionMetrics NSURLSessionTaskTransactionMetrics
中的属性都是用来做统计的,功能都是记录某个值,并没有逻辑上的意义。所以这里就对一些主要的属性做一下解释,基本涵盖了大部分属性,其他就不管了。
下面这张网图,标示了NSURLSessionTaskTransactionMetrics
的属性在请求过程中处于什么位置。
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 @property (copy , readonly ) NSURLRequest *request;@property (nullable , copy , readonly ) NSURLResponse *response;@property (nullable , copy , readonly ) NSDate *fetchStartDate;@property (nullable , copy , readonly ) NSDate *domainLookupStartDate;@property (nullable , copy , readonly ) NSDate *domainLookupEndDate;@property (nullable , copy , readonly ) NSDate *connectStartDate;@property (nullable , copy , readonly ) NSDate *connectEndDate;@property (nullable , copy , readonly ) NSDate *secureConnectionStartDate;@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;@property (nullable , copy , readonly ) NSString *networkProtocolName;@property (assign , readonly , getter =isProxyConnection) BOOL proxyConnection;@property (assign , readonly , getter =isReusedConnection) BOOL reusedConnection;@property (assign , readonly ) NSURLSessionTaskMetricsResourceFetchType resourceFetchType;@property (nullable , copy , readonly ) NSString *localAddress;@property (nullable , copy , readonly ) NSNumber *localPort;@property (nullable , copy , readonly ) NSString *remoteAddress;@property (nullable , copy , readonly ) NSNumber *remotePort;@property (nullable , copy , readonly ) NSNumber *negotiatedTLSProtocolVersion;@property (readonly , getter =isCellular) BOOL cellular;
六、NSURLSession文件上传 6.1 表单上传 上传文件现在主流的方式,都是采取表单上传的方式,也就是multipart/from-data
,AFNetworking
对表单上传也有很有的支持。
常见的几种Content-Type
:
1 2 3 4 Content-Type: text/html; charset=utf-8 Content-Type: application/json Content-Type: application/x-www-form-urlencoded Content-Type: multipart/form-data; boundary=something
表单上传需要遵循下面的格式进行上传:(multipart/form-data
规范定义在rfc2388 ,详细字段可以看一下规范)
multipart/form-data
:表示包含了一系列的部分(parts)。 每个part都应包含一个content-disposition
header,值为form-data
;包含 name
参数,值为表单(form)中的原始字段名称。
multipart/form-data只是multipart的一种。其他的比如multipart/mixed, multipart/related和multipart/alternative等
boundary
: 是一个16进制字符串,可以是任何且唯一的。boundary
的功能用来进行字段分割,区分开不同的参数部分。
name
: 是一个表单字段名,每一个字段名会对应一个子部分。在同一个字段名对应多个文件的情况下,则多个子部分共用同一个字段名。
filename
:是要传送的文件的初始名称的字符串。这个参数总是可选的,而且不能盲目使用:路径信息必须舍掉,同时要进行一定的转换以符合服务器文件系统规则。
这个参数主要用来提供展示性信息。
当与 Content-Disposition: attachment 一同使用的时候,它被用作”保存为”对话框中呈现给用户的默认文件名。
1 2 3 4 5 6 7 8 9 10 --boundary Content-Disposition: form-data; name="参数名" 参数值 --boundary Content-Disposition:form-data;name="表单控件名" ;filename="上传文件名" Content-Type:mime type 要上传文件二进制数据 --boundary--
拼接上传文件基本上可以分为下面三部分,上传参数、上传信息、上传文件。并且通过UTF-8
格式进行编码,服务端也采用相同的解码方式,则可以获得上传文件和信息。需要注意的是,换行符数量是固定的 ,这都是固定的协议格式,不要多或者少,会导致服务端解析失败。
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 - (NSData *)writeMultipartFormData:(NSData *)data parameters:(NSDictionary *)parameters { if (data.length == 0 ) { return nil ; } NSMutableData *formData = [NSMutableData data]; NSData *lineData = [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding ]; NSData *boundary = [kBoundary dataUsingEncoding:NSUTF8StringEncoding ]; [parameters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { [formData appendData:boundary]; [formData appendData:lineData]; NSString *thisFieldString = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n%@" , key, obj]; [formData appendData:[thisFieldString dataUsingEncoding:NSUTF8StringEncoding ]]; [formData appendData:lineData]; }]; [formData appendData:boundary]; [formData appendData:lineData]; NSString *thisFieldString = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\nContent-Type: %@" , @"name" , @"filename" , @"mimetype" ]; [formData appendData:[thisFieldString dataUsingEncoding:NSUTF8StringEncoding ]]; [formData appendData:lineData]; [formData appendData:lineData]; [formData appendData:data]; [formData appendData:lineData]; [formData appendData: [[NSString stringWithFormat:@"--%@--\r\n" , kBoundary] dataUsingEncoding:NSUTF8StringEncoding ]]; return formData; }
除此之外,表单提交还需要设置请求头的Content-Type
和Content-Length
。
设置Content-Type
时,一定要加上boundary
,这个boundary
和拼接上传文件的boundary
需要是同一个。服务端从请求头拿到boundary
,来解析上传文件。
Content-Length
并不是强制要求的,要看后端的具体支持情况。
1 2 3 4 5 6 NSString *headerField = [NSString stringWithFormat:@"multipart/form-data; charset=utf-8; boundary=%@" , kBoundary];[request setValue:headerField forHTTPHeaderField:@"Content-Type" ]; NSUInteger size = [[[NSFileManager defaultManager] attributesOfItemAtPath:uploadPath error:nil ] fileSize];headerField = [NSString stringWithFormat:@"%lu" , size]; [request setValue:headerField forHTTPHeaderField:@"Content-Length" ];
6.1.3 创建NSURLSessionUploadTask 随后我们通过下面的代码创建NSURLSessionUploadTask
,并调用resume
发起请求,实现对应的代理回调即可。
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 NSURLSessionUploadTask *uploadTask = [self .backgroundSession uploadTaskWithRequest:request fromData:fromData];[uploadTask resume]; - (void )URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { } - (void )URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend { } - (void )URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { } - (void )URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { }
6.2 分片上传 客户端有时候需要给服务端上传大文件,进行大文件肯定不能全都加载到内存里,一口气都传给服务器。进行大文件上传时,一般都会对需要上传的文件进行分片,分片后逐个文件进行上传。需要注意的是,分片上传和断点续传并不是同一个概念,上传并不支持断点续传。
进行分片上传时,需要对本地文件进行读取,我们使用NSFileHandle
来进行文件读取。NSFileHandle
提供了一个偏移量的功能,我们可以将handle
的当前读取位置seek
到上次读取的位置,并设置本次读取长度,读取的文件就是我们指定文件的字节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 - (NSData *)readNextBuffer { if (self .maxSegment <= self .currentIndex) { return nil ; } if (!self .fileHandler){ NSString *filePath = [self uploadFile]; NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath]; self .fileHandler = fileHandle; } [self .fileHandler seekToFileOffset:(self .currentIndex) * self .segmentSize]; NSData *data = [self .fileHandler readDataOfLength:self .segmentSize]; return data; }
6.3 动态分片 用户在上传时网络环境会有很多情况,WiFi
、4G、弱网等很多情况。如果上传分片太大可能会导致失败率上升,分片文件太小会导致网络请求太多,产生太多无用的boundary
、header
、数据链路等资源的浪费。
为了解决这个问题,我们采取的是动态分片大小的策略。根据特定的计算策略,预先使用第一个分片的上传速度当做测速分片,测速分片的大小是固定的。根据测速的结果,对其他分片大小进行动态分片,这样可以保证分片大小可以最大限度的利用当前网速。
当然,如果觉得这种分片方式太过复杂,也可以采取一种阉割版的动态分片策略。即根据网络情况做判断,如果是WiFi
就固定某个分片大小,如果是流量就固定某个分片大小。然而这种策略并不稳定,因为现在很多手机的网速比WiFi
还快,我们也不能保证WiFi
都是百兆光纤。
1 2 3 4 5 if ([Reachability reachableViaWiFi]) { self .segmentSize = 500 * 1024 ; } else if ([Reachability reachableViaWWAN]) { self .segmentSize = 300 * 1024 ; }
6.4 并行上传 上传的所有任务如果使用的都是同一个NSURLSession
的话,是可以保持连接的,省去建立和断开连接的消耗。在iOS
平台上,NSURLSession
支持对一个Host
保持4个连接,所以,如果我们采取并行上传,可以更好的利用当前的网络。
并行上传的数量在iOS
平台上不要超过4个,最大连接数是可以通过NSURLSessionConfiguration
设置的,而且数量最好不要写死。同样的,应该基于当前网络环境,在上传任务开始的时候就计算好最大连接数,并设置给Configuration
。
经过我们的线上用户数据分析,在线上环境使用并行任务的方式上传,上传速度相较于串行上传提升四倍左右。计算方式是每秒文件上传的大小。
1 2 iPhone串行上传:715 kb/s iPhone并行上传:2909 kb/s
6.5 队列管理 分片上传过程中可能会因为网速等原因,导致上传失败。失败的任务应该由单独的队列进行管理,并且在合适的时机进行失败重传。
例如对一个500MB的文件进行分片,每片是300KB,就会产生1700多个分片文件,每一个分片文件就对应一个上传任务。如果在进行上传时,一口气创建1700多个uploadTask
,尽管NSURLSession
是可以承受的,也不会造成一个很大的内存峰值。但这样并不太好,实际上并不会同时有这么多请求发出。
1 2 3 4 @property (nonatomic , strong ) NSMutableArray *successSegments;@property (nonatomic , strong ) NSMutableArray *uploadSegments;
所以在创建上传任务时,可设置一个最大任务数,就是同时向NSURLSession
发起的请求不会超过这个数量。需要注意的是,这个数值是创建uploadTask
的任务数,并不是最大并发数,最大并发数由NSURLSession
来控制。
将待上传任务都放在uploadSegments
中,上传成功后从待上传任务数组中取出一条或多条,并保证同时进行的任务始终不超过最大任务数。失败的任务理论上来说也是需要等待上传的,所以把失败任务也放在uploadSegments
中,插入到队列最下面,这样就保证了待上传任务完成后,继续重试失败任务。
将成功的任务放在successSegments
中,并且始终保持和uploadSegments
没有交集。两个队列中保存的并不是uploadTask
,而是分片的索引,这也是为什么给分片命名的时候用索引做名字。当successSegments
等于分片数量时,就表示所有任务上传完成。
七、NSURLSession文件下载 7.1 普通下载
iOS 原生级别后台下载详解
和上传代码一样,创建下载任务很简单,通过NSURLSession
创建一个downloadTask
,并调用resume
即可开启一个下载任务。
1 2 3 4 5 6 7 8 9 NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]]; NSURL *url = [NSURL URLWithString:@"http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4" ];NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];[downloadTask resume];
我们可以调用suspend
将下载任务挂起,随后调用resume
方法继续下载任务,suspend
和resume
需要是成对的。
但是suspend
挂起任务是有超时的,默认为60s,如果超时系统会将TCP
连接断开,我们再调用resume
是失效的。可以通过NSURLSessionConfiguration
的timeoutIntervalForResource
来设置上传和下载的资源耗时。suspend
只针对于下载任务,其他任务挂起后将会重新开始。
下面两个方法是下载比较基础的方法,分别用来接收下载进度和下载完的临时文件地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 - (void )URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { CGFloat progress = (CGFloat )totalBytesWritten / (CGFloat )totalBytesExpectedToWrite; self .progressView.progress = progress; } - (void )URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *filePath = [[NSSearchPathForDirectoriesInDomains (NSCachesDirectory , NSUserDomainMask , YES ) lastObject] stringByAppendingPathComponent:@"123.zip" ]; NSURL *fileURL = [NSURL fileURLWithPath:filePath]; [fileManager copyItemAtURL:location toURL:fileURL error:NULL ]; }
7.2 断点续传 HTTP
协议支持断点续传操作,在开始下载请求时通过请求头设置Range
字段,标示从什么位置开始下载。
服务端收到客户端请求后,开始从512kb的位置开始传输数据,并通过Content-Range
字段告知客户端传输数据的起始位置。
1 Content-Range:bytes 512000 -/1024000
downloadTask
任务开始请求后,可以调用cancelByProducingResumeData:
方法可以取消下载,并且可以获得一个resumeData
,resumeData
中存放一些断点下载的信息。可以将resumeData
写到本地,后面通过这个文件可以进行断点续传。
1 2 3 4 5 NSString *library = NSSearchPathForDirectoriesInDomains (NSLibraryDirectory , NSUserDomainMask , YES ).firstObject;NSString *resumePath = [library stringByAppendingPathComponent:[self .downloadURL md5String]];[self .downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) { [resumeData writeToFile:resumePath atomically:YES ]; }];
在创建下载任务前,可以判断当前任务有没有之前待恢复的任务,如果有的话调用downloadTaskWithResumeData:
方法并传入一个resumeData
,可以恢复之前的下载,并重新创建一个downloadTask
任务。
1 2 3 4 5 NSString *library = NSSearchPathForDirectoriesInDomains (NSLibraryDirectory , NSUserDomainMask , YES ).firstObject;NSString *resumePath = [library stringByAppendingPathComponent:[self .downloadURL md5String]];NSData *resumeData = [[NSData alloc] initWithContentsOfFile:resumePath];self .downloadTask = [self .session downloadTaskWithResumeData:resumeData];[self .downloadTask resume];
通过suspend
和resume
这种方式挂起的任务,downloadTask
是同一个对象,而通过cancel
然后resumeData
恢复的任务,会创建一个新的downloadTask
任务。
当调用downloadTaskWithResumeData:
方法恢复下载后,会回调下面的方法。
1 2 3 4 - (void )URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes;
7.3 后台下载 7.3.1 可后台下载的downloadTask NSURLSession
是在单独的进程中运行,所以通过此类发起的网络请求,是独立于应用程序运行的,即使App挂起也不会停止请求。
通过backgroundSessionConfigurationWithIdentifier
方法创建后台上传或后台下载类型的NSURLSessionConfiguration
,并且设置一个唯一标识,需要保证这个标识在不同的session
之间的唯一性。后台任务只支持http
和https
的任务,其他协议的任务并不支持。
1 2 3 4 5 6 7 8 NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"identifier" ];config.isDiscretionary = YES ; config.sessionSendsLaunchEvents = YES ; NSURLSession * session =[NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
通过这种方式创建的URLSession
,其实是__NSURLBackgroundSession
:
必须使用background(withIdentifier:)
方法创建URLSessionConfiguration
,其中这个identifier
必须是固定的,而且为了避免跟其他 App 冲突,建议这个identifier
跟 App 的Bundle ID
相关
创建URLSession
的时候,必须传入delegate
必须在 App 启动的时候创建Background Sessions
,即它的生命周期跟 App 几乎一致,为方便使用,最好是作为AppDelegate
的属性,或者是全局变量
当应用进入到后台时,可以继续下载,如果客户端没有开启Background Mode
,则不会回调客户端进度。下次进入前台时,会继续回调新的进度。
7.3.2 APP生命周期改变对Task的影响 支持后台下载的 downloadTask 在不同情况下的表现:
操作
创建
运行中
暂停(suspend)
取消(cancel(byProducingResumeData:))
取消(cancel)
立即产生的效果
在 App 沙盒的 caches 文件夹里面创建 tmp 文件
把下载的数据写入 caches 文件夹里面的 tmp 文件
caches 文件夹里面的 tmp 文件不会被移动
caches 文件夹里面的 tmp 文件会被移动到 Tmp 文件夹; 会调用 didCompleteWithError
caches 文件夹里面的tmp 文件会被删除; 会调用 didCompleteWithError
进入后台
自动开启下载
继续下载
没有发生任何事情
没有发生任何事情
没有发生任何事情
手动kill App
caches 文件夹里面的 tmp 文件会被删除; 重新打开 App 后创建相同 identifier 的 session,会调用 didCompleteWithError(等于调用了 cancel)
下载停止了;然后操作同右列 。
caches 文件夹里面的 tmp 文件不会被移动; 重新打开 App 后创建相同 identifier 的 session,tmp 文件会被移动到 Tmp 文件夹; 会调用 didCompleteWithError(等于调用了 cancel(byProducingResumeData:))
没有发生任何事情
没有发生任何事情
crash或者被系统关闭
自动开启下载; caches 文件夹里面的 tmp 文件不会被移动; 重新打开 App 后,不管是否有创建相同 identifier 的 session,都会继续下载(保持下载状态)
继续下载; caches 文件夹里面的 tmp 文件不会被移动; 重新打开 App 后,不管是否有创建相同 identifier 的 session,都会继续下载(保持下载状态)
caches 文件夹里面的 tmp 文件不会被移动; 重新打开 app 后创建相同 identifier 的 session,不会调用 didCompleteWithError; session 里面还保存着 task,此时task还是暂停状态,可以恢复下载
没有发生任何事情
没有发生任何事情
7.3.3 下载完成时 由于支持后台下载,下载任务完成时,App 有可能处于不同状态,所以还要了解对应的表现:
在前台:跟普通的 downloadTask 一样,调用相关的 session 代理方法
在后台:
当Background Sessions
里面所有的任务(注意是所有任务,不单单是下载任务)都完成后,会调用AppDelegate
的application(_:handleEventsForBackgroundURLSession:completionHandler:)
方法,激活 App;
然后跟在前台时一样,调用相关的 session 代理方法;
最后再调用urlSessionDidFinishEvents(forBackgroundURLSession:)
方法
crash 或者 App 被系统关闭:
当Background Sessions
里面所有的任务(注意是所有任务,不单单是下载任务)都完成后,会自动启动 App,调用AppDelegate
的application(_:didFinishLaunchingWithOptions:)
方法;
然后调用application(_:handleEventsForBackgroundURLSession:completionHandler:)
方法
当根据 identifier 创建了对应的Background Sessions 后,才会跟在前台时一样,调用相关的 session 代理方法,
最后再调用urlSessionDidFinishEvents(forBackgroundURLSession:)
方法
crash 或者 App 被系统关闭,打开 App 保持前台,当所有的任务都完成后才创建对应的Background Sessions
:
没有创建 session 时,只会调用AppDelegate
的application(_:handleEventsForBackgroundURLSession:completionHandler:)
方法;
当创建了对应的Background Sessions
后,才会跟在前台时一样,调用相关的 session 代理方法,最后再调用urlSessionDidFinishEvents(forBackgroundURLSession:)
方法
crash 或者 App 被系统关闭,打开 App,创建对应的Background Sessions
后所有任务才完成:跟在前台的时候一样
总结:
只要不在前台,当所有任务完成后会调用AppDelegate
的application(_:handleEventsForBackgroundURLSession:completionHandler:)
方法
只有创建了对应Background Sessions
,才会调用对应的 session 代理方法,如果不在前台,还会调用urlSessionDidFinishEvents(forBackgroundURLSession:)
具体处理方式:
首先就是Background Sessions
的创建时机,前面说过:
必须在 App 启动的时候创建URLSession
,即它的生命周期跟 App 几乎一致,为方便使用,最好是作为AppDelegate
的属性,或者是全局变量。
原因:下载任务有可能在 App 处于不同状态时完成,所以需要保证 App 启动的时候,Background Sessions
也已经创建,这样才能使它的代理方法正确的调用,并且方便接下来的操作。
根据下载任务完成时的表现,结合苹果官方文档:
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 func application (_ application : UIApplication , handleEventsForBackgroundURLSession identifier : String , completionHandler : @escaping () -> Void ) { if identifier == urlSession.configuration.identifier ?? "" { backgroundCompletionHandler = completionHandler } }
然后要在 session 的代理方法里调用completionHandler
:
1 2 3 4 5 6 7 8 9 10 11 12 func urlSessionDidFinishEvents (forBackgroundURLSession session : URLSession ) { guard let appDelegate = UIApplication .shared.delegate as? AppDelegate , let backgroundCompletionHandler = appDelegate.backgroundCompletionHandler else { return } DispatchQueue .main.async { backgroundCompletionHandler() } }
至此,下载完成的情况也处理完毕。
后台下载过程中会设计到一系列的代理方法调用,下面是时序图。
7.3.4 下载错误时 支持后台下载的 downloadTask 失败的时候,在urlSession(_:task:didCompleteWithError:)
方法里面的(error as NSError).userInfo
可能会出现一个 key 为NSURLErrorBackgroundTaskCancelledReasonKey
的键值对,由此可以获得只有后台下载任务失败时才有相关的信息,具体请看:Background Task Cancellation
1 2 3 4 5 func urlSession (_ session : URLSession , task : URLSessionTask , didCompleteWithError error : Error ?) { if let error = error { let backgroundTaskCancelledReason = (error as NSError ).userInfo[NSURLErrorBackgroundTaskCancelledReasonKey ] as? Int } }
7.3.5 错误汇总 如果重复后台已经存在的下载任务,会提示这个错误。
1 A background URLSession with identifier backgroundSession already exists
需要在页面退出或程序退出时,调用finishTasksAndInvalidate
方法将任务invalidate
。
1 2 3 4 5 6 7 8 9 10 11 12 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector (willTerminateNotification) name:UIApplicationWillTerminateNotification object:nil ]; - (void )willTerminateNotification { [self .session getAllTasksWithCompletionHandler:^(NSArray <__kindof NSURLSessionTask *> * _Nonnull tasks) { if (tasks.count) { [self .session finishTasksAndInvalidate]; } }]; }