一、前言
NSURLSession是iOS7以后提出的网络请求API,这个API通过一系列的代理方法支持认证,让你的app能够实现后台下载。
NSURLSession原生地支持 data,file,ftp,http和https等URL schemes。苹果提供了五种方案来更改URL的加载。
二、NSURLSession
NSURLSession提供了通过HTTP下载内容的API,主要是通过代理方法来实现。要使用这个API必须创建多个session,每个session都协调处理一组相关的数据转移任务。在浏览器中,每一个tab或者窗口都包含一个session对象。
NSURLSession API是高度异步的,如果使用系统提供的代理方法,开发者必须指定一个completion block 用来将数据转换到app中。
NSULRSession支持取消,重启,暂停和继续任务。
2.1 session的task
task的行为只要取决于:
- session类型:
session类型主要由创建session时使用的配置对象--NSURLSessionConfiguration
对象决定
default session将用户证书存储到keychain 中,使用磁盘永久存储缓存。
+(NSURLSessionConfiguration *)defaultSessionConfiguration;
Ephemeral session不存储任何缓存和数据
+(NSURLSessionConfiguration *)ephemeralSessionConfiguration;
Background session与default session类似,只是多了些局限性
// NS_AVAILABLE(10_10, 8_0);
+(NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier
- 任务类型
NSURLSession支持三种类型的任务;
Data task使用NSData对象上传接收数据。当你的app和服务器发生短暂的经常性的交互时使用Data task比较合适。它能在所有数据都被接收后一次返回一点数据给你的app,或者通过completion handler全部返回。
Download task支持后台下载,检索数据。
Upload task以file的类型传递数据,支持后台上传
- 任务是否是在前台被创建
NSURLSession支持后台下载上传数据,在后台运行的session有以下限制:
1、必须提供session 的代理
2、支持HTTP和HTTPS协议
3、只有从文件中上传任务才会成功,如果是以data或者stream流形式会失败
note:iOS8之前,data task不支持后台session
当app不再运行而且后台传输结束或者请求证书的时候,iOS会调用application:handleEventsForBackgroundURLSession:completionHandler:
方法在后台自动重启应用。这个方法提供了session标识来重启app,app应该存储的comoletionHandler
,用这个标识来创建一个后台configuration
对象,然后用这个configuration
对象创建一个session
.这个新的session会自动与后台的活动关联起来。之后当session晚餐最后一个后台任务的适合,会调用代理的URLSessionDidFinishEventsForBackgroundURLSession:
方法,在这个代理方法中回到主线程调用之前存储的completionHandler
,以便于让系统知道你的app又可以再次安全的处于挂起状态了。
Note: You must create exactly one session per identifier (specified when you create the configuration object). The behavior of multiple sessions sharing the same identifier is undefined.
创建session的时候要精确的使用标识。
当前任务完成的时候app处于挂起状态,代理的URLSession:downloadTask:didFinishDownloadingToURL:
放会被调用。
相似地,如果task请求证书,session对象会调用代理的URLSession:task:didReceiveChallenge:completionHandler:
或者URLSession:didReceiveChallenge:completionHandler:
方法。
后台的上传下载任务会在网络错误的时候自动重连,不需要通过其他API去决定什么时候重新连接。
2.2 URL Session的生命周期
apple提供了两种使用NSURLSession API的方法:
使用自定义的delegate和系统提供的delegate,主要区别在于是否给代理赋值,通俗点就是:
使用系统代理方法:session.delegate = nil
使用自己定义的代理方法:session.delegate = aDelegate
如果你有以下需求的时候就需要用自己指定的代理来实现:
- app不在运行的时候需要在后台上传下功能
- 实现自定义的身份认证
- 实现自定义的 SSL 证书验证
- 从一个主题流中下载数据
- 禁止缓存
- 禁止HTTP重定向
2.2.1 系统代理URLSession生命周期
步骤:
1、创建一个session configuration对象,如果是background session ,必须指定明确的identify,存储这个identity,当app退出,崩溃,挂起的时候可以通过这个identity找到关联的session。
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration()
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("com.nuclear.bgsession")
2、创建一个session对象,设置configuration对象,代理为nil
let session = NSURLSession(configuration:configuration)
//或者
let session = NSURLSession(configuration:configuration, delegate:nil, delegateQueue:nil)
3、通过session对象创建一个包含源请求的task对象。
每个task对象期初都是出于挂起状态,直到你调用了task的resume
()方法,它才会开始下载特定的资源。
task对象都是NSURLSessionTask
的子类:
-
NSURLSessionDataTask
:请求资源,以一个或多个NSData对象返回服务器响应数据。支持 default,ephemeral,shared类型的session,不支持background session。 -
NSULRSessionUploadTask
:与data task类似,但是upload task使得创建请求体更加容易,它还支持background session。 -
NSURLSessionDownloadTask
:将资源直接下载到磁盘中,支持所有类型的session -
NSURLSessionStreamTask
:建立一个TCP / IP连接的主机名和端口或一个网络服务对象。
Important: If you are using the NSURLSession class without providing delegates, your app must create tasks using a call that takes a completionHandler parameter, because otherwise it cannot obtain data from the class.
如果不提供delegate,那么开发者需要使用comoletionHandler参数创建task,否则无法接收数据。
4、对于一个下载任务,如果用户暂停了下载,你应该这么做:调用cancelByProducingResumeData:
方法取消任务,当用户点击继续下载的时候将返回的剩余数据传递给downloadTaskWithResumeData:
或者downloadTaskWithResumeData:completionHandler:
方法,创建一个新的下载任务继续下载。
5、任务结束,session对象会调用 task的completion handler
note: NSURLSession does not report server errors through the error parameter. The only errors your app receives through the error parameter are client-side errors, such as being unable to resolve the hostname or connect to the host.Server-side errors are reported through the HTTP status code in the NSHTTPURLResponse object
error参数只会报告客户端的错误,比如:无法解析主机名或者无法连接主机,不会报告服务器的错误。服务器端的错误通过在NSHTTPURLResponse里面的HTTP的状态码来判定。error code详细内容参见 URL Loading System Error。
6、不再需要session的时候调用invalidateAndCancel
方法停止所有任务并使session失效;或者调用finishTasksAndInvalidate
方法让任务继续运行,在运行结束时取消session。
2.2.2 自定义delegate 的URL Session生命周期
自定义delegate,并结合各个delegate方法能够实现更多的功能,也更加灵活,当然生命周期也更复杂。
apple提供了几点最基本的session 使用步骤:
前三步同默认delegate一样:
1、创建season configuration;
2、根据创建好的configuration创建session;
3、定义task类型,用创建好的session创建task
4、当远程服务器需要authentication并且此authentication需要challenge(例如:SSL客户端证书),这个时候就需要用到 authentication challenge 代理方法:
- session-level challenge:
let NSURLAuthenticationMethodNTLM: String
let NSURLAuthenticationMethodNegotiate: String
let NSURLAuthenticationMethodClientCertificate: String
let NSURLAuthenticationMethodServerTrust: String
session会调用代理方法:URLSession:didReceiveChallenge:completionHandler:
,如果没有实现session的代理方法,那么会调task 的代理方法:URLSession:task:didReceiveChallenge:completionHandler:
来处理challenge。
- non-session-level challenge
session对象调用task delegate方法URLSession:task:didRecevieChallenge:completionHandler:
方法处理,不调用session的delegate方法。
5、接收到HTTP重定向请求,session对象调用URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:
方法。
6、使用downloadTaskWithResumeData:
或者downloadTaskWithResumeData:completionHandler:
方法来创建继续下载任务,session调用URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:
方法响应新的task对象。
7、对于data task,对应session的URLSession:dataTask:didBecomeDownloadTask:
。在决定是否将data task转变为download task之后,会调用completionHandler方法来继续接收数据或者下载数据。
如果app将data task 转变成为 download task ,session会调用 URLSession:dataTask:didBecomeDownloadTask:
,这个之后,session不会再接收data task的回调,转而接收download task的。
8、如果task是通过uploadTaskWithStreamRqeust:
方法创建的,session会通过URLSession:task:needNewBodyStream
提供一个新的数据主体。
9、在上传内容主体到服务器期间,delegate会周期性收到URLSession:downloadTask:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
,可以通过这个方法获知上传的进度
10、
下载任务中:
URLSession:dowloadTask:didiWriteData:totalBytesWriteen:totalBytesExpectedToWrite:
获取下载进度;
cancelByProducingResumeData:
暂停下载;
用户继续下载任务时,将接收到的data传递给downloadTaskWithResumeData:
或者downloadTaskWithResumeData:completionHandler:
来创建一个新的下载任务。data task中
通过URLSession:dataTask:didReceiveData:
方法获取接收进度;
URLSession:dataTask:willCacheResponse:completionHandler:
决定是否使用缓存,
11、下载任务完成之后,URLSession:downloadTask:didFinishDownloadingToURL:
会被调用,它会返回下载的数据存放的临时路径,应该及时处理数据(读取数据或移入沙盒做持久化存储等)。
12、不管什么类型的task结束,URLSession:task:didCompleteWithError:
都会被调用,根据error是否为空判断成功失败
在任务失败的情况下,大多数app应当尝试重新请求直到用户取消任务或者服务端返回error code预示这个任务不会成功。
NSError
对象的userInfo
字典包含一个key值为NSURLSessionDownloadTaskResumeData
的value,应该将这个值传递给dowloadTaskWithResumeData:
或者downloadTaskWithResumeData:completionHandler:
并创建一个新的下载任务以继续执行之前的下载。
如果该下载任务不能继续下载,那应该创建一个新的下载任务重新开始下载。
13、如果response是多重编码的,session会多次调用didReceiveResponse
方法,紧跟随的是didReceiveData
的调用
14、如果要停止一个session,可以invalidateAndCancel
和finishTasksAndInvalidate
实现。session失效之后会给URLSession:didBecomeInvalideWithError:
方法发送消息,当这个方法返回之后,session不再对delegate持有强引用。
Important: The session object keeps a strong reference to the delegate until your app explicitly invalidates the session. If you do not invalidate the session, your app leaks memory.
session会对delegate持续强引用知道失效为止,如果不调用invalid方法会造成内存泄露。
除此之外,如果你取消正在执行的下载任务,URLSession:task:didCompleteWithError:
方法会被触发
三、基本用法
上面的内容都是与session的基本生命周期有关的,接下来来看看session的基本用法
3.1 配置session
session有三种类型,其中background类型需要知道identifier以便于开发者辨认和调试。
func configSession() {
/*
创建configuration
*/
let defaultConfig = NSURLSessionConfiguration.defaultSessionConfiguration()
let ephemeralConfig = NSURLSessionConfiguration.ephemeralSessionConfiguration()
let bgConfig = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("com.nuclear.bgsession")
/*
获取缓存路径
*/
let path = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true).first
let fullPath = path! + NSBundle.mainBundle().bundleIdentifier! + "MyCacheDir"
//设置cache路径和policy
let cache = NSURLCache(memoryCapacity: 16*1024, diskCapacity: 256*1024*1024, diskPath: fullPath)
defaultConfig.URLCache = cache
defaultConfig.requestCachePolicy = .UseProtocolCachePolicy
/*
配置session
*/
let defaultSession = NSURLSession(configuration: defaultConfig, delegate: self, delegateQueue: nil)
let ephemeralSession = NSURLSession(configuration: ephemeralConfig, delegate: self, delegateQueue: nil)
let bgSession = NSURLSession(configuration: bgConfig, delegate: self, delegateQueue: nil)
session的创建方法中,如果delegateQueue为nil的时候,session会创建一个串行队列,并在该队列中执行操作。
configuration对象可以随时安全地被更改,因为session会对configuration对象执行深复制,因此你的更改只会对新创建的session有效,因此需要创建一个新的session
ephemeralConfig.allowsCellularAccess = false
let epheMeralSession = NSSession(configuration:ehpemeralConfig, delegate: self, delegateQueue:nil)
3.2 不实现代理方法的情况下发起网络请求:
func createSession() {
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: NSOperationQueue.mainQueue())
let request = NSURLRequest(URL: NSURL(string:"https://www.baidu.com")!)
session.dataTaskWithRequest(request) { (data, response, error) -> Void in
NSLog("Got response \(response) with error \(error)\n");
NSLog("DATA:\n%@\nEND DATA\n",NSString(data: data!, encoding: NSUTF8StringEncoding)!)
}.resume()
}
3.3 文件下载
实现此功能需要实现的代理方法有:
//提供获取到的数据的临时存储地址
URLSession:downloadTask:didFinishDownloadingToURL:
//获取下载进度
URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
//先前失败的下载被重启了
URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:
//下载完成失败
URLSession:didCompleteWithError:
tip:
- 暂停任务步骤:
调用cancelByproducingResumeData:
方得到还未下载的NSData对象,继续下载的时候将这个对象传递个dowloadTaskWithResumeData:
或者downloadTaskWithResumeData:completionHandler:
方法来创建一个新的download task继续下载。
2、下载失败:
代理方法URLSession:task:didCompleteWithError:
会被调用,同时会得到一个NSEerror对象error,error有一个userInfo
的字典,包含一个NSURLSessionDownloadTskResumeData
的key,这个key值对应的value为NSData对象,如果需要重新下载,应当将这value值传递给dowloadTaskWithResumeData:
或者dowloadTaskWithReusmeData:completionHandler:
方法,创建一个新的download task继续下载。
实现下载功能关键步骤的代码:
/*
init sesion and download task
*/
lazy var session:NSURLSession = {
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: NSOperationQueue.currentQueue())
return session
}()
lazy var downloadTask:NSURLSessionDownloadTask = {
let URLString = "http://yourDownloadFileURL.com"
let URL = NSURL(string: URLString)
let request = NSURLRequest(URL: URL!)
let downloadTask = self.session.downloadTaskWithRequest(request)
return downloadTask
}()
/*
download task delegate
*/
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let p = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
NSLog("progress:\(p)")
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
NSLog("resume succeed")
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
let path = savePathForDownloadData(location, task: downloadTask)
NSLog("download completed in path:\(path)")
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
NSLog("download failed ")
let resumeData = error?.userInfo[NSURLSessionDownloadTaskResumeData]
if let rd = resumeData {
self.downloadTask = self.session.downloadTaskWithResumeData(rd as! NSData)
NSLog("task restart ")
self.downloadTask.resume()
}
}
//MARK: save downloaded data then return save path
func savePathForDownloadData(location:NSURL, task:NSURLSessionDownloadTask) -> NSURL {
let manager = NSFileManager.defaultManager()
let docDict = manager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first
let originURL = task.originalRequest?.URL
let distinationURL = docDict?.URLByAppendingPathComponent((originURL?.lastPathComponent)!)
try! manager.copyItemAtURL(location, toURL: distinationURL!)
return distinationURL!
}
/*
start,pause,resuem download task
*/
@IBAction func startDownload(sender: AnyObject) {
if downloadTask.state == .Running{return}
NSLog("task start")
downloadTask.resume()
}
@IBAction func pauseDownload(sender: AnyObject) {
NSLog("task pause")
downloadTask.cancelByProducingResumeData({ (data) -> Void in
if let d = data{
self.resumeData = d
}
})
}
@IBAction func resumeDownload(sender: AnyObject) {
if downloadTask.state == .Running {return}
downloadTask = session.downloadTaskWithResumeData(resumeData)
NSLog("task resume")
downloadTask.resume()
}
3.4 上传文件
有三种上传方式,分别对应三种情况:
- NSData obj
已经有data在内存中时。
使用uploadTskWithRequest:fromData:
或者uploadTaskWithRequest:fromData:completionHandler:
创建一个新的upload task,在fromData
参数中配置HTTP的请求体数据。
-
file
上传磁盘中的文件,或者使用后台上传,或者你想通过这个方式使得占用的内存较少时。相似地,上传文件时使用
uploadTskWithRequest:fromFile:
或者uploadTskWithRequest:fromFile:completionHandler:
创建upload task。session会根据待上传数据的大小来计算
Content-Length
header;如果没有提供Content-Type
头信息,session会自动创建一个。如果需要提供其他的头信息,通过request对象来实现。
- stream
上传通过网络获取的数据(可能是边获取边上传)或者是将含有steram请求体的NSURLConnection转换为NSURLSession的时候。
创建:uploadTaskWithSreamedRequest:
不管通过哪种方式,都必须实现URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
方法获取上传进度信息,
除此之外,如果使用stream上传还必须实现URLSession:task:needNewBodyStream:
方法
3.5 后台下载
运行后台下载的任务会涉及到一个比较重要的appd elegate方法:
func application(application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: () -> Void)
the block passed to the completionHandler
parameter is an opaque callback. The background transfer service needs to know when your application is done handling events for the session. Invoking that block informs the service that the application has completely processing of this set of events and the daemon can move on.
completionHandler:
这个block是一个系统的回调,不能将一个具体的操作赋值给它,用于通知服务器app已经完成了这一系列的事件,后台的程序可以继续往下运行。
completionHandler
是系统创建的,开发者不能更改它,但是可以复制,同时提供一个自定义的回调并将completionHandler
传递给自定义回调,在适当的时候通知系统以进行下一步操作。
/*
app delegate 中实现方法
*/
func application(application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: () -> Void) {
let bgSessionVC = BGSessionViewController.bgDownload()
let _ = NSURLSession(configuration: NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(identifier), delegate: bgSessionVC, delegateQueue: NSOperationQueue.currentQueue())
bgSessionVC.addCompletionHandler(completionHandler, session: identifier)
}
/*
background session 实现方法
*/
typealias completionHandler = ()->()
var completionHandlerDic = [String:completionHandler]()
func URLSessionDidFinishEventsForBackgroundURLSession(session: NSURLSession) {
if let ID = session.configuration.identifier {
self.callHandlerForSession(ID)
}
}
func callHandlerForSession(identify:String) {
if let hander = self.completionHandlerDic[identify] {
NSLog("handler execute")
hander()
}
}
func addCompletionHandler(handler:completionHandler,session:String) {
if let _ = completionHandlerDic[session] {
NSLog("compleHandler has exited")
return
}
completionHandlerDic[session] = handler
}
```
更详细的步骤可以看看这个[小demo](https://github.com/hah1992/NSURLSessionDemo)。