iOS学习笔记13-网络(二)NSURLSession

时间:2021-04-13 09:52:48

在2013年WWDC上苹果揭开了NSURLSession的面纱,将它作为NSURLConnection的继任者。现在使用最广泛的第三方网络框架:AFNetworking、SDWebImage等等都使用了NSURLSession。作为iOS开发人员,应该紧随苹果的步伐,不断的学习,无论是软件的更新、系统的更新、API的更新,而不能墨守成规。

  • 相比较NSURLConnection,NSURLSession提供了 配置会话缓存、协议、cookie和证书能力,这使得网络架构和应用程序可以独立工作、互不干扰。
  • 另外,NSURLSession另一个重要的部分是 会话任务,它负责加载数据,在客户端和服务器端进行文件的上传下载。

下面让我们正式进入NSURLSession学习。

一、NSURLSession介绍

在NSURLSession时代,网络请求基本上由3个任务完成:
  • NSURLSessionData:请求数据任务
  • NSURLSessionUploadTask:请求上传任务
  • NSURLSessionDownloadTask:请求下载任务
关系图如下:

NSURLSessionTask支持任务的暂停、取消和恢复,并且默认任务运行在其他非主线程中

二、NSURLSession使用

说了这么多,是时候来露两手了,具体NSURLSession怎么用呢?

1. 数据请求

先看一个网络数据请求实例,和上一章的NSURLConnection请求对比参考:
- (void)loadJsonData{
//1.创建url
NSString *urlStr = @"http://192.168.1.208/ViewStatus.aspx?userName=KenshinCui&password=123";
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];
//2.创建请求
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//3.创建会话(这里使用了一个全局会话)
NSURLSession *session = [NSURLSession sharedSession];
//4.通过会话创建任务
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",dataStr);
}else{
NSLog(@"error is :%@",error.localizedDescription);
}
}];
//5.每一个任务默认都是挂起的,需要调用 resume 方法启动任务
[dataTask resume];
}

不难发现NSURLSession网络请求的五步走黄金油战略

  1. 创建NSURL
  2. 创建NSURLRequest
  3. 创建会话NSURLSession
  4. 通过会话创建任务NSURLSessionTask的子类
  5. 调用resume方法,启动任务

2. 文件下载

文件下载也是一样的,只是换上下载任务NSURLSessionDownloadTask就行,对回调做不同处理,一切都要贯彻五步走战略,O(∩_∩)O哈!

常用的创建文件下载任务的方法如下:
/* 回调类型,这是我为了排版方便抽出来的,实际框架中没有 */
typedef void (^downloadCompletionBlock)(NSURL*,NSURLReponse*,NSError*);
/* 创建文件下载任务,需要请求NSURLRequest */
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
completionHandler:(downloadCompletionBlock)completion;
/* 创建文件任务,简化了一些操作,只需要URL就能进行文件下载 */
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url
completionHandler:(downloadCompletionBlock)completion;
下面是下载实例
-(void)downloadFile{
//1.创建url
NSString *fileName = @"1.jpg";
NSString *urlStr = [NSString stringWithFormat: @"http://192.168.1.208/FileDownload.aspx?file=%@",fileName];
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];
//2.创建请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//3.创建会话(这里使用了一个全局会话)
NSURLSession *session = [NSURLSession sharedSession];
//4.创建文件下载任务
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request
completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
if (!error) {
//注意location是下载后的临时保存路径,需要将它移动到需要保存的位置
NSError *saveError;
NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *savePath = [cachePath stringByAppendingPathComponent:fileName];
NSURL *saveUrl = [NSURL fileURLWithPath:savePath];
[[NSFileManager defaultManager] copyItemAtURL:location toURL:saveUrl error:&saveError];
if (!saveError) {
NSLog(@"save sucess.");
}
}
}];
//5.启动任务
[downloadTask resume];
}
  • 回调中的location是下载后的临时保存路径,需要将它移动到需要保存的位置
  • NSFileManager的对象方法
    ```objc
    //将fromURL路径下的文件拷贝到toURL路径下
  • (void)copyItemAtURL:(NSURL )fromUrl
    toURL:(NSURL 
    )toUrl
    error:(NSError **)error;
    ```

3.文件上传

使用NSURLConnection的文件上传时,我们还需要自己构建上传请求,主要是拼接上传表单,这是个十分麻烦的过程。
现在使用NSURLSessionUploadTask文件上传任务,我们就可以解放了,简单粗暴。
\(^o^)/~

