NSURLSession使用, 后台下载
现在越来越多的开发习惯于使用各种第三方框架,诚然,第三方框架给我们开发带来了很多便利,但我们不能太依赖于第三方,在使用第三方的同时学习其原理才是硬道理。
所以今天我们就来讲讲AFNetworking所使用的NSURLSession相关的知识。
NSURLSession 有几个优势:
1支持HTTP2.0协议
2在处理下载任务的时候可以直接把数据下载到磁盘
3支持后台下载上传
4同一个session发送多个请求,只需要建立一次连接(复用了TCP)
5多线程异步处理,效率更高
要了解NSURLSession的使用,我们可以先来看看相关的几个类
NSURLSessionTask 是抽象类, 我们不能直接使用,只能使用其子类。
NSURLSessionTask 有三个个子类,NSURLSessionDataTask,NSURLDownloadTask 和NSURLStreamTask, 其中NSURLDownloadTask对下载做了优化,会写到缓存中,避免内存暴涨的问题。 NSURLSessionDataTask 有一个子类,NSURLUploadTask。继承关系如下:
接下来我们来熟悉一下NSURLSession的使用,
1: 创建NSURLSession(也可以直接使用系统提供的单例方法)
2:创建NSURLSessionTask
3:开始请求
NSURLRequest相信大家都比较熟悉,通过NSURLRequest和NSURLSessionDataTask我们可以直接发起GET或者POST请求
NSURLSessionc除了单例之外,有两个对象的获取方法
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id <NSURLSessionDelegate>)delegate delegateQueue:(nullable NSOperationQueue *)queue;
其中,两个方法中都使用到了NSURLSessionConfiguration, 这里主要配置一些缓存,超时的规则,大多数情况下我们只要使用默认的配置就好了。
第二个方法我们看到初始化时候使用到了代理和NSOperationQueue, 当不设置代理时,我们只能在dataTask的block中做处理,但如果我们要获取下载进度之类的信息时,必须使用代理。此外,传入的NSOperationQueue可以让我们指定任务所在的队列,限制并发的任务数等。
接下来我们要创建NSURLSessionTask(只能使用其子类)。 我们先来看看其相关属性。
@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));
在通过NSURLSession来获取NSURLSessionTask时要使用NSURLRequest或者USURL来进行初始化,下面以获取NSURLDataTask为例
/* 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;
直接传入url或默认为get方法,如果需要修改需要使用NSMutuableURLRequest
我们来看看一次完整的请求
NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"]; NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url]; req.HTTPMethod = @"GET"; //修改http 方法 //创建session,可以传入代理和处理的队列,代理方法不再一一列出 NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:nil delegateQueue:nil]; //创建任务 NSURLSessionDataTask *task = [session dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { //请求结束,进行处理 }]; [task resume]; //开始任务
当然,回调方法比较简单,很多时候并不能满足我们的需求,这时候就需要使用代理了。需要注意的时,NSURLSession会对代理对象强引用,因此需要在适当的时候进行释放。
以下是几个常用的代理方法:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { ////默认情况下不接收数据 //必须告诉系统是否接收服务器返回的数据 if (completionHandler) { completionHandler(disposition); } } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { //接收到返回数据时调用,可能调用多次 } -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { //请求完成或者失败时调用 } //NSURLDownloadDelegate - (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 { //下载接收到数据调用,可能调用多次 } - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { //断点下载 }
创建NSURLSessionUploadTask 和 NSURLSessionDownloadTask以及NSURLStreamTask的使用方式类似。
但要注意一些特殊情况,NSURLSessionDownloadTask在下载完毕只有会被从缓存中清除,需要我们在代理方法中将其复制保存到其他地方。
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location;
此外NSURLSessionDownload是支持断点重传的。
NSURLSessioNUploadSessionTask和NSURLSessionDataTask之间的区别并不大,只是可以直接传入需要上传的数据,不需要手动构建http请求。
/* 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;
NSURLStreamTask是进行流传输的,可以用来做视频流音频流,水平有限,具体实战还需摸索。
关于后台下载和上传:
NSURLSession是支持后台上传和下载的,而且用起来相当方便。以下载为例;
首先,创建NSURLSession时设置对应的cofigration,否则无法在后台执行。同时要对应的sessionID,这是为了后台下载完成时使用的。
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.test.backGroudSession"]];
这样设置之后,我们的任务下载任务能在后台执行了,但是由于苹果的后台机制,当我们按下home键的时候,所有线程包括主线程的任务都会被挂起,NSURLSession的代理方法是不会走的,
如果想要正确的后台下载可以有两种方式:
1:申请后台执行时间
2:在appDelegate中实现相关的代理方法
这两种方式可以根据实际情况来选择,对于一些很重要的任务可以申请后台执行时间。
我们先来介绍一下如何申请后台执行时间:(3分钟,iOS7之前是10分钟)
__block UIBackgroundTaskIdentifier bgTaskIdentifier; bgTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ if (bgTaskIdentifier != UIBackgroundTaskInvalid) { //结束所有后台任务 bgTaskIdentifier = UIBackgroundTaskInvalid; } }];
申请后台执行时间只有这样一段代码,只要监听一下UIApplicationDidEnterBackgroundNotification在进入后台时申请就好了
需要注意的是,在后台执行时间结束前必须结束任务,否则会被系统回收。有时候有些异步操作在结束回调中执行并不能保证在后台执行时间结束前停止,这时候可以通过
backgroundTimeRemaining 属性来判断何时结束
[UIApplication sharedApplication].backgroundTimeRemaining
接下来介绍一下实现appDelegate相关方法的方式,在进入后台时,NRULSessionDelegate相关的方法是不会走的,但是在下载完成的时候,会在
AppDelegate中回调。
首先在Appdelegate中实现回调,保存回调方法,当然如果有多个session在后台下载,可以根据identifier进行区分保存。
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler { //保存回调方法,后续需要使用 self.backGroundCompletionHandler = completionHandler; }
当应用进入前台时,此时会回调NSURLSessionDelegate相关的方法
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { NSLog(@"所有后台任务已经完成"); if (session.configuration.identifier) { // 执行实现保存的后台session回调 AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate; if (delegate.backGroundCompletionHandler) { delegate.backGroundCompletionHandler(); } } }
当然,此时也会回调下载成功失败的相关回调,和正常下载时一样的。