iOS网络请求 — NSURLSession

NSURLSession简介

  • iOS9之后NSURLConnection彻底推出历史舞台,NSURLSession是2013年iOS7发布的用于替代NSURLConnection的。
  • 优点:
    1. NSURLSession 支持 http2.0 协议;
    2. 在处理下载任务的时候可以直接把数据下载到磁盘;
    3. 支持后台下载或上传;
    4. 同一个session 发送多个请求,只需要建立一次连接(复用了TCP;
    5. 提供了全局的 session 并且可以统一配置,使用更加方便;
    6. 下载的时候是多线程异步处理,效率更高。
  • 基本使用
    使用NSURLSession创建task,然后执行task。

NSURLSessionTask

  • NSURLSessionTask 是一个抽象类,如果要使用那么只能使用它的子类,NSURLSessionTask 有两个子类:
    1. NSURLSessionDataTask,可以用来处理一般的网络请求,如 GET | POST 请求等,有一个子类为 NSURLSessionUploadTask,用于处理上传请求的时候有优势
    2. NSURLSessionDownloadTask,主要用于处理下载请求,有很大的优势。
      NSURLSessionTask

常用的API

  • NSURLSession常用方法
//获得共享的Session
+ (NSURLSession *)sharedSession;
//自定义Session
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(id <NSURLSessionDelegate>)delegate delegateQueue:(NSOperationQueue *)queue;
//通过request创建任务,一般配合自定义Session及其代理使用,下同
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request;
//通过url创建任务,内部会自动的将请求路径作为参数创建一个请求对象(GET)
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url;
//通过文件路劲上传文件
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;
//直接上传文件数据
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;
//通过request创建下载任务,一般配合自定义Session及其代理使用,下同
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request;
//通过url创建下载任务,内部会自动的将请求路径作为参数创建一个请求对象(GET)
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url;
//分类方法
@interface NSURLSession (NSURLSessionAsynchronousConvenience)
//通过request创建任务
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
//通过url创建任务,内部会自动的将请求路径作为参数创建一个请求对象(GET)
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
//通过request创建下载任务
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
//通过url创建下载任务
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
  • NSURLSessionTask常用方法
- (void)suspend; // 暂停
- (void)resume; // 恢复
- (void)cancel; // 取消
@property (readonly, copy) NSError *error; // 错误
@property (readonly, copy) NSURLResponse *response; // 响应
  • NSURLSessionDataDelegate常用代理方法
/**
 *  1.接收到服务器的响应 它默认会取消该请求
 *  @param session           会话对象
 *  @param dataTask          请求任务
 *  @param response          响应头信息
 *  @param completionHandler 回调 传给系统
 */
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler;
/**
 *  接收到服务器返回的数据 调用多次
 *  @param session           会话对象
 *  @param dataTask          请求任务
 *  @param data              本次下载的数据
 */
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data;
/**
 *  请求结束或者是失败的时候调用
 *  @param session           会话对象
 *  @param dataTask          请求任务
 *  @param error             错误信息
 */
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;
  • NSURLSessionDownloadTask常用方法
- (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler; // 取消任务
  • NSURLSessionDownloadDelegate下载代理方法
/**
 *  写数据,获得数据的时候会一直调用
 *  @param session                   会话对象
 *  @param downloadTask              下载任务
 *  @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;
/**
 *  当恢复下载的时候调用该方法
 *  @param fileOffset         从什么地方下载
 *  @param expectedTotalBytes 文件的总大小
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes;
/**
 *  当下载完成的时候调用
 *  @param location     文件的临时存储路径
 */
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location;
/**
 *  请求结束
 */
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;

NSURLSessionConfiguration

  • 在使用自定义方式创建NSURLSession对像时,都需要传入一个NSURLSessionConfiguration参数,这个参数是对Session的网络请求的基本配置。
  • 有三个方法来创建NSURLSessionConfiguration:
    defaultSessionConfiguration 使用全局的cachecookie,使用硬盘来缓存数据。
    ephemeralSessionConfiguration 临时session配置,与默认配置相比,这个配置不会将缓存、cookie等存在本地,只会存在内存里,所以当程序退出时,所有的数据都会消失
    backgroundSessionConfiguration 后台session配置,与默认配置类似,不同的是会在后台开启另一个线程来处理网络数据。
  • 创建了NSURLSessionConfiguration后,就可以给它设置各种属性:
@interface NSURLSessionConfiguration : NSObject <NSCopying>  

/* 三种创建方式 */  

+ (NSURLSessionConfiguration *)defaultSessionConfiguration;  
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;  
+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier NS_AVAILABLE(10_10, 8_0);  

/* 当使用上述第三种方式创建后台sessionConfiguration时可以读到初始化时传入的唯一标识,其他创建方式都为空 */  
@property (nullable, readonly, copy) NSString *identifier;  

/*  
缓存策略,默认值是NSURLRequestUseProtocolCachePolicy 
 */  
@property NSURLRequestCachePolicy requestCachePolicy;  

/* 给request指定每次接收数据超时间隔,如果下一次接受新数据用时超过该值,则发送一个请求超时给该request。默认为60s */  
@property NSTimeInterval timeoutIntervalForRequest;  

/* 给指定resource设定一个超时时间,resource需要在时间到达之前完成。默认是7天。 */  
@property NSTimeInterval timeoutIntervalForResource;  

/* 指定网络传输类型。精切指出传输类型,可以让系统快速响应,提高传输质量,延长电池寿命等。 
typedef NS_ENUM(NSUInteger, NSURLRequestNetworkServiceType) 
{ 
    NSURLNetworkServiceTypeDefault = 0,    // 普通网络传输,默认使用这个 
    NSURLNetworkServiceTypeVoIP = 1,    // 网络语音通信传输,只能在VoIP使用 
    NSURLNetworkServiceTypeVideo = 2,    // 影像传输 
    NSURLNetworkServiceTypeBackground = 3, // 网络后台传输,优先级不高时可使用。对用户不需要的网络操作可使用 
    NSURLNetworkServiceTypeVoice = 4       // 语音传输 
}; 
 */  
@property NSURLRequestNetworkServiceType networkServiceType;  

/* 是否使用蜂窝网络,默认是yes. */  
@property BOOL allowsCellularAccess;  

/* 是否由系统根据性能自动裁量后台任务。默认值是NO。同sessionSendsLaunchEvent一样,只对后台configuration有效。 */  
@property (getter=isDiscretionary) BOOL discretionary NS_AVAILABLE(10_10, 7_0);  

/*  
如果要为app的插件提供session,需要给这个值赋值 
 */  
@property (nullable, copy) NSString *sharedContainerIdentifier NS_AVAILABLE(10_10, 8_0);  

/*  
 表示当后台传输结束时,是否启动app.这个属性只对 后台sessionConfiguration 生效,其他configuration类型会自动忽略该值。默认值是YES。 
 */  
@property BOOL sessionSendsLaunchEvents NS_AVAILABLE(NA, 7_0);  

/*  
指定了会话连接中的代理服务器。同样地,大多数面向消费者的应用程序都不需要代理,所以基本上不需要配置这个属性,默认为NULL 
*/  
@property (nullable, copy) NSDictionary *connectionProxyDictionary;  

/* 确定是否支持SSLProtocol版本的会话 
 */  
@property SSLProtocol TLSMinimumSupportedProtocol;  

/*  
确定是否支持SSLProtocol版本的会话 
*/  
@property SSLProtocol TLSMaximumSupportedProtocol;  

/*  
它可以被用于开启HTTP管道,这可以显着降低请求的加载时间,但是由于没有被服务器广泛支持,默认是禁用的 
 */  
@property BOOL HTTPShouldUsePipelining;  

/*  
默认为yes,是否提供来自shareCookieStorge的cookie,如果想要自己提供cookie,可以使用HTTPAdditionalHeaders来提供。 
 */  
@property BOOL HTTPShouldSetCookies;  

/* Policy for accepting cookies.  This overrides the policy otherwise specified by the cookie storage. */  
@property NSHTTPCookieAcceptPolicy HTTPCookieAcceptPolicy;  

/*  
指定了一组默认的可以设置出站请求的数据头。这对于跨会话共享信息,如内容类型,语言,用户代理,身份认证,是很有用的。 
例如: 
    @{@"Accept": @"application/json", 
     @"Accept-Language": @"en", 
     @"Authorization": authString, 
     @"User-Agent": userAgentString 
   } 
 */  
@property (nullable, copy) NSDictionary *HTTPAdditionalHeaders;  

/*  
同时连接一个host的最大数。iOS默认是4.APP是作为一个整体来看的 
 */  
@property NSInteger HTTPMaximumConnectionsPerHost;  

/*  
存储cookie,清除存储,直接set为nil即可。 
对于默认和后台的session,使用sharedHTTPCookieStorage。 
对于短暂的session,cookie仅仅储存到内存,session失效时会自动清除。 
 */  
@property (nullable, retain) NSHTTPCookieStorage *HTTPCookieStorage;  

/*  
证书存储,如果不使用,可set为nil. 
默认和后台session,默认使用的sharedCredentialStorage. 
短暂的session使用一个私有存储在内存中。session失效会自动清除。 
 */  
@property (nullable, retain) NSURLCredentialStorage *URLCredentialStorage;  

/*  
缓存NSURLRequest的response。 
默认的configuration,默认值的是sharedURLCache。 
后台的configuration,默认值是nil 
短暂的configuration,默认一个私有的cache于内存,session失效,cache自动清除。 
*/  
@property (nullable, retain) NSURLCache *URLCache;  

/* Enable extended background idle mode for any tcp sockets created.    Enabling this mode asks the system to keep the socket open 
 *  and delay reclaiming it when the process moves to the background (see https://developer.apple.com/library/ios/technotes/tn2277/_index.html)  
 */  
@property BOOL shouldUseExtendedBackgroundIdleMode NS_AVAILABLE(10_11, 9_0);  

/*  
处理NSURLRequest的NSURLProtocol的子类。 
重要:对后台Session失效。 
 */  
@property (nullable, copy) NSArray<Class> *protocolClasses;  

@end  

NSURLSessionDataTask发送GET请求和POST请求及NSURLSessionDataDelegate代理方法使用

1.NSURLSessionDataTask发送GET请求

  • GET请求非常简单,创建任务,执行任务
- (void)getRequest{
    NSURL * url = [NSURL URLWithString:@"https://blog.csdn.net/Bolted_snail/article/details/79876055"];
    NSURLRequest * request = [NSURLRequest requestWithURL:url];
    //获取会话对象
    NSURLSession * session = [NSURLSession sharedSession];
    //创建任务
    /*
     data:响应体信息
     response:响应头信息
     error:错误信息当请求失败的时候 error有值
     */
    NSURLSessionDataTask * dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        //解析数据
        NSLog(@"%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);

    }];
    //执行任务或恢复执行任务
    [dataTask resume];
}

2.NSURLSessionDataTask发送POST请求

  • POST请求需要设置请求方式为POST请求,设置请求体,其他步骤与GET请求是一样的
- (void)postRequest{
    NSURL * url = [NSURL URLWithString:@"http://120.25.226.186:32812/login"];
    NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:url];
    //设置请求方法
    request.HTTPMethod = @"POST";
    //设置请求体
    request.HTTPBody = [@"username=520it&pwd=520it&type=JSON" dataUsingEncoding:NSUTF8StringEncoding];
    //创建task
    NSURLSessionDataTask * dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    }];
    //执行任务
    [dataTask resume];
}

