NSURLSession详细解析
一 概述
NSURLConnection在iOS9被宣布弃用,NSURLSession在2013年随着iOS7的发布一起面世。
Session翻译为中文意思是会话,我们知道,在七层网络协议中有物理层->数据链路层->网络层->传输层->会话层->表示层->应用层,那我们可以将NSURLSession类理解为会话层,用于管理网络接口的创建、维护、删除等等工作,我们要做的工作也只是会话层之后的层即可,底层的工作NSURLSession已经帮我们封装好了。
Snip20200612_7.png
二 NSURLSession的使用
NSURLSession 本身是不会进行请求的,而是通过创建 task 的形式进行网络请求(resume() 方法的调用),同一个 NSURLSession 可以创建多个 task,并且这些 task 之间的 cache 和 cookie 是共享的。NSURLSession的使用有如下几步:
第一步:创建NSURLSession对象
第二步: 使用NSURLSeesion对象创建Task
第三步: 启动任务
1.创建NSURLSession对象
(1)直接创建
NSURLSession *session = [NSURLSession sharedSession];
(2)设置加代理获得
// 使用代理方法需要设置代理,但是session的delegate属性是只读的,要想设置代理只能通过这种方式创建session
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
delegate:self
delegateQueue:[[NSOperationQueue alloc] init]];
(3)配置后创建
[NSURLSession sessionWithConfiguration:defaultSessionConfiguration];
注意:
关于NSURLSession的配置有三种类型
//默认的配置会将缓存存储在磁盘上
+ (NSURLSessionConfiguration *)defaultSessionConfiguration;
//瞬时会话模式不会创建持久性存储的缓存
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;
//后台会话模式允许程序在后台进行上传下载工作
+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier
2. 使用NSURLSession对象创建Task
NSURLSessionTask的创建要根据具体需要创建相应类型的Task
Snip20200612_5.png
2.1 NSURLSessionDataTask
通过request对象或url创建:
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request;
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url;
通过request对象或url创建,同时指定任务完成后通过completionHandler指定回调的代码块:
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;
2.2 NSURLSessionUploadTask
通过request创建,在上传时指定文件源或数据源:
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;
- (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request;
通过completionHandler指定任务完成后的回调代码块:
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;
2.3 NSURLSessionDownloadTask
下载任务支持断点续传,第三种方式是通过之前已经下载的数据来创建下载任务:
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request;
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url;
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;
同样地可以通过completionHandler指定任务完成后的回调代码块:
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler;
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler;
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler;
我们在使用三种 task 的任意一种的时候都可以指定相应的代理。NSURLSession 的代理对象结构如下图:
Snip20200612_6.png
NSURLSessionDelegate – 作为所有代理的基类,定义了网络请求最基础的代理方法。
NSURLSessionTaskDelegate – 定义了网络请求任务相关的代理方法。
NSURLSessionDownloadDelegate – 用于下载任务相关的代理方法,比如下载进度等等。
NSURLSessionDataDelegate – 用于普通数据任务和上传任务。
3.启动任务
//启动任务
[task resume];
三 举例
1.GET 请求
/// 向网络请求数据
- (void)NSURLSessionTest {
// 1.创建url
// 请求一个网页
NSString *urlString = @"https://www.jianshu.com/u/835badda6ae0";
// 一些特殊字符编码
urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSURL *url = [NSURL URLWithString:urlString];
// 2.创建请求
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30];
// 3.采用苹果提供的共享session
NSURLSession *sharedSession = [NSURLSession sharedSession];
// 4.由系统直接返回一个dataTask任务
NSURLSessionDataTask *dataTask = [sharedSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// 网络请求完成之后就会执行,NSURLSession自动实现多线程
NSLog(@"%@",[NSThread currentThread]);
if (data && (error == nil)) {
// 网络访问成功
NSLog(@"data=%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
} else {
// 网络访问失败
NSLog(@"error=%@",error);
}
}];
// 5.每一个任务默认都是挂起的,需要调用 resume 方法
[dataTask resume];
}
2.POST 请求
//1、创建NSURLSession对象
NSURLSession *session = [NSURLSession sharedSession];
//2、利用NSURLSession创建任务(task)
NSURL *url = [NSURL URLWithString:@"http://www.jianshu.com/login"];
//创建请求对象里面包含请求体
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
request.HTTPBody = [@"username=121&pwd=123456" dataUsingEncoding:NSUTF8StringEncoding];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
//打印解析后的json数据
//NSLog(@"%@", [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);
}];
//3、执行任务
[task resume];
3.文件的上传
我们可以使用NSURLSessionUploadTask进行文件的上传,使用NSURLSessionUploadTask文件上传共有两种方法:
方法1:
NSURLSessionUploadTask *task =
[[NSURLSession sharedSession] uploadTaskWithRequest:request
fromFile:fileName
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
}];
方法2:
[self.session uploadTaskWithRequest:request
fromData:body
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(@"-------%@", [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);
}];
1.以数据流的方式进行上传
这种方式好处就是大小不受限制,代码如下:
- (void) NSURLSessionBinaryUploadTaskTest {
// 1.创建url
NSString *urlString = @"http://www.jianshu.com/upload.php";
// urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]];
NSURL *url = [NSURL URLWithString:urlString];
// 2.创建请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 文件上传使用post
request.HTTPMethod = @"POST";
// 3.开始上传 request的body data将被忽略,而由fromData提供
[[[NSURLSession sharedSession] uploadTaskWithRequest:request fromData:[NSData dataWithContentsOfFile:@"/Desktop/121/Desktop/121.jpg"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error == nil) {
NSLog(@"upload success:%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
} else {
NSLog(@"upload error:%@",error);
}
}] resume];
}
2.以拼接表单的方式进行上传
2.1上传的关键是请求体部分的表单拼接,获取本地上传文件的类型(MIME Types),至于具体的网络上传则很简单。 另外拼接表单的方式会有大小限制,即HTML的MAX_FILE_SIZE限制(可以自己设定,一般2MB)。
2.2 根据上面的继承关系图,我们知道uploadTask是dataTask的子类,也可以使用uploadTask来代替dataTask。
表单拼接格式如下,boundary作为分界线:
--boundary
Content-Disposition:form-data;name=”表单控件名称”;filename=”上传文件名称”
Content-Type:要上传文件MIME Types
要上传文件二进制数据;
--boundary--
示例代码如下:
- (void)NSURLSessionUploadTaskTest {
// 1.创建url 采用Apache本地服务器
NSString *urlString = @"http://localhost/upload/upload.php";
urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]];
NSURL *url = [NSURL URLWithString:urlString];
// 2.创建请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 文件上传使用post
request.HTTPMethod = @"POST";
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",@"boundary"];
[request setValue:contentType forHTTPHeaderField:@"Content-Type"];
// test.jpg
// 3.拼接表单,大小受MAX_FILE_SIZE限制(2MB) FilePath:要上传的本地文件路径 formName:表单控件名称,应于服务器一致
NSData* data = [self getHttpBodyWithFilePath:@"/Users/lifengfeng/Desktop/test.jpg" formName:@"file" reName:@"newName.png"];
request.HTTPBody = data;
// 根据需要是否提供,非必须,如果不提供,session会自动计算
[request setValue:[NSString stringWithFormat:@"%lu",data.length] forHTTPHeaderField:@"Content-Length"];
// 4.1 使用dataTask
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error == nil) {
NSLog(@"upload success:%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
} else {
NSLog(@"upload error:%@",error);
}
}] resume];
#if 0
// 4.2 开始上传 使用uploadTask fromData:可有可无,会被忽略
[[[NSURLSession sharedSession] uploadTaskWithRequest:request fromData:nil completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error == nil) {
NSLog(@"upload success:%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
} else {
NSLog(@"upload error:%@",error);
}
}] resume];
#endif
}
/// filePath:要上传的文件路径 formName:表单控件名称 reName:上传后文件名
- (NSData *)getHttpBodyWithFilePath:(NSString *)filePath formName:(NSString *)formName reName:(NSString *)reName
{
NSMutableData *data = [NSMutableData data];
NSURLResponse *response = [self getLocalFileResponse:filePath];
// 文件类型:MIMEType 文件的大小:expectedContentLength 文件名字:suggestedFilename
NSString *fileType = response.MIMEType;
// 如果没有传入上传后文件名称,采用本地文件名!
if (reName == nil) {
reName = response.suggestedFilename;
}
// 表单拼接
NSMutableString *headerStrM =[NSMutableString string];
[headerStrM appendFormat:@"--%@\r\n",@"boundary"];
// name:表单控件名称 filename:上传文件名
[headerStrM appendFormat:@"Content-Disposition: form-data; name=%@; filename=%@\r\n",formName,reName];
[headerStrM appendFormat:@"Content-Type: %@\r\n\r\n",fileType];
[data appendData:[headerStrM dataUsingEncoding:NSUTF8StringEncoding]];
// 文件内容
NSData *fileData = [NSData dataWithContentsOfFile:filePath];
[data appendData:fileData];
NSMutableString *footerStrM = [NSMutableString stringWithFormat:@"\r\n--%@--\r\n",@"boundary"];
[data appendData:[footerStrM dataUsingEncoding:NSUTF8StringEncoding]];
// NSLog(@"dataStr=%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
return data;
}
/// 获取响应,主要是文件类型和文件名
- (NSURLResponse *)getLocalFileResponse:(NSString *)urlString
{
urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]];
// 本地文件请求
NSURL *url = [NSURL fileURLWithPath:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
__block NSURLResponse *localResponse = nil;
// 使用信号量实现NSURLSession同步请求
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
localResponse = response;
dispatch_semaphore_signal(semaphore);
}] resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return localResponse;
}
4.文件的下载
-(void)startDownLoad:(UIButton *)sender {
//1.url
// NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_01.mp4"];
NSURL *url = [NSURL URLWithString:@"http://imgcache.qq.com/qzone/biz/gdt/dev/sdk/ios/release/GDT_iOS_SDK.zip"];
//2.创建请求对象
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//3.创建session
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
//4.创建Task
NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithRequest:request];
//5.执行Task
[downloadTask resume];
self.downloadTask = downloadTask;
}
#pragma mark ---------------------- 代理方法
#pragma mark 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
{
//1. 获得文件的下载进度
NSLog(@"%f",1.0 * totalBytesWritten/totalBytesExpectedToWrite);
}
/**
* 当恢复下载的时候调用该方法
*
* @param fileOffset 从什么地方下载
* @param expectedTotalBytes 文件的总大小
*/
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes
{
NSLog(@"%s",__func__);
}
/**
* 当下载完成的时候调用
*
* @param location 文件的临时存储路径
*/
-(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(@"didCompleteWithError");
}
//暂停操作
- (void)pauseDownLoad:(UIButton *)sender {
NSLog(@"+++++++++++++++++++暂停");
[self.downloadTask suspend];
}
//取消操作
- (void)cancelDownLoad:(UIButton *)sender {
NSLog(@"+++++++++++++++++++取消");
//[self.downloadTask cancel];
//恢复下载的数据!=文件数据
[self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
self.resumData = resumeData;
}];
}
//恢复下载
- (void)resumDownLoad:(UIButton *)sender {
NSLog(@"+++++++++++++++++++恢复下载");
if(self.resumData)
{
self.downloadTask = [self.session downloadTaskWithResumeData:self.resumData];
}
[self.downloadTask resume];
}