NSURLConnection在iOS9被宣布弃用,NSURLSession从13年发展到现在,终于迎来了它独步江湖的时代.NSURLSession是苹果在iOS7后为HTTP数据传输提供的一系列接口,比NSURLConnection强大,坑少。
一、NSURLSession的简介
1.NSURLSession的创建
(1)使用shareSession返回session的单例,创建会话对象
NSURLSession *session = [NSURLSession sharedSession];
(2)使用NSURLSessionConfiguration来配置session,NSURLSessionConfiguration对象用于初始化NSURLSession对象,展开请求级别中与NSMutableURLRequet相关的方案NSURLSessionConfiguration对于会话如何产生请求,提供了相当多的控制和灵活性。从网络访问性能,到cookie,安全性,缓存策略,自定义协议,启动事件设置,以及用于移动设备优化的几个新属性,你会发现你一直在寻找的,正是NSURLSessionConfiguration。配置在初始化时被读取一次,之后都是不会变化的。
+ defaultSessionConfiguration返回标准配置,这实际上与NSURLConnection的网络协议栈是一样的,具有相同的共享NSHTTPCookieStorage,共享NSURLCache和共享NSURLCredentialStorage。
+ ephemeralSessionConfiguration返回一个预设配置,没有持久性存储的缓存,Cookie或证书。这对于实现像秘密浏览功能的功能来说,是很理想的。
+ backgroundSessionConfiguration:独特之处在于,它会创建一个后台会话。后台会话不同于常规的,普通的会话,它甚至可以在应用程序挂起,退出,崩溃的情况下运行上传和下载任务。初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程提供上下文。
创建和配NSURLSession的示例代码如下:
//默认类型的
NSURLSessionConfiguration * defaultConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
//即时类型的
NSURLSessionConfiguration * ephemeralConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
//后台类型的
NSURLSessionConfiguration * backgroundConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"SessionId"];
//创建并设置session
NSURLSession * defaultSession = [NSURLSession sessionWithConfiguration:defaultConfiguration];
NSURLSession * ephemeralSession = [NSURLSession sessionWithConfiguration:ephemeralConfiguration];
NSURLSession * backgroundSession = [NSURLSession sessionWithConfiguration:backgroundConfiguration];
(3)NSURLSession的优势
*它支持http2.0
*支持后台上传/下载,下载时是多线程异步处理,处理任务时直接把数据下载到磁盘上
*提供全局的session并且可以统一配置
*同一个session发送多个请求,只需建立一次连接(复用TCP)
2、NSURLSessionTask
NSURLSessionTask是一个抽象类,使用时应该使用它的子类;NSURLSessionTask有两个子类,
(1)NSURLSessionDataTask,可以用来处理一般的网络请求,如GET、POST,它的子类NSURLSessionUploadTask多用来处理上传请求。
(2)NSURLSessionDownloadTask,主要用来处理下载请求。
3、下载任务
(1)NSURLConnection文件下载
对于小文件的下载,直接使用sendAsynchronousRequest:queue:completionHandler:发送一个异步的get请求,回调的data就是下载的内容,它放在内存中;这种下载方式简单,是一次性将下载内容下载完,适用于小文件的下载。如果下载大文件,一次性下载完,内存会爆。
NSURLConnection下载大文件主要是使用它的代理方法,其中didReceiveData代理方法用来接受数据,他会被频繁调用,每次传回来一部分data,我们只需定义一个全局的NSMutableData(在didReceiveResponse接受到响应时初始化),在didReceiveData中拼接,最后在connectionDidFinishLoading中将整个NSMutableData写入到沙盒中。
注意:通常大文件下载是需要给用户展示下载进度的。
这个数值是: 已经下载的数据大小/要下载的文件总大小。已经下载的数据我们可以记录,要下载的文件总大小在服务器返回的响应头里面可以拿到,在接受到响应的方法里执行
NSHTTPURLResponse *res = (NSHTTPURLResponse*)response;
NSDictionary *headerDic = res.allHeaderFields;
NSLog(@"%@",headerDic);
self.fileLength = [[headerDic objectForKey:@"Content-Length"] intValue];
不得不说苹果太为开发者考虑了,我们不必这么麻烦的去获取文件总大小了,
response.expectedContentLength 这句代码就搞定了。
response.suggestedFilename 这句代表获取下载的文件名
但是,上述的处理方法会出现内存问题,应为用来接受文件的NSmutableData一直在内存中,所以随着文件的下载内存会一直变大,解决的方法是获取到一部分data时就写入到沙盒,然后释放内存中的data
断点下载
NSURLConnection 提供了一个cancel方法,这并不是暂停,而是取消下载任务。如果要实现断点下载必须要了解HTTP协议中请求头的Range。
不难看出,通过设置请求头的Range我们可以指定下载的位置、大小。
那么我们这样设置bytes=500- 从500字节以后的所有字节,
只需要在didReceiveData中记录已经写入沙盒中文件的大小(self.currentLength),把这个大小设置到请求头中,因为第一次下载肯定是没有执行过didReceive方法,self.currentLength也就为0,也就是从头开始下。
#pragma mark --按钮点击事件
- (IBAction)btnClicked:(UIButton *)sender {
// 状态取反
sender.selected = !sender.isSelected;
// 断点续传
// 断点下载
if (sender.selected) { // 继续(开始)下载
// 1.URL
NSURL *url = [NSURL URLWithString:@"http://localhost:8080//term_app/hdgg.zip"];
// 2.请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 设置请求头
NSString *range = [NSString stringWithFormat:@"bytes=%lld-", self.currentLength];
[request setValue:range forHTTPHeaderField:@"Range"];
// 3.下载
self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
} else { // 暂停
[self.connection cancel];
self.connection = nil;
}
在下载过程中,为了提高效率,充分利用cpu性能,通常会执行多线程下载,代码就不贴了,分析一下思路:
下载开始,创建一个和要下载的文件大小相同的文件(如果要下载的文件为100M,那么就在沙盒中创建一个100M的文件,然后计算每一段的下载量,开启多条线程下载各段的数据,分别写入对应的文件部分)。
(2)NSURLSession下载
使用NSURLSessionDownload不需要考虑边下载,边写入沙盒的问题了,因为苹果为我们做好了,调用downloadTaskWithURL:completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error)即可。
NSURL* url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/9d/25765/sogou_mac_32c_V3.2.0.1437101586.dmg"];
// 得到session对象
NSURLSession* session = [NSURLSession sharedSession];
// 创建任务
NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
}];
// 开始任务
[downloadTask resume];
回调中的location是下载好的文件写入到沙盒temp中的地址,因为temp中的文件会自动删除,所以我们要在回调中把文件移到cache中。
NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// response.suggestedFilename : 建议使用的文件名,一般跟服务器端的文件名一致
NSString *file = [caches stringByAppendingPathComponent:response.suggestedFilename];
// 将临时文件剪切或者复制Caches文件夹
NSFileManager *mgr = [NSFileManager defaultManager];
// AtPath : 剪切前的文件路径
// ToPath : 剪切后的文件路径
[mgr moveItemAtPath:location.path toPath:file error:nil];
缺点:不过通过这种方式下载有个缺点就是无法监听下载进度,要监听下载进度,苹果通常的作法是通过delegate,要遵
- (void)URLSession:(NSURLSession )session downloadTask:(NSURLSessionDownloadTask )downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite方法可以监听下载进度
#pragma mark -- NSURLSessionDownloadDelegate
/**
* 下载完毕会调用
*
* @param location 文件临时地址
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
}
/**
* 每次写入沙盒完毕调用
* 在这里面监听下载进度,totalBytesWritten/totalBytesExpectedToWrite
*
* @param bytesWritten 这次写入的大小
* @param totalBytesWritten 已经写入沙盒的大小
* @param totalBytesExpectedToWrite 文件总大小
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
self.pgLabel.text = [NSString stringWithFormat:@"下载进度:%f",(double)totalBytesWritten/totalBytesExpectedToWrite];
}
/**
* 恢复下载后调用,
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{
}
断点续传
用NSURLSessionDownloadTask做断点下载也很简单,我们先了解一下任务的取消方法
断点续传—
断点续传涉及到的类和方法
NSURLSessionDownloadTask:
- (void) suspend; 暂停 ,可以恢复
- (void) cancel; 取消,不可以恢复
- (void) cancelByProducingResumeData:^(NSData * _Nullable resumeData) : ; 取消的任务
- (void) resume; 在创建新的任务下resume,相当于重新启动任务
注意:(1)如果使用suspend方法暂停下载,因为是可恢复的,那么对应的下载任务对象是唯一的。使用的时候suspend要和resume成对使用,都是同一个NSURLSessionDownloadTask调用的对象方法。
(2)如果使用cancel,就相当于同时将NSURLSessionDwonloadTask任务也被取消了。所以如果要重新下载就需要重新创建NSURLSessionDownloadTask对象,而且,下载的内容不是重头开始
(3)如果使用cancel是无法恢复下载,但是为了能够恢复下载就只能用 cancelByProducingResumeData:^(NSData * _Nullable resumeData)方法,其中这个方法中的resumeData存储的是之前已经下载好的数据相关的信息:文件名,存储位置,已经下载好的数据的长度等信息,并不是下载的数据本身。恢复下载也是需要通过这个resumeData来恢复,然后继续下载。同时也要重新创建下载任务对象NSURLSessionDownloadTask。
- (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler;
取消操作以后会调用一个Block,并传入一个resumeData,该参数包含了继续下载文件的位置信息。也就是说,当你下载了10M得文件数据,暂停了。那么你下次继续下载的时候是从第10M这个位置开始的,而不是从文件最开始的位置开始下载。因而为了保存这些信息,所以才定义了这个NSData类型的这个属性:resumeData。这个data只包含了url跟已经下载了多少数据,不会很大,不用担心内存问题。另外,session还提供了通过resumeData来创建任务的方法
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;
我们只需要在取消操作的回调中记录好resumeData,然后在恢复下载的适合通过上面的方法创建任务就好了,相比NSURLconnection简单太多了。
关于文件下载与暂停的分析
当使用NSURLSessionDownloadTask进行下载的时候,系统会在cache文件夹下创建一个下载的路径,路径下会有一个以”CFNetworking”打头的.tmp文件(以下简称”下载文件”防止混淆),这个就是我们正在下载中的文件。而当我们调用了cancelByProducingResumeData:方法后,会得到一个data文件,通过String格式化后,发现是一个XML文件,里面包含了关于.tmp文件的一些关键点的描述,包括”Range”,”key”,”下载文件的路径”等等.而原本存在于download文件下的下载文件,则被移动到了系统tmp文件夹目录下.而当我们再次进行resume操作的时候,下载文件则又被移回到了download文件夹下。
关于程序被杀掉的断点续传resumeData
根据上面的分析,基本可以得到以下结论:
1.DownloadTask每次进行断点续传的时候,会根据data文件中的”路径Key”去寻找下载文件,然后校验后再根据”Range”属性去进行断点续传。
2.download文件夹中存放的只会是下载中的文件,一旦暂停就会被移动到tmp文件夹下。
3.每个暂停得到的data文件,与下载文件一一对应。
3.断点续传只与tmp文件夹中的文件有关。
使用NSURLSessionDataTask可以很轻松实现断点续传,可是有个致命的缺点就是无法进行后台下载,一点应用程序进入了后台,便会停止下载。所以无法满足我们的需求。而NSURLSessionDownloadTask是唯一可以实现后台下载的类,所以我们只能从这个类进行下手了。
四、进行后台下载任务
NSURLSession最大的优势在于其后台下载的灵活性,使用如下的代码进行后台数据下载:
NSURLSessionConfiguration * backgroundConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.zyprosoft.backgroundsession"];
NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
NSURLSession * backgroundSession = [NSURLSession sessionWithConfiguration:backgroundConfiguration delegate:self delegateQueue:nil];
[[backgroundSession downloadTaskWithRequest:request]resume];
在下面的回调方法中可以进行下载进度的监听:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
NSLog(@"######");
}
如果在下载过程中点击Home键使应用程序进入后台,NSURLSession的相关代理方法将不再被回调,但是下载任务依然在进行,当后台下载完成后会与AppDelegate进行交互,会调用AppDelegate中的如下方法:
-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler{
NSLog(@"1111");
}
之后应用程序在后台会调用NSURLSesstion代理的如下方法来通知下载结果:
//此方法无论成功失败都会调用
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
NSLog(@"完成:error%@",error);
}
//此方法只有下载成功才会调用 文件放在location位置
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
}
最后将调用NSURLSesstion的如下方法:
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
NSLog(@"All tasks are finished");
}
代码:
https://github.com/onebutterflyW/HFDownLoad
https://github.com/onebutterflyW/NSURLSession:中包含三个工程
NSURLSession-master是NSURLSession的简单使用,数据的上传下载
WMNSURLSessionHelper是封装的NSURLSession的帮助类,使用completionHandler一次性处理所有数据
SimpleBackgroundTransfer是apple官方后台下载的例子