首页 > 代码库 > iOS学习笔记13-网络(二)NSURLSession

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

在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 = http://www.mamicode.com/[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 = http://www.mamicode.com/[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 = http://www.mamicode.com/[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();  
    }
}

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