下面是常用的创建上传任务的方法:
/* 回调类型,这是我为了排版方便抽出来的,实际框架中没有 */
typedef void (^UploadCompletionBlock)(NSData*,NSURLReponse*,NSError*);
/* 创建上传任务,需要提供上传文件二进制数据 */
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request
fromData:(NSData *)bodyData
completionHandler:(UploadCompletionBlock)completion;
/* 创建上传任务,需要提供上传文件所在的URL路径,不过这个方法常配合“PUT”请求使用 */
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request
fromFile:(NSURL *)fillURL
completionHandler:(UploadCompletionBlock)completion;
下面是上传实例:
- (void) NSURLSessionBinaryUploadTaskTest {
// 1.创建url,采用Apache本地服务器进行测试
NSString *urlStr = @"http://localhost/upload.php";
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];
// 2.创建请求,这里要设置POST请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";// 文件上传使用post
// 3.获取全局会话Session
NSURLSession *session = [NSURLSession sharedSession];
// 4.创建上传任务,Request的Body Data将被忽略,而由fromData提供
NSData *data = [NSData dataWithContentsOfFile:@"/Users/userName/Desktop/IMG_0359.jpg"];
NSURLSessionUploadTask *upload =
[session uploadTaskWithRequest:request
fromData:data
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error == nil) {
NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"upload success:%@",result);
} else {
NSLog(@"upload error:%@",error);
}
}]
// 5.启动任务
[upload resume];
}

是不是很简单,数据请求、文件下载、文件上传基本上都差不多,使用起来比NSURLConnection方便多了,还有什么理由不用NSURLSession呢!!

4.用dataTask上传文件【闲得蛋疼可以试一下】

除了上面的上传方式,实际上你也可以用NSURLSessionDataTask的方式上传,不过你就要自己设置上传BodyData和Header了,具体构建细节可以参考iOS学习笔记12-网络请求(一)NSURLConnection里面的构建过程,这里给个参考吧:

#pragma mark 上传文件
-(void)uploadFile{
NSString *fileName = @"pic.jpg";
//1.创建url
NSString *urlStr = @"http://192.168.1.208/FileUpload.aspx";
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];
//2.创建请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
//3.构建上传表单数据
//设置数据体
NSData *data = [self getHttpBody:fileName];
request.HTTPBody = data;
//设置请求头
NSString *lengthStr = [NSString stringWithFormat:@"%lu",(unsigned long)data.length];
[request setValue:lengthStr forHTTPHeaderField:@"Content-Length"];
NSString *typeStr = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",kBOUNDARY_STRING];
[request setValue:typeStr forHTTPHeaderField:@"Content-Type"];
//4.创建会话
NSURLSession *session = [NSURLSession sharedSession];
//5.创建dataTask任务,去做上传的功能
NSURLSessionDataTask *uploadTask = [session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",dataStr);
}else{
NSLog(@"error is :%@",error.localizedDescription);
}
}];
//6.启动任务
[uploadTask resume];
}
上面的获取数据体方法getHttpBody,我也贴过来了
#pragma mark 取得数据体
-(NSData *)getHttpBody:(NSString *)fileName{
NSMutableData *dataM = [NSMutableData data];
NSString *type = [self getMIMETypes:fileName];
//构建请求体body的顶部
NSMutableString *bodyTop = [NSMutableString string];
//宏kBOUNDARY_STRING就是boundary标示
[bodyTop appendFormat:@"--%@\n",kBOUNDARY_STRING];
[bodyTop appendFormat:@"Content-Disposition: form-data; name=\"file1\"; filename=\"%@\"\n",fileName];
[bodyTop appendFormat:@"Content-Type: %@\n\n",type];
//构建请求体body的底部
NSString *bodyBottom = [NSString stringWithFormat:@"\n--%@--",kBOUNDARY_STRING];
NSString *filePath = [[NSBundle mainBundle] pathForResource:fileName ofType:nil];
//构建请求体body中间的二进制上传数据
NSData *fileData = [NSData dataWithContentsOfFile:filePath];
//把顶部、数据、底部组合起来,形成body
[dataM appendData:[bodyTop dataUsingEncoding:NSUTF8StringEncoding]];
[dataM appendData:fileData];
[dataM appendData:[bodyBottom dataUsingEncoding:NSUTF8StringEncoding]];
return dataM;
}

三、会话Session控制

上面我们都是使用的全局NSURLSession,一般情况下我们就够用,但如果遇到两个连接使用不同的资源配置的情况,怎么办?答案就是自己定制。

  • NSURLSession支持我们自己定制NSURLSession
  • NSURLSession支持的三种会话配置:
  1. defaultSessionConfiguration
    进程内会话(默认会话),用硬盘来缓存数据。
  2. ephemeralSessionConfiguration
    临时的进程内会话(内存),不会将cookie、缓存储存到本地,只会放到内存中,当应用程序退出后数据也会消失。
  3. backgroundSessionConfiguration
    后台会话,相比默认会话,该会话会在后台开启一个线程进行网络数据处理。
下面就是定制NSURLSession的过程:
//使用默认会话配置
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 5.0f;//请求超时时间
sessionConfig.allowsCellularAccess = true;//是否允许蜂窝网络下载(2G/3G/4G)
//创建会话,指定配置和代理
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
上面设置了代理,NSURLSession有很多代理协议:
  • NSURLSessionDelegateNSObject
    会话父协议
  • NSURLSessionTaskDelegateNSURLSessionDelegate
    任务协议
  • NSURLSessionDataDelegateNSURLSessionTaskDelegate
    数据协议
  • NSURLSessionDownloadDelegate: NSURLSessionTaskDelegate
    下载协议
  • NSURLSessionStreamDelegateNSURLSessionTaskDelegate
    网络流协议
下面就拿最常用的下载协议NSURLSessionDownloadDelegate来讲下:
/* 下载中(会多次调用,可以记录下载进度) */
- (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 /* 下载任务 */
didFinishDownloadingToURL:(NSURL *)location;/* 下载完成后临时存放的URL */ /* 任务完成,不管是否下载成功 */
-(void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task /* 下载任务 */
didCompleteWithError:(NSError *)error;/* 错误 */
实际上NSURLSessionTask任务除了resume启动之外,还有一些方法
/* 取消任务 */
- (void)cancel;
/* 挂起任务(暂停任务) */
- (void)suspend;
/* 启动任务 */
- (void)resume;
下面来个代码总结:
-(void)downloadFile{
NSString *fileName = _textField.text;
NSString *urlStr = [NSString stringWithFormat: @"http://192.168.1.208/FileDownload.aspx?file=%@",fileName];
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 5.0f;//请求超时时间
sessionConfig.allowsCellularAccess = true;//是否允许蜂窝网络下载(2G/3G/4G)
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
_downloadTask = [session downloadTaskWithRequest:request];
[_downloadTask resume];
}
#pragma mark 点击取消下载
-(void)cancelDownload{
[_downloadTask cancel];
}
#pragma mark 点击挂起下载
-(void)suspendDownload{
[_downloadTask suspend];
}
#pragma mark 点击恢复下载
-(void)resumeDownload{
[_downloadTask resume];
}
#pragma mark - 下载任务代理
#pragma mark 下载中(会多次调用,可以记录下载进度)
-(void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
[self setUIStatus:totalBytesWritten expectedToWrite:totalBytesExpectedToWrite];//设置界面状态
}
#pragma mark 下载完成
-(void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
NSError *error;
NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *savePath = [cachePath stringByAppendingPathComponent:_textField.text];
NSURL *saveUrl = [NSURL fileURLWithPath:savePath];
[[NSFileManager defaultManager] copyItemAtURL:location toURL:saveUrl error:&error];
}
#pragma mark 任务完成,不管是否下载成功
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
[self setUIStatus:0 expectedToWrite:0];//设置界面状态
}

四、Session后台开启任务

NSURLSession支持程序的后台下载和上传,苹果官方将其称为进程之外的上传和下载,这些任务都是交给后台守护线程完成的,而非应用程序本身。
即使文件在下载和上传过程中崩溃了也可以继续运行(注意如果用户强制退关闭应用程序,NSURLSession会断开连接)。

我们先来看下如何创建一个后台Session
#pragma mark 取得一个后台会话(保证一个后台会话,这通常很有必要,这里采用单例模式的形式)
- (NSURLSession *)backgroundSession{
static NSURLSession *session = nil;
static dispatch_once_t token;//下面代码块只执行一次,以后都不执行
dispatch_once(&token, ^{
NSStirng *identifier = @"com.cmjstudio.URLSession";
NSURLSessionConfiguration *sessionConfig =
[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
sessionConfig.timeoutIntervalForRequest = 5.0f;//请求超时时间
sessionConfig.discretionary = YES;//系统自动选择最佳网络下载
sessionConfig.HTTPMaximumConnectionsPerHost = 5;//限制每次最多5个连接
//创建会话
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
});
return session;
}

然后我们拿到这个后台Session就可以做上面我们讲的下载和上传任务了。

我们来了解下程序进入后台后,任务是如何调度的,先上图:

当程序进入后台后,事实上任务是交给iOS系统来调度的,具体什么时候下载完成就不得而知,例如有个较大的文件经过一个小时下载完了,正常打开应用程序看到的此文件下载进度应该在100%的位置,但是由于程序已经在后台无法更新程序UI,而此时可以通过应用程序代理方法进行UI更新。

在AppDelegate.m中添加以下函数:
/*
有其中几个任务完成后,系统会调用此应用程序的该方法
此方法会包含一个competionHandler,通常我们会保存此对象
competionHandler此操作表示应用完成所有处理工作
*/
- (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)())completionHandler
{
//backgroundSessionCompletionHandler是自定义的一个属性
self.backgroundSessionCompletionHandler = completionHandler;
}
在XXSession.m文件中实现NSURLSessionDelegate代理方法:
/*
直到最后一个任务完成,系统会调用该方法。
在这个方法中通常可以进行UI更新,并调用completionHandler通知系统已经完成所有操作。
*/
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; //这中间就可以写更新UI的代码了,code if (appDelegate.backgroundSessionCompletionHandler) {
void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
appDelegate.backgroundSessionCompletionHandler = nil;
completionHandler();
}
}