NSURLSession 简介
NSURLConnection这个名字,实际上指的是一组构成Foundation框架中URL加载系统的相互关联的组件:NSURLRequest,NSURLResponse,NSURLProtocol,NSURLCache,NSHTTPCookieStorage,NSURLCredentialStorage,以及和它同名的NSURLConnection。
在WWDC 2013中,Apple的团队对NSURLConnection进行了重构,并推出了NSURLSession作为替代。
NSURLSession也是一组相互依赖的类,它的大部分组件和NSURLConnection中的组件相同如NSURLRequest,NSURLCache等。而NSURLSession的不同之处在于,它将NSURLConnection替换为NSURLSession和NSURLSessionConfiguration,以及3个NSURLSessionTask的子类:NSURLSessionDataTask, NSURLSessionUploadTask, 和NSURLSessionDownloadTask。
NSURLSession 支持 task 特性,NSURLSessionConfiguration 配置对象,以及代理之外还提供了很多关于网络请求的相关特性,比如缓存控制,Cookie 控制,HTTP 验证操作等等。总之 NSURLSession 简单的接口之外,也提供了强大的体系。
获取NSURLSession类对象有几种方式:
/* * The shared session uses the currently set global NSURLCache, * NSHTTPCookieStorage and NSURLCredentialStorage objects. */ + (NSURLSession *)sharedSession; /* * Customization of NSURLSession occurs during creation of a new session. * If you only need to use the convenience routines with custom * configuration options it is not necessary to specify a delegate. * If you do specify a delegate, the delegate will be retained until after * the delegate has been sent the URLSession:didBecomeInvalidWithError: message. */ + (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration; + (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(id <NSURLSessionDelegate>)delegate delegateQueue:(NSOperationQueue *)queue;
第一种方式是使用静态的sharedSession方法,该类使用共享的会话,该会话使用全局的Cache,Cookie和证书。这个实例是全局共享的,并且功能受限。比如,由于全局实例没有代理对象,我们就不能够检测诸如下载进度这类的事件。以及我们无法设置后台下载的机制,等等。
第二种方式是通过sessionWithConfiguration:方法创建对象,也就是创建对应配置的会话,与NSURLSessionConfiguration合作使用。系统默认创建一个新的OperationQueue处理Session的消息
第三种方式是通过sessionWithConfiguration:delegate:delegateQueue方法创建对象,二三两种方式可以创建一个新会话并定制其会话类型。该方式中指定了session的委托和委托所处的队列。当不再需要连接时,可以调用Session的invalidateAndCancel直接关闭,或者调用finishTasksAndInvalidate等待当前Task结束后关闭。这时Delegate会收到URLSession:didBecomeInvalidWithError:这个事件。Delegate收到这个事件之后会被解引用。(-finishTasksAndInvalidate and -invalidateAndCancel do not have any effect on the shared session singleton.)
获取NSURLSessionConfiguration类
其中NSURLSessionConfiguration用于配置会话的属性,可以通过该类配置会话的工作模式:
+ (NSURLSessionConfiguration *)defaultSessionConfiguration; //这个配置会使用全局的缓存,cookie 等信息,这个相当于 NSURLSessionConfiguration 的默认配置行为。
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration; //这个配置不会对缓存或 cookie 以及认证信息进行存储,相当于一个私有的 Session,如果你开发一个浏览器产品,这个配置就相当于浏览器的隐私模式。
+ (NSURLSessionConfiguration *)backgroundSessionConfiguration:(NSString *)identifier; //这个配置可以让你的网络操作在你的应用切换到后台的时候还能继续工作,identifier参数指定了会话的ID,用于标记后台的session。
除了这三种预设的模式之外 NSURLSessionConfiguration 还可以进行很多的配置。NSURLSessionConfiguration中的属性:
timeoutIntervalForRequest 和 timeoutIntervalForResource 可以控制网络操作的超时时间。
allowsCellularAccess 性指定是否允许使用蜂窝连接。
HTTPAdditionalHeaders 可以指定 HTTP 请求头。
discretionary属性为YES时表示当程序在后台运作时由系统自己选择最佳的网络连接配置,该属性可以节省通过蜂窝连接的带宽。在使用后台传输数据的时候,建议使用discretionary属性,而不是allowsCellularAccess属性,因为它会把WiFi和电源可用性考虑在内。补充:这个标志允许系统为分配任务进行性能优化。这意味着只有当设备有足够电量时,设备才通过Wifi进行数据传输。如果电量低,或者只仅有一个蜂窝连接,传输任务是不会运行的。后台传输总是在discretionary模式下运行。
NSURLSessionConfiguration 几乎可以完成网络操作的大多数配置功能,并且这些配置都绑定到当前的 Session 中,我们一旦用配置好的 NSURLSessionConfiguration 初始化 NSURLSession 实例后,就不能修改这个 NSURLSession 相关的配置了。所以,一切的配置操作都放在初始化 NSURLSession 之前。
NSURLSessionTask类
NSURLSession 本身是不会进行请求的,而是通过创建 task 的形式进行网络请求(resume() 方法的调用),同一个 NSURLSession 可以创建多个 task,并且这些 task 之间的 cache 和 cookie 是共享的。那么我们就来看看 NSURLSession 都能创建哪些 task 吧。
NSURLSessionTask是一个抽象子类,它有三个子类:NSURLSessionDataTask,NSURLSessionUploadTask和NSURLSessionDownloadTask。这三个类封装了现代应用程序的三个基本网络任务:获取数据,比如JSON或XML,以及上传和下载文件。
下面是其继承关系:
NSURLSessionDataTask: 这个就是我们第一个例子中创建的 DataTask,它主要用于读取服务端的简单数据,比如 JSON 数据。
NSURLSessionDownloadTask: 这个 task 的主要用途是进行文件下载,它针对大文件的网络请求做了更多的处理,比如下载进度,断点续传等等。
NSURLSessionUploadTask: 和下载任务对应,这个 task 主要是用于对服务端发送文件类型的数据使用的。
看几个例子,比如如何下载文件:
let imageURL = NSURL(string: "https://httpbin.org/image/png")! NSURLSession.sharedSession().downloadTaskWithURL(imageURL) { location, response, error in guard let url = location else { return } guard let imageData = NSData(contentsOfURL: url) else { return } guard let image = UIImage(data: imageData) else { return } dispatch_async(dispatch_get_main_queue()) { //... } }.resume()
下载文件的时候,我们使用 downloadTaskWithURL 方法,这个方法的闭包中会接受一个 location 参数,这个参数表示我们下载好的文件的存放位置。
注意,downloadTaskWithURL 会将文件保存在一个临时目录中,location 参数指向这个临时目录的位置,如果我们要将下载好的文件进行持久保存的话,我们还需要将文件从这个临时目录中移动出来。
我们通过 location 参数可以找到文件的位置,然后将文件的内容读取出来,就像我们上面的例子中那样。
有多种方法创建对应的任务对象:
(1)NSURLSessionDataTask
通过request对象或url创建:
/* Creates a data task with the given request. The request may have a body stream. */ - (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request; /* Creates a data task to retrieve the contents of the given URL. */ - (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url;
通过request对象或url创建,同时指定任务完成后通过completionHandler指定回调的代码块:
/* * data task convenience methods. These methods create tasks that * bypass the normal delegate calls for response and data delivery, * and provide a simple cancelable asynchronous interface to receiving * data. Errors will be returned in the NSURLErrorDomain, * see <Foundation/NSURLError.h>. The delegate, if any, will still be * called for authentication challenges. */ - (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; - (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;
(2)NSURLSessionUploadTask
通过request创建,在上传时指定文件源或数据源。
/* Creates an upload task with the given request. The body of the request will be created from the file referenced by fileURL */ - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL; /* Creates an upload task with the given request. The body of the request is provided from the bodyData. */ - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData; /* Creates an upload task with the given request. The previously set body stream of the request (if any) is ignored and the URLSession:task:needNewBodyStream: delegate will be called when the body payload is required. */ - (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request;
在创建upload task对象时,通过completionHandler指定任务完成后的回调代码块:
/* * upload convenience method. */ - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;(3)NSURLSessionDownloadTask
/* Creates a download task with the given request. */ - (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request; /* Creates a download task to download the contents of the given URL. */ - (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url; /* Creates a download task with the resume data. If the download cannot be successfully resumed, URLSession:task:didCompleteWithError: will be called. */ - (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;
下载任务支持断点续传,第三种方式是通过之前已经下载的数据来创建下载任务。
同样地可以通过completionHandler指定任务完成后的回调代码块:
/* * download task convenience methods. When a download successfully * completes, the NSURL will point to a file that must be read or * copied during the invocation of the completion routine. The file * will be removed automatically. */ - (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler; - (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler; - (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler;
NSURLSessionDelegate的介绍
我们前面的例子都是通过一个闭包在网络操作完成的时候进行处理。那么有什么方法可以监听网络操作过程中发生的事件呢,比如我们下载一个大文件的时候,如果要等到下载完成可能会需要比较长的事件,这时候更好的体验是能够提供一个下载进度。类似这样的事件我们就需要用到代理。
我们在使用三种 task 的任意一种的时候都可以指定相应的代理。NSURLSession 的代理对象结构如下:
NSURLSessionDelegate - 作为所有代理的基类,定义了网络请求最基础的代理方法。
NSURLSessionTaskDelegate - 定义了网络请求任务相关的代理方法。
NSURLSessionDownloadDelegate - 用于下载任务相关的代理方法,比如下载进度等等。
NSURLSessionDataDelegate - 用于普通数据任务和上传任务。
我们可以用代理来检测下载进度:
class Downloader:NSObject, NSURLSessionDownloadDelegate {
var session: NSURLSession?
override init() {
super.init()
let imageURL = NSURL(string: "https://httpbin.org/image/png")!
session = NSURLSession(configuration: NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("taask"), delegate: self, delegateQueue: nil)
session?.downloadTaskWithURL(imageURL).resume()
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
print("下载完成")
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
print("正在下载 \(totalBytesWritten)/\(totalBytesExpectedToWrite)")
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
print("从 \(fileOffset) 处恢复下载,一共 \(expectedTotalBytes)")
}
}
我们的 Downloader 类实现了 NSURLSessionDownloadDelegate 协议,并实现了这个协议的三个方法,分别用于接收下载完成的通知,下载进度变化的通知,以及下载进度恢复的通知。
注意,Downloader 同时也继承自 NSObject,这个是必须的,否则我们在实现 NSURLSessionDownloadDelegate 协议的方法时会报错。又由于 NSURLSessionDelegate 继承自 NSObjectProtocol,所以我们需要让 Downloader 继承自 NSObject 类,
这个只有在 Swift 中需要显示的继承,在 Objective-C 中则不需要,因为 Objective-C 中的任何类都是继承自 NSObject 的。
后台传输服务
如果是一个BackgroundSession,在Task执行的时候,用户切到后台,Session会和ApplicationDelegate做交互。当程序切到后台后,在BackgroundSession中的Task还会继续下载,这部分文档叙述比较少,现在分三个场景分析下Session和Application的关系:
1、当加入了多个Task,程序没有切换到后台。
这种情况Task会按照NSURLSessionConfiguration的设置正常下载,不会和ApplicationDelegate有交互。
2、当加入了多个Task,程序切到后台,所有Task都完成下载。
在切到后台之后,Session的Delegate不会再收到,Task相关的消息,直到所有Task全都完成后,系统会调用ApplicationDelegate的application:handleEventsForBackgroundURLSession:completionHandler:回调,之后“汇报”下载工作,对于每一个后台下载的Task调用Session的Delegate中的URLSession:downloadTask:didFinishDownloadingToURL:(成功的话)和URLSession:task:didCompleteWithError:(成功或者失败都会调用)。
之后调用Session的Delegate回调URLSessionDidFinishEventsForBackgroundURLSession:。
注意:在ApplicationDelegate被唤醒后,会有个参数ComplietionHandler,这个参数是个Block,这个参数要在后面Session的Delegate中didFinish的时候调用一下,如下:
@implementation APLAppDelegate
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)())completionHandler
{
BLog();
/*
Store the completion handler. The completion handler is invoked by the view controller's checkForAllDownloadsHavingCompleted method (if all the download tasks have been completed).
*/
self.backgroundSessionCompletionHandler = completionHandler;
}
//……
@end
//Session的Delegate
@implementation APLViewController
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
APLAppDelegate *appDelegate = (APLAppDelegate *)[[UIApplication sharedApplication] delegate];
if (appDelegate.backgroundSessionCompletionHandler) {
void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
appDelegate.backgroundSessionCompletionHandler = nil;
completionHandler();
}
NSLog(@"All tasks are finished");
}
@end
3、当加入了多个Task,程序切到后台,下载完成了几个Task,然后用户又切换到前台。(程序没有退出)
切到后台之后,Session的Delegate仍然收不到消息。在下载完成几个Task之后再切换到前台,系统会先汇报已经下载完成的Task的情况,然后继续下载没有下载完成的Task,后面的过程同第一种情况。
4、当加入了多个Task,程序切到后台,几个Task已经完成,但还有Task还没有下载完的时候关掉强制退出程序,然后再进入程序的时候。(程序退出了)
最后这个情况比较有意思,由于程序已经退出了,后面没有下完Session就不在了后面的Task肯定是失败了。但是已经下载成功的那些Task,新启动的程序也没有听“汇报”的机会了。经过实验发现,这个时候之前在NSURLSessionConfiguration设置的NSString类型的ID起作用了,当ID相同的时候,一旦生成Session对象并设置Delegate,马上可以收到上一次关闭程序之前没有汇报工作的Task的结束情况(成功或者失败)。但是当ID不相同,这些情况就收不到了,因此为了不让自己的消息被别的应用程序收到,或者收到别的应用程序的消息,起见ID还是和程序的Bundle名称绑定上比较好,至少保证唯一性。
Thanks:
http://blog.csdn.net/jymn_chen/article/details/18937819 NSURLSession学习笔记(一)简介
http://www.cnblogs.com/biosli/p/iOS_Network_URL_Session.html NSURLSession使用说明及后台工作流程分析
https://lvwenhan.com/ios/457.html 自己动手写一个 iOS 网络请求库(四)——快速文件上传
http://www.swiftcafe.io/2015/12/20/nsurlsession/ NSURLSession 网络库 - 原生系统送给我们的礼物
https://realm.io/cn/news/gwendolyn-weston-ios-background-networking/?comefrom=http://blogread.cn/news/ 应用沉睡之时:后台传输服务
https://developer.apple.com/library/mac/documentation/Foundation/Reference/NSURLSession_class/index.html#//apple_ref/occ/cl/NSURLSession NSURLSession
http://www.swiftcafe.io/2015/12/23/nsurlsession-app/ 使用 NSURLSession 开发一个支持后台下载和断点续传的下载工具