Github : Jerry4me, Demo : JRBgSessionDemo
前言
本文主要是结合官方文档, 挖掘NSURLSession的类层次结构及其联系, 总结出关于NSURLSession的一些关键点及其用法.
关于NSURLSession为什么能取代NSURLConnection, 其优势是什么, 及其NSURLSession API的概述, 见
- iOS7 NSURLSession.
关于ATS, HTTP/2, 以及iOS9 NSURLSession新特性 : sharedCookies, streamTask和taskMetrics, 见
- iOS9 ATS HTTP/2 NSURLSession
以上两篇文章都是我看wwdc视频然后总结出来的文章, 大家感兴趣的可以先了解了解. 如果不想知道那么多, 只想知道怎么用NSURLSession, 那就直接看本文的正文.
*了解URL Loading System
目录
-
介绍NSURLSession相关类
-
身份验证和自定义TLS
-
[App Transport Security](#App Transport Security)
-
[NSURLSession 工作流程](#NSURLSession 工作流程)
-
[后台传输及其用法](#Background Transport)
-
[NSURLSession API](#NSURLSession API)
-
[其他一些注意点](#Something else Important)
NSURLSession
NSURLSession
- 支持data, ftp, http(s)协议, 同时支持代理服务器和socks网关.
- 支持http/1.1, http/2, spdy协议, 但同时需要服务器支持ALPN和NPN.
ALPN与NPN
- ALPN(Application Layer Protocol Negotiation,应用层协议协商)
- NPN(Next Protocol Negotiation,下一代协议协商)
NPN是服务端发送它支持的HTTP协议列表, 供客户端选择; 而ALPN则相反, 由客户端发送它支持的HTTP协议列表, 供服务端选择. 如果缺少NPN/ALPN其中一个, 则无法使用HTTP/2通信. 具体请见为什么我们应该尽快支持 ALPN.
NSURLSession相关类为 :
- NSURLSession
- NSURLSessionConfiguration
- NSURLSessionDelegate
- NSURLSessionTask
- NSURLSessionTaskMetrics
- NSURLSessionTaskTransactionMetrics
他们相互的关系如下 :
NSURLSession
session分为 :
- 全局共享单例session :
NSURLSession sharedSession
, 有一定的局限性 - 自定义session : 自定义配置文件, 设置代理, 大部分时间我们都是用这个
- 后台session : 也是自定义session的一种, 只是他专门用于做后台上传/下载任务
session为哪一种类型完全由其内部的Configuration而定.
NSURLSessionConfiguration
配置分为 :
- defaultSessionConfiguration : 系统默认
- ephemeralSessionConfiguration : 仅内存缓存, 不做磁盘缓存的配置
- backgroundSessionConfiguration : 这里需要指定一个identifier, identifier用来后台重连session对象. (做后台上传/下载就是这个config)
另外, 我们还可以给Configuration对象再自定义一些属性, 例如每端口的最大并发HTTP请求数目, 以及是否允许蜂窝网络, 请求缓存策略, 请求超时, cookies/证书存储策略等等
NSURLSessionDelegate
session管理的一组tasks共享一个代理, 不想实现代理方法时, 代理传nil即可.
代理协议分为 :
-
NSURLSessionDelegate
: session-level的代理方法 -
NSURLSessionTaskDelegate
: task-level面向all的代理方法 -
NSURLSessionDataDelegate
: task-level面向data和upload的代理方法 -
NSURLSessionDownloadDelegate
: task-level的面向download的代理方法 -
NSURLSessionStreamDelegate
: task-level的面向stream的代理方法
NSURLSessionTask
session task类型分为 :
-
NSURLSessionTask
: Task的抽象基类 -
NSURLSessionDataTask
: 以NSData的形式接收一个URLRequest的内容 -
NSURLSessionUploadTask
: 上传NSData或者本地磁盘中的文件, 完成后以NSData的形式接收一个URLRequest的响应 -
NSURLSessionDownloadTask
: 下载完成后返回临时文件在本地磁盘的URL路径 -
NSURLSessionStreamTask
: 用于建立一个TCP/IP连接
NSURLSessionTaskMetrics 和 NSURLSessionTaskTransactionMetrics
对发送请求/DNS查询/TLS握手/请求响应等各种环节时间上的统计. 更易于我们检测, 分析我们App的请求缓慢到底是发生在哪个环节, 并对此进行优化提升我们APP的性能.
NSURLSessionTaskMetrics对象与NSURLSessionTask对象一一对应. 每个NSURLSessionTaskMetrics对象内有3个属性 :
- taskInterval : task从开始到结束总共用的时间
- redirectCount : task重定向的次数
- transactionMetrics : 一个task从发出请求到收到数据过程中派生出的每个子请求, 它是一个装着许多NSURLSessionTaskTransactionMetrics对象的数组. 每个对象都代表下图的一个子过程
API很简单, 就一个方法 : - (void)URLSession: task: didFinishCollectingMetrics:
, 当收集完成的时候就会调用该方法.
身份验证和自定义TLS
当一个服务器请求身份验证或TLS握手期间需要提供证书的话, URLSession会调用他的代理方法
URLSession:didReceiveChallenge:completionHandler:
去处理.如果你没有实现该代理方法, URLSession就会这么做 :
- 使用身份认证信息作为请求URL的一部分(如果可用的话)
- 在用户的keychain中查找网络密码和证书(in macOS), 在app的keychain中查找(in iOS)
- 如果证书还是不可用或服务器拒绝该证书, 就会继续缺少身份认证的连接.
- 对于HTTP(S)连接, 请求失败并返回一个状态码, 可能会提供一些替代的内容, 例如一个私人网站的公共网页.
- 对于其他URL类型(如FTP等), 则连接请求失败, 直接返回错误信息
App Transport Security
从iOS9开始支持ATS, 且默认ATS只支持发送HTTPS请求, 不允许发送不安全的HTTP请求. 如果用户需要发送HTTP请求需要在info.plist
中配置一些东西.
详情在文章开头的iOS9 ATS HTTP/2 NSURLSession中说得很详细, 想了解的可以进去阅读.
NSURLSession 工作流程
那么如何使用NSURLSession像从前用NSURLConnection那样发送一个请求呢?
// 设置配置
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
/** 设置其他配置属性 **/
// 代理队列
NSOperationQueue *queue = [NSOperationQueue mainQueue];
// 创建session
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:queue];
// 利用session创建n个task
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"]];
// 开始
[task resume];
然后就可以在代理方法中处理各种事情了. 简单吧? 下面分task说明代理方法的调用情况..
身份验证或TLS握手
这是所有task都必须经历的一个过程. 当一个服务器请求身份验证或TLS握手期间需要提供证书的话, URLSession会调用他的代理方法URLSession:didReceiveChallenge:completionHandler:
去处理., 另外, 如果连接途中收到服务器返回需要身份认证的response, 也会调用该代理方法.
重定位response
这也是所有task都有可能经历的一个过程, 如果response是HTTP重定位, session会调用代理的URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:
方法. 这里需要调用completionHandler告诉session是否允许重定位, 或者重定位到另一个URL, 或者传nil表示重定位的响应body有效并返回. 如果代理没有实现该方法, 则允许重定位直到达到最大重定位次数.
DataTask
- 对于一个data task来说, session会调用代理的
URLSession:dataTask:didReceiveResponse:completionHandler:
方法, 决定是否将一个data dask转换成download task, 然后调用completion回调继续接收data或下载data.
- 如果你的app选择转换成download task, session会调用代理的
URLSession:dataTask:didBecomeDownloadTask:
方法并把新的download task对象以方法参数的形式传给你. 之后代理不会再收到data task的回调而是转为收到download task的回调
在服务器传输数据给客户端期间, 代理会周期性地收到
URLSession:dataTask:didReceiveData:
回调session会调用
URLSession:dataTask:willCacheResponse:completionHandler:
询问你的app是否允许缓存. 如果代理不实现这个方法的话, 默认使用session绑定的Configuration的缓存策略.
DownloadTask
对于一个通过
downloadTaskWithResumeData:
创建的下载任务, session会调用代理的URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:
方法.在服务器传输数据给客户端期间, 调用
URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
给用户传数据
- 当用户暂停下载时, 调用
cancelByProducingResumeData:
给用户传已下好的数据. - 如果用户想要恢复下载, 把刚刚的resumeData以参数的形式传给
downloadTaskWithResumeData:
方法创建新的task继续下载.
- 如果download task成功完成了, 调用
URLSession:downloadTask:didFinishDownloadingToURL:
把临时文件的URL路径给你. 此时你应该在该代理方法返回以前读取他的数据或者把文件持久化.
UploadTask
上传数据去服务器期间, 代理会周期性收到URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
回调并获得上传进度的报告.
StreamTask
如果任务的数据是由一个stream发出的, session就会调用代理的URLSession:task:needNewBodyStream:方法去获取一个NSInputStream对象并提供一个新请求的body data.
task completion
任何task完成的时候, 都会调用URLSession:task:didCompleteWithError:
方法, error有可能为nil(请求成功), 不为nil(请求失败)
请求失败, 但是该任务是可恢复下载的, 那么error对象的userInfo字典里有一个
NSURLSessionDownloadTaskResumeData
对应的value, 你应该把这个值传给downloadTaskWithResumeData:
方法重新恢复下载请求失败, 但是任务无法恢复下载, 那么应该重新创建一个下载任务并从头开始下载.
因为其他原因(如服务器错误等等), 创建并恢复请求.
Note
NSURLSession不会收到服务器传来的错误, 代理只会收到客户端出现的错误, 例如无法解析主机名或无法连接上主机等等. 客户端错误定义在URL Loading System Error Codes. 服务端错误通过HTTP状态法进行传输, 详情请看NSHTTPURLResponse和NSURLResponse类.
销毁session
如果你不再需要一个session了, 一定要调用它的invalidateAndCancel
或finishTasksAndInvalidate
方法. (前者是取消所有未完成的任务然后使session失效, 后者是等待正在执行的任务完成之后再使session失效). 否则的话, 有可能造成内存泄漏. 另外, session失效后会调用URLSession:didBecomeInvalidWithError:
方法, 之后session释放对代理的强引用.
Background Transport
需要注意的是, 在后台session中, 一些代理方法将失效. 下面说一些使用后台session的注意点 :
- 后台session必须提供一个代理处理突发事件
- 只支持HTTP(S)协议. 其他协议都不可用.
- 只支持上传/下载任务, data任务不支持.
- 后台任务有数量限制
- 当任务数量到达系统指定的临界值的时候, 一些后台任务就会被取消. 也就是说, 一个需要长时间上传/下载的任务很可能会被系统取消然后有可能过一会再重新开始, 所以支持断点续传很重要.
- 如果一个后台传输任务是在app在后台的时候开启的, 那么这个任务很可能会出于对性能的考虑随时被系统取消掉. . (相当于session的Configuration对象的discretionary属性为true.)
后台session限制确实很多, 所以尽可能使用前台session做事情.
Note
后台session最好用来传输一些支持断点续传大文件. 或对这个过程进行一些针对性的优化
- 最好把文件先压缩成zip/tar等压缩文件再上传/下载.
- 把大文件按数据段分别发送, 发送完之后服务端再把数据拼接起来.
- 上传的时候服务端应该返回一个标识符, 这样可以追踪传输的状态, 及时做出传输的调整
- 增加一个web代理服务器中间层, 以促进上述的优化
Usage
那么如何使用这个后台传输呢?
创建一个后台session
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.Jerry4me.backgroundSessionIdentifier"];
_backgroundSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];创建一个upload or download task
NSURL *URL = [NSURL URLWithString:@"http://www.bz55.com/uploads/allimg/140402/137-140402153504.jpg"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
self.task = [self.session downloadTaskWithRequest:request];
/**注意 : 后台任务不能使用带有completionHandler的方法创建 **/
/**注意 : 如果任务只想在app进入后台后才处理, 那么可不调用[task resume]主动执行, 待程序进入后台后会自动执行 **/
- 我们等下载到一半后进入后台, 打开App Switcher过一会可以发现, 图片下载完之后就会显示在应用程序上. 方法调用顺序为 : 下面四个方法全部都是app在后台时调用的
2017-03-24 14:17:09.458415 JRBgSessionDemo[2766:1080861] 下载中 - 58%
2017-03-24 14:17:09.567957 JRBgSessionDemo[2766:1080861] 下载中 - 59%
2017-03-24 14:17:16.916830 JRBgSessionDemo[2766:1080828] -[AppDelegate application:handleEventsForBackgroundURLSession:completionHandler:]
2017-03-24 14:17:16.951185 JRBgSessionDemo[2766:1080977] -[DownloadViewController URLSession:downloadTask:didFinishDownloadingToURL:]
2017-03-24 14:17:16.953951 JRBgSessionDemo[2766:1080977] -[DownloadViewController URLSession:task:didCompleteWithError:]
2017-03-24 14:17:16.954574 JRBgSessionDemo[2766:1080977] -[DownloadViewController URLSessionDidFinishEventsForBackgroundURLSession:]
总结后台传输
- 尽量用真机进行调试, 模拟器会跳过某一两个方法
- 只能进行upload/download task, 不能进行data task
- 不能使用带completionHandler的方法创建task, 否则程序直接挂掉
- Applecation里的completionHandler必须存储起来, 等你处理完所有事情之后再调用告诉系统可以进行Snapshot和挂起app了
- 后台下载最好支持断点续传, 因为任务有可能会被系统主动取消(例如系统性能下降了, 资源不够用的情况下)
后台传输的Demo在文章头部的地方, 也可以点这里进去
NSURLSession API
-
创建Session
-
+ sessionWithConfiguration:
: 创建一个指定配置的session -
+ sessionWithConfiguration:delegate:delegateQueue:
: 创建一个指定配置, 代理和代理方法执行队列的session -
sharedSession
: 返回session单例
-
-
配置Session
-
configuration
: 配置 -
delegate
: 代理对象 -
delegateQueue
: 代理方法的执行队列 -
sessionDescription
: app定义的对于该session的描述
-
-
添加data任务
-
- dataTaskWithURL:
: 获取指定URL内容 -
- dataTaskWithURL:completionHandler:
: 获取指定URL内容, 在completionHandler中处理数据. 该方法会绕过代理方法(除了身份认证挑战的代理方法) -
- dataTaskWithRequest:
: 获取指定URLRequest内容 -
- dataTaskWithRequest:completionHandler:
: 获取指定URLRequest内容, 在completionHandler中处理数据. 该方法会绕过代理方法(除了身份认证挑战的代理方法)
-
-
添加download任务
-
- downloadTaskWithURL:
: 下载指定URL内容 -
- downloadTaskWithURL:completionHandler:
: 下载指定URL内容, 在completionHandler中处理数据. 该方法会绕过代理方法(除了身份认证挑战的代理方法) -
- downloadTaskWithRequest:
: 下载指定URLRequest内容 -
- downloadTaskWithRequest:completionHandler:
: 下载指定URLRequest内容, 在completionHandler中处理数据. 该方法会绕过代理方法(除了身份认证挑战的代理方法) -
- downloadTaskWithResumeData:
: 创建一个之前被取消/下载失败的download task -
- downloadTaskWithResumeData:completionHandler:
: 创建一个之前被取消/下载失败的download task, 在completionHandler中处理数据. 该方法会绕过代理方法(除了身份认证挑战的代理方法)
-
-
添加upload任务
-
- uploadTaskWithRequest:fromData:
: 通过HTTP请求发送data给指定URL -
- uploadTaskWithRequest:fromData:completionHandler:
: 通过HTTP请求发送data给指定URL, 在completionHandler中处理数据. 该方法会绕过代理方法(除了身份认证挑战的代理方法) -
- uploadTaskWithRequest:fromFile:
: 通过HTTP请求发送指定文件给指定URL -
- uploadTaskWithRequest:fromFile:completionHandler:
: 通过HTTP请求发送指定文件给指定URL, 在completionHandler中处理数据. 该方法会绕过代理方法(除了身份认证挑战的代理方法) -
uploadTaskWithStreamedRequest
: 通过HTTP请求发送指定URLRequest数据流给指定URL
-
-
添加stream任务
-
- streamTaskWithHostName:port:
: 通过给定的域名和端口建立双向TCP/IP连接 -
- streamTaskWithNetService:
: 通过给定的network service建立双向TCP/IP连接
-
-
管理session
-
finishTasksAndInvalidate
: 任务全部完成后销毁session -
flushWithCompletionHandler:
: 清除硬盘上的cookies和证书, 清理暂时的缓存, 确保未来能响应一个新的TCP请求 -
getTasksWithCompletionHandler:
: 异步调用session中所有upload, download, data tasks的completion回调. -
invalidateAndCancel
: 取消所有未完成的任务并销毁session -
resetWithCompletionHandler:
: 清空cookies, 缓存和证书存储, 移除所有磁盘文件, 清理正在执行的下载任务, 确保未来能响应一个新的socket请求
-
API总结
所有创建task的方法, 只要带有completionHandler这个参数的, 均表示为请求过程中不会触发代理方法. 所有不带有completionHandler这个参数的, 均会走代理方法流程.
如果你实现了URLSession:didReceiveChallenge:completionHandler:
方法又没有在该方法调用completionHandler, 请求就会遭到阻塞
断点续传
- 下载失败/暂停/被取消, 可以通过task的
- cancelByProducingResumeData:
方法保存已下载的数据, 然后调用session的downloadTaskWithResumeData:
方法, 触发代理的URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:
方法
Something else Important
NSCopying Behavior
session, task和configuration对象都支持copy操作 :
- session/task copy : 返回session对象本身
- configuration copy : 返回一个无法修改(immutable)的对象.
线程安全
URLSession 的API全部都是线程安全的. 你可以在任何线程上创建session和tasks, task会自动调度到合适的代理队列中运行.
Warning
后台传输的代理方法
URLSessionDidFinishEventsForBackgroundURLSession:
可能会在其他线程中被调用. 在该方法中你应该回到主线程然后调用completion handler去触发AppDelegate中的application:handleEventsForBackgroundURLSession:completionHandler:
方法.
常量
-
NSURLSession-Specific NSError userInfo Dictionary Keys
: NSURLSession API 中出现的NSError的userInfo的keys -
Background Task Cancellation reasons
: 指示系统为什么取消了你的后台任务的理由 -
Transfer Size Constant
: 指示一个未知传输大小的常量
参考文档
NSURLSession - Foundation
WWDC 2013 - Session 204 - What's New with Multitasking
WWDC 2013 - Session 705 - What's New in Foundation Networking
WWDC 2015 - Session 711 - Networking with NSURLSession
WWDC 2016 - Session 711 - NSURLSession: New Features and Best Practices