3.NSURLSessionDataDelegate代理方法使用

#import "ViewController.h"
@interface ViewController ()<NSURLSessionDataDelegate>
@property (nonatomic, strong) NSMutableData *fileData;
@end
@implementation ViewController
-(NSMutableData *)fileData{
    if (_fileData == nil) {
        _fileData = [NSMutableData data];
    }
    return _fileData;
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self requestWithDelegate];
}
- (void)requestWithDelegate{
     NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login?username=123&pwd=123&type=JSON"];
     NSURLRequest *request = [NSURLRequest requestWithURL:url];
    //创建会话对象并设置代理
    /*
     第一个参数:配置信息 [NSURLSessionConfiguration defaultSessionConfiguration]
     第二个参数:代理
     第三个参数:设置代理方法在哪个线程中调用
     */
    NSURLSession * session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    //创建任务
    NSURLSessionDataTask * dataTask = [session dataTaskWithRequest:request];
    //执行任务
    [dataTask resume];
    //其它方法,如取消任务,暂停任务等
    //[dataTask cancel];
    //[dataTask suspend];
}
#pragma mark NSURLSessionDataDelegate
//接收到服务器的响应 它默认会取消该请求
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler{
    NSLog(@"%s",__func__);
    /*
     NSURLSessionResponseCancel = 0,取消 默认
     NSURLSessionResponseAllow = 1, 接收
     NSURLSessionResponseBecomeDownload = 2, 变成下载任务
     NSURLSessionResponseBecomeStream        变成流
     */
    completionHandler(NSURLSessionResponseAllow);
}
//接收到服务器返回的数据,调用多次
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{
    NSLog(@"%s",__func__);
    //拼接数据
    [self.fileData appendData:data];
}
//请求结束或者是失败的时候调用
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
    NSLog(@"%s",__func__);
    NSLog(@"error: %ld",error.code);
    //解析数据
    NSLog(@"%@",[[NSString alloc]initWithData:self.fileData encoding:NSUTF8StringEncoding]);
}
@end

NSURLSessionDownloadTask实现文件下载

1.block实现文件下载

  • 优点:不需要担心内存,缺点:无法监听文件下载进度,或自动开启子线程进行下载操作。
- (void)downloadWithBlock{
    NSURL *url = [NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1526736353007&di=41951e65de2abe2167444f7207d840e4&imgtype=jpg&src=http%3A%2F%2Fimg3.imgtn.bdimg.com%2Fit%2Fu%3D2152174003%2C412466376%26fm%3D214%26gp%3D0.jpg"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    //4.创建下载Task
    /*
     第一个参数:请求对象
     第二个参数:completionHandler 回调
     location:文件的临时存储路径
     response:响应头信息
     error:错误信息
     */
    //该方法内部已经实现了边接受数据边写沙盒(tmp)的操作
    NSURLSessionDownloadTask * downloadTask = [[NSURLSession sharedSession] downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        //会开启子线程去执行下载任务
        NSLog(@"%@---%@",location,[NSThread currentThread]);
        //拼接文件全路径
        NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
        //将临时路径获取的数据文件剪切到到拼接的路径中
        [[NSFileManager defaultManager]moveItemAtURL:location toURL:[NSURL fileURLWithPath:fullPath] error:nil];
        NSLog(@"%@",fullPath);
    }];
    //5.执行Task
    [downloadTask resume];
}

2.delegate实现文件下载

- (void)downloadWithDelegate{
    NSURL *url = [NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1526736353007&di=41951e65de2abe2167444f7207d840e4&imgtype=jpg&src=http%3A%2F%2Fimg3.imgtn.bdimg.com%2Fit%2Fu%3D2152174003%2C412466376%26fm%3D214%26gp%3D0.jpg"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    //创建session
    NSURLSession * session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    //创建下载任务
    NSURLSessionDownloadTask * downloadTask = [session downloadTaskWithRequest:request];
    //执行任务
    [downloadTask resume];
}
#pragma mark NSURLSessionDownloadDelegate
//写数据,获得数据的时候会一直调用
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
    //1. 获得文件的下载进度
    NSLog(@"进度:%f",1.0 * totalBytesWritten/totalBytesExpectedToWrite);
}
//当恢复下载的时候调用该方法
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes{
    NSLog(@"%s",__func__);
}
//当下载完成的时候调用
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
    NSLog(@"%@",location);
    //1 拼接文件全路径
    NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:downloadTask.response.suggestedFilename];

    //2 剪切文件
    [[NSFileManager defaultManager]moveItemAtURL:location toURL:[NSURL fileURLWithPath:fullPath] error:nil];
    NSLog(@"%@",fullPath);
}
//请求结束
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
    NSLog(@"%s",__func__);
}

3.断点续传下载(非离线)

  • 先看一下效果图:
    断点下载
  • 分析:
    NSURLSession拥有终止下载的方法:- (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler;。其中的参数resumeData已经下载的数据,以及下载文件的位置信息。而且NSURLSession还有一个方法- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;可以利用上次停止下载的resumeData,开启一个新的任务继续下载。
    因为涉及保存上次下载的resumeData,所以我们要将resumeData保存为全局变量,以便使用。另外还有一些其他类需要保存为全局变量。但是使用这样的方法进行断点下载,如果程序被杀死,再重新启动的话,是无法继续下载的。只能重新开始下载。也就是说不支持离线下载。
  • NSURLSession断点下载(不支持离线)实现断点下载的步骤如下:
    1. 设置一个downloadTask、session以及resumeData的全局变量,
    2. 如果开始下载,就创建一个新的downloadTask,并启动下载,
    3. 如果暂停下载,调用取消下载的函数,并在block中保存本次的resumeData到全局resumeData中,
    4. 如果恢复下载,将上次保存的resumeData加入到任务中,并启动下载。
  • 具体示例:
    Main.storyboard子控件布局:
    storyboard布局
#import "ViewController.h"
@interface ViewController ()<NSURLSessionDownloadDelegate>
@property(nonatomic ,strong) NSURLSession * session;
@property(nonatomic ,strong) NSURLSessionDownloadTask * downloadTask;
@property (nonatomic, strong) NSData * resumData;//保存下载数据
@property (weak, nonatomic) IBOutlet UIProgressView *progressV;
@property (weak, nonatomic) IBOutlet UILabel *progressL;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
}
//开始下载
- (IBAction)startBtnClick:(id)sender {
    self.progressV.progress = 0;
    self.progressL.text = @"当前下载进度:0%";
    self.resumData = nil;
    NSURL * url = [NSURL URLWithString:@"https://dldir1.qq.com/weixin/mac/Wechat_2.3.13.dmg"];//微信mac的下载地址
    NSURLRequest * request = [NSURLRequest requestWithURL:url];
    //创建会话
    self.downloadTask = [self.session downloadTaskWithRequest:request];
    [self.downloadTask resume];
}
//暂停下载
- (IBAction)pauseBtnClick:(id)sender {
    //如果是下载状态就暂停
    if (self.downloadTask.state == NSURLSessionTaskStateRunning) {
        //是可以恢复,恢复下载的数据!=文件数据
   __weak typeof(self) weakSelf = self;
    [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
        weakSelf.resumData = resumeData;
            }];
    }
}
//继续下载
- (IBAction)resumeBtnClick:(id)sender {
    if (self.resumData) {
        self.downloadTask = [self.session downloadTaskWithResumeData:self.resumData];
    }
    [self.downloadTask resume];
}
//取消下载
- (IBAction)cancelBtnClick:(id)sender {
    [self.downloadTask cancel];//取消后不能恢复
    self.resumData = nil;
}
#pragma mark NSURLSessionDownloadDelegate
// 每次写入数据到临时文件时,就会调用一次这个方法。可在这里获得下载进度
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
    //1. 获得文件的下载进度
    NSLog(@"%0.2f",1.0 * totalBytesWritten/totalBytesExpectedToWrite);
    self.progressV.progress =  1.0 * totalBytesWritten/totalBytesExpectedToWrite;
    self.progressL.text = [NSString stringWithFormat:@"当前下载进度:%0.2f%%",self.progressV.progress * 100];

}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes{
    self.progressV.progress =  1.0 * fileOffset/expectedTotalBytes;
    self.progressL.text = [NSString stringWithFormat:@"当前下载进度:%0.2f%%",self.progressV.progress * 100];
}
//下载完成后调用,
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
    //设置下载文件保存到本地的路劲
    //获取本地目录
    NSString * documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)firstObject];
    //通过文件后缀名拼接文件路劲
    NSString * path = [documentsPath stringByAppendingPathComponent:downloadTask.response.suggestedFilename];
    NSLog(@"%@",path);
    //将临时文件(通过URL:location获取下载的临时文件)剪切到目标路劲下
    [[NSFileManager defaultManager]moveItemAtURL:location toURL:[NSURL URLWithString:path] error:nil];
}
@end

4.离线下载

  • NSURLSessionDownloadTask会自动将文件下载到了tmp临时文件中。我们只能在文件下载完毕的时候,将临时下载文件转存到永久文件路径保存起来。这样的话,如果程序被杀死,再次启动的时候,之前下载的临时文件已经消失了。我们很难拿到已经下载的文件,然而我们可以用NSURLSessionDataTask来实现NSURLSession的离线断点下载。
  • NSURLSessionDataTask在发送请求之后,能够将返回的数据,作为data一部分一部分的接受过来。这样,我们就可以像NSURLConnection上边那样,创建一个NSFilehandle(文件句柄)类,在接受数据的时候,一点点写入永久沙盒文件中。并且在下次开始的时候,设置好HTTP请求头的Rang。我们就可以实现离线断点下载了。
  • 具体代码
#import "ViewController.h"
#define WechatName @"Wechat666.dmg"
#define INFO @"progress.plist"
@interface ViewController ()<NSURLSessionDataDelegate>
@property (weak, nonatomic) IBOutlet UILabel *progressL;
@property (weak, nonatomic) IBOutlet UIProgressView *progressV;
@property (weak, nonatomic) IBOutlet UIButton *btn;
@property(nonatomic ,strong) NSURLSession * session;//会话对象
@property(nonatomic ,strong) NSURLSessionDataTask * dataTask;//下载任务对象
@property(nonatomic ,assign) NSInteger totolSize;//文件总大小
@property(nonatomic ,assign) NSInteger currentSize;//当前文件大小
@property(nonatomic ,copy) NSString * path;//文件路径
@property(nonatomic ,strong) NSFileHandle * handle;//文件句柄
@end
@implementation ViewController
-(void)viewDidLoad{
    [super viewDidLoad];
    //获取UI信息
    NSDictionary * infoDic = [NSDictionary dictionaryWithContentsOfFile:[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:INFO]];
     NSLog(@"infoDic:%@",infoDic);
    if (infoDic) {//获取到了UI信息就设置UI
        self.progressV.progress = [[infoDic valueForKey:@"progress"] floatValue];
        self.progressL.text = [infoDic valueForKey:@"title"];
        self.btn.selected = [[infoDic valueForKey:@"btnSelect"] boolValue];
    }
}
-(NSURLSession *)session{
    if (!_session) {
        _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue currentQueue]];
    }
    return _session;
}
-(NSURLSessionDataTask *)dataTask{
    if (!_dataTask) {
        NSURL * url = [NSURL URLWithString:@"https://dldir1.qq.com/weixin/mac/Wechat_2.3.13.dmg"];
        NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:url];
        //设置请求头信息,告诉服务器请求那一部分数据
        //根据文件路径获取已下载文件大小
        self.currentSize = [self fileLengthForPath:self.path];
        NSString * range = [NSString stringWithFormat:@"bytes=%ld-",self.currentSize];
        //设置请求头,告诉服务器从哪个地方开始下载文件数据
        [request setValue:range forHTTPHeaderField:@"Range"];
        //创建任务
        _dataTask = [self.session dataTaskWithRequest:request];
    }
    return _dataTask;
}
//获取已下载的文件大小
- (NSInteger)fileLengthForPath:(NSString *)path {
    NSInteger fileLength = 0;
    NSFileManager *fileManager = [[NSFileManager alloc] init];
    if ([fileManager fileExistsAtPath:path]) {//如果能获取到文件
        NSError *error = nil;
        NSDictionary *fileDict = [fileManager attributesOfItemAtPath:path error:&error];
        if (!error && fileDict) {
            fileLength = [fileDict fileSize];//获取问价大小
        }
    }
    return fileLength;
}
//创建文件保存或获取路径
- (NSString *)path{
    if (!_path) {
        _path =  [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:WechatName];
    }
    return _path;
}

- (IBAction)startOrSuspendBtnClick:(UIButton *)sender {
    sender.selected = !sender.selected;
    if (sender.selected) {//点击了下载
        [self.dataTask resume];
    }else{//点击了暂停
        [self.dataTask suspend];
        self.dataTask = nil;//要置nil,才会每次开始后去重新创建
    }
}
#pragma mark NSURLSessionDataDelegate
//接受到响应
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler{
    //获取总文件长度
    self.totolSize = response.expectedContentLength + self.currentSize;
    // 如果没有下载文件的话,就创建一个文件。如果有下载文件的话,则不用重新创建(不然会覆盖掉之前的文件)
    if (self.currentSize == 0) {
        [[NSFileManager defaultManager]createFileAtPath:self.path contents:nil attributes:nil];
    }
    //创建文件句柄
    self.handle = [NSFileHandle fileHandleForWritingAtPath:self.path];
    //移动指针,指定数据的写入位置,文件内容的最后面
    [self.handle seekToEndOfFile];
    // 允许处理服务器的响应,才会继续接收服务器返回的数据
    completionHandler(NSURLSessionResponseAllow);
}
//持续接收到数据并保存到沙盒中
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{
    //向沙盒写入数据
    [self.handle writeData:data];
    //计算当前下载进度
    self.currentSize += data.length;
    //回主线程刷新UI
    __weak typeof(self)weakSlef = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        weakSlef.progressV.progress = 1.0 * self.currentSize/self.totolSize;
        weakSlef.progressL.text = [NSString stringWithFormat:@"当前下载进度:%0.2f%%",weakSlef.progressV.progress * 100];
        //将UI信息和状态到本地沙盒中,当再次启动应用程序的时候刷新UI
        NSMutableDictionary * dict = [NSMutableDictionary dictionary];
        [dict setValue:[NSNumber numberWithFloat:weakSlef.progressV.progress] forKey:@"progress"];
        [dict setValue:weakSlef.progressL.text forKey:@"title"];
        [dict setValue:[NSNumber numberWithBool:weakSlef.btn.selected] forKey:@"btnSelect"];
        NSString * path = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:INFO];
        [dict.copy writeToFile:path atomically:YES];
    });
}
//下载完成后关闭文件,清空长度
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
    //关闭句柄
    NSLog(@"文件路径:%@",self.path);
    [self.handle closeFile];
    self.handle = nil;
    self.currentSize = 0;
    self.totolSize = 0;
}
@end
  • 效果如下:
    离线下载效果
    下载的文件

NSURLSessionUploadTask实现文件上传

  • 简介
    NSURLSessionUploadTask支持三种任务类型中的上传任务类型,它支持三种上传类型:NSData对象、文件和流
    · 如果你的数据全部在内存中,使用NSData对象好一点。
    · 如果你的数据作为文件上传,这种方式不太占内存。
    · 如果你需要边生产数据边上传,请使用数据流。
    不管使用何种方式上传数据,获取数据上传信息(进度等),需实现代理方法:
/**
 * @param (int64_t)bytesSent 每秒上传多少数据
 * @param (int64_t)totalBytesSent 已经上传了多少数据
 * (int64_t)totalBytesExpectedToSend 需要上传数据的总共的大小
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend;
//NSData对象上传 
-(NSURLSessionUploadTask )uploadTaskWithRequest:(NSURLRequest )request fromData:(NSData *)bodyData;
//文件上传 
-(NSURLSessionUploadTask )uploadTaskWithRequest:(NSURLRequest )request fromFile:(NSURL *)fileURL;
//流上传 
-(NSURLSessionUploadTask )uploadTaskWithStreamedRequest:(NSURLRequest )request;
  • 使用NSURLSessionUploadTask上传文件主要步骤:
    1. 确定上传请求的路径( NSURL );
    2. 创建可变的请求对象( NSMutableURLRequest );
    3. 修改请求方法为 POST;
    4. 设置请求头信息(告知服务器端这是一个文件上传请求);
    5. 按照固定的格式拼接要上传的文件等参数;
    6. 根据请求对象创建会话对象( NSURLSession 对象);
    7. 根据 session 对象来创建一个 uploadTask 上传请求任务;
    8. 执行该上传请求任务(调用 resume 方法);
    9. 得到服务器返回的数据,解析数据(上传成功 | 上传失败)。
  • 注意事项:
    1. 创建可变的请求对象,因为需要修改请求方法为 POST,设置请求头信息
    2. 设置请求头这个步骤可能会被遗漏
    3. 要处理上传参数的时候,一定要按照固定的格式来进行拼接
    4. 上传的格式需要自己写。POST上传文件的格式遵循W3C制定的标准,但是OC没有做封装,自己写的时候必须按照这个格式来写( MIMEType,获取方式可参考:MIME 参考手册)。
#import "ViewController.h"
#define Kboundary @"----WebKitFormBoundaryjv0UfA04ED44AhWx1111"
#define KNewLine [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]
@interface ViewController ()<NSURLSessionDataDelegate>
@property(nonatomic ,strong) NSURLSession * session;
@end

@implementation ViewController
-(NSURLSession *)session{
    if (!_session) {
        //创建会话配置对象
        NSURLSessionConfiguration * config = [NSURLSessionConfiguration defaultSessionConfiguration];
        config.allowsCellularAccess = YES;//设置允许蜂窝网络请求数据
        config.timeoutIntervalForRequest = 50.0f;//设置请求时长
        _session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue currentQueue]];
    }
    return _session;
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self upload];
}
- (void)upload{
    NSURL * url = [NSURL URLWithString:@"http://1812/upload"];
    NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = @"POST";
    //设置请求头信息,告诉服务器要上次的文件格式和内容
    [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",Kboundary] forHTTPHeaderField:@"Content-Type"];
    //创建上传任务
    /*
     第一个参数:请求对象
     第二个参数:传递是要上传的数据(请求体)
     */
    NSURLSessionUploadTask * uploadTask = [self.session uploadTaskWithRequest:request fromData:[self getBodyData] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        //解析
        NSLog(@"data:%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
        NSLog(@"%@",error);
    }];
    //执行上传任务
    [uploadTask resume];
}
-(NSData *)getBodyData{
    NSMutableData *fileData = [NSMutableData data];
    //1 文件参数
    /*
     --分隔符
     Content-Disposition: form-data; name="file"; filename="Snip20160225_341.png"
     Content-Type: image/png(MIMEType:大类型/小类型)
     空行
     文件参数
     */
    [fileData appendData:[[NSString stringWithFormat:@"--%@",Kboundary] dataUsingEncoding:NSUTF8StringEncoding]];
    [fileData appendData:KNewLine];

    //name:file 服务器接收文件参数的key值,服务器端定义,不需要自己指定
    //filename:abc.png 文件保存到服务器上面的名称
    //Content-Type:文件内容格式
    [fileData appendData:[@"Content-Disposition: form-data; name=\"file\"; filename=\"abc.png\"" dataUsingEncoding:NSUTF8StringEncoding]];
    //拼接分割线
    [fileData appendData:KNewLine];
    [fileData appendData:[@"Content-Type: image/png" dataUsingEncoding:NSUTF8StringEncoding]];
    [fileData appendData:KNewLine];
    [fileData appendData:KNewLine];
    //文件内容
    UIImage *image = [UIImage imageNamed:@"UPLOAD.png"];
    NSData *imageData = UIImagePNGRepresentation(image);
    [fileData appendData:imageData];
    [fileData appendData:KNewLine];
    //2 非文件参数
    /*
     --分隔符
     Content-Disposition: form-data; name="username"
     空行
     123456
     */
    [fileData appendData:[[NSString stringWithFormat:@"--%@",Kboundary] dataUsingEncoding:NSUTF8StringEncoding]];
    [fileData appendData:KNewLine];
    [fileData appendData:[@"Content-Disposition: form-data; name=\"username\"" dataUsingEncoding:NSUTF8StringEncoding]];
    [fileData appendData:KNewLine];
    [fileData appendData:KNewLine];
    [fileData appendData:[@"123456" dataUsingEncoding:NSUTF8StringEncoding]];
    [fileData appendData:KNewLine];

    //3 结尾标识
    /*
     --分隔符--
     */
    [fileData appendData:[[NSString stringWithFormat:@"--%@--",Kboundary] dataUsingEncoding:NSUTF8StringEncoding]];
    return fileData;
}
#pragma mark NSURLSessionDataDelegate
/*
 *  @param bytesSent                本次发送的数据
 *  @param totalBytesSent           上传完成的数据大小
 *  @param totalBytesExpectedToSend 文件的总大小
 */
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend{
    NSLog(@"上传进度:%f",1.0 *totalBytesSent / totalBytesExpectedToSend);
}
@end

相关原码:NSURLSession详解


参考博文:
NSURLSession笔记(一)文件下载,断点下载
iOS里实现multipart/form-data格式上传文件
iOS网络之NSURLSession的文件上传
NSURLSession笔记(二)上传文件
NSNull,null,nil,Nil的区别
NSAttributedString使用

猜你喜欢

转载自blog.csdn.net/bolted_snail/article/details/80229151