美文网首页
NSURLSession 概要总结

NSURLSession 概要总结

作者: 大成小栈 | 来源:发表于2021-04-20 20:37 被阅读0次

iOS9.0之后,苹果推荐使用NSURLSession来代替NSURLConnection 。 我们首先要明白NSURLSessionTask、NSURLSessionDataTask、NSURLSessionUploadTask和NSURLSessionDownloadTask之间的关系。NSURLSessionTask本身是一个抽象类,通常是根据具体的需求使用它的几个子类,关系如下:

而NSURLSession是对以上不同类复杂操作的封装,过程中还包含一些配置。NSURLSession使用简单,先根据会话对象创建一个请求Task,然后执行该Task即可。

1. NSURLSessionDataTask

一般发送一个GET/POST网络请求,直接使用NSURLSessionDataTask即可。它的常用方法如下:

// 遵守协议
<NSURLSessionDataDelegate>
// 创建NSMutableURLRequest
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
[request setValue:[NSString stringWithFormat:@"bytes=%zd-", tmpFileSize] forHTTPHeaderField:@"Range"];
// 创建NSURLSessionDataTask
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
// 开始、继续下载
[dataTask resume];
// 暂停下载
[dataTask suspend];
// 取消下载
[dataTask cancel];

NSURLSessionDataTask下载数据时,内容以NSData对象返回,需要我们不断写入文件,但不支持后台传输,切换后台会终止下载,回到前台时在协议方法中输出error。有几点需要注意,调用cancel方法会立即进入-URLSession: task: didCompleteWithError这个回调;调用suspend方法,即使任务已经暂停,但达到超时时长,也会进入这个回调,可以通过error进行判断;当一个任务调用了resume方法,但还未开始接受数据,这时调用suspend方法是无效的。也可以通过cancel方法实现暂停,只是每次需要重新创建NSURLSessionDataTask。

上传/下载文件时,我们可能需要监听网络请求的过程以获取进度等信息,那么就需要用到代理方法。NSURLSession网络请求会涉及代理方法的使用:

#import "ViewController.h"

@interface ViewController ()<NSURLSessionDataDelegate> 

@property (nonatomic, strong) NSMutableData *responseData; 

@end

@implementation ViewController 

-(NSMutableData *)responseData {

 if (_responseData == nil) {
        _responseData = [NSMutableData data];
    }
 return _responseData;
} 

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self delegateTest];
} 

-(void)delegateTest {
  
    NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login?username=520it&pwd=520it&type=JSON"]; 
    NSURLRequest *request = [NSURLRequest requestWithURL:url]; 
    
    // delegateQueue: 队列
    // [NSOperationQueue mainQueue] 代理方法在主线程中调用
    // [[NSOperationQueue alloc]init] 代理方法在子线程中调用
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]]; 
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
   [dataTask resume];
}

// 该方法中可以得到响应头(response)
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
 
    NSLog(@"didReceiveResponse--%@",[NSThread currentThread]); 

    // 注:completionHandler回调告诉系统如何处理返回的数据的)
    // 默认NSURLSessionResponseCancel(还有其他方式:NSURLSessionResponseAllow,NSURLSessionResponseBecomeDownload,NSURLSessionResponseBecomeStream)
    completionHandler(NSURLSessionResponseAllow);
} 

// 收到server返回数据,若数据较大可能会被调多次
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {

    NSLog(@"didReceiveData--%@",[NSThread currentThread]); 

    //拼接服务器返回的数据
    [self.responseData appendData:data];
} 

// 请求完成(成功/失败)时调用,若失败返回error
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {

    NSLog(@"didCompleteWithError--%@",[NSThread currentThread]); 
    
    if(!error)  {
        NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:self.responseData options:kNilOptions error:nil];
        NSLog(@"%@",dict);
    }
}

@end

2. 一个GET请求

使用NSURLSession发送GET请求过程如下:

  • 确定请求URL(GET请求参数直接跟在URL后面)
  • 创建请求对象(默认包含请求头和请求方法GET,可以省略)
  • 创建会话对象(NSURLSession)
  • 根据会话对象创建请求任务(NSURLSessionDataTask),并执行
  • 收到响应后解析数据(XML|JSON|HTTP)

代码如下:

NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login?username=520it&pwd=520it&type=JSON"];

NSURLRequest *request = [NSURLRequest requestWithURL:url]; 
NSURLSession *session = [NSURLSession sharedSession];

// 创建dataTask
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error == nil) { //6.解析服务器返回的数据 //说明:(此处返回的数据是JSON格式的,因此使用NSJSONSerialization进行反序列化处理)
        NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];

        NSLog(@"%@",dict);
    }
}]; 

[dataTask resume];

////也可以省略创建request
//NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login?username=520it&pwd=520it&type=JSON"];
//NSURLSession *session = [NSURLSession sharedSession];
//NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { //5.解析数据
//    NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
//    NSLog(@"%@",dict);
//}];
//[dataTask resume];

3. 一个POST请求

使用NSURLSession发送POST请求过程如下:

  • 确定请求路径URL
  • 创建可变请求对象
  • 修改请求方法为POST
  • 设置请求体,把参数转换为二进制数据并设置请求体
  • 创建会话对象(NSURLSession)
  • 根据会话对象创建请求任务(NSURLSessionDataTask),并执行
  • 收到响应数据,并解析数据(XML|JSON|HTTP)

代码如下:

NSURLSession *session = [NSURLSession sharedSession]; 
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]; 

NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { //8.解析数据
   NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
   NSLog(@"%@",dict);
}];
[dataTask resume];

4. NSURLSessionUploadTask

上传请求在请求头、请求体的格式上有一些特殊要求,需要我们注意。一个简单的上传请求流程如下:

NSURL *url = [NSURL URLWithString:@"https://upload.api.weibo.com/2/statuses/upload.json"];

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";

//设置请求头
[request setValue:[NSString stringWithFormat:@"multipart/form-data; charset=utf-8;boundary=%@", kBoundary] forHTTPHeaderField:@"Content-Type"];

//设置请求体
NSString *imgPath = [[NSBundle mainBundle] pathForResource:@"屏幕快照" ofType:@"png"];
NSData *fileData = [NSData dataWithContentsOfFile:imgPath];
NSData *bodyData = [self buildHTTPBodyWithFileData:fileData];

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromData:bodyData completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    NSLog(@"上传图片成功,%@", response);
}];
[uploadTask resume];

这里我们没有使用代理方法来监听网络请求的过程,而是直接使用了block方法来检测上传任务的完成。

  • 上传请求的Header

默认情况下请求头当中的Content-Type是这样的:

"Content-Type" = "application/x-www-form-urlencoded";

这意味着消息内容会经过URL编码,就像GET请求里URL中的拼接字符串那样。日常的业务交互、向服务器请求数据啥的都不需要自己修改请求头中的这个Content-Type。但是,如果是要向后台服务器上传一个文件,那就必须修改Content-Type的格式:

"Content-Type" = "multipart/form-data; charset=utf-8;boundary=B8Y6J0FD366";

multipart/form-data表明该网络请求是上传文件的;我们上传的文件都是要转成二进制之后才上传的,charset=utf-8表明二进制编码的格式;boundary是内容分隔符,用于分割请求体中的多个模块的内容,不然接收方就无法正常解析和还原这个文件了。

设置请求头代码如下:

// 设置请求头
 [request setValue:[NSString stringWithFormat:@"multipart/form-data; charset=utf-8;boundary=%@", kBoundary] forHTTPHeaderField:@"Content-Type"];
  • 上传请求的Body

在上传文件的网络请求中,请求体的格式是固定的,必须严格按照下面的格式来写:

--AaB03x
Content-Disposition: form-data; name="key1"

value1
--AaB03x
Content-disposition: form-data; name="key2"

value2
--AaB03x
Content-disposition: form-data; name="key3"; filename="file"
Content-Type: application/octet-stream

文件数据...
--AaB03x--

这里的 AaB03x 是我们自定义的、写在请求头中的boundary,用于分割请求体中各个模块的内容。该网络请求的请求体的代码如下:

- (NSData *)buildHTTPBodyWithFileData:(NSData *)fileData {

    //表示用户登录的令牌
    NSString *accessToken = @"5.055TDewTgPDfa63e0f0883oK2XE";
    NSString *weiboText = @"啊哈哈";
    
    NSMutableData *bodyData = [NSMutableData data];
    NSMutableString *bodyString = [NSMutableString string];

    //1.设置accessToken
    [bodyString appendFormat:@"--%@\r\n", kBoundary];
    [bodyString appendFormat:@"Content-Disposition: form-data; name=\"access_token\"\r\n\r\n"];
    [bodyString appendFormat:@"%@\r\n", accessToken];
    
    //2.设置微博发送文本内容
    [bodyString appendFormat:@"--%@\r\n", kBoundary];
    [bodyString appendFormat:@"Content-Disposition: form-data; name=\"status\"\r\n\r\n"];
    [bodyString appendFormat:@"%@\r\n", weiboText];

    //3.设置图片
    [bodyString appendFormat:@"--%@\r\n", kBoundary];
    [bodyString appendFormat:@"Content-Disposition: form-data; name=\"pic\"; filename=\"file\"\r\n"];
    [bodyString appendFormat:@"Content-Type: application/octet-stream\r\n\r\n"];

    //把之前拼接的字符串转成NSData
    NSData *textData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
    [bodyData appendData:textData];
    [bodyData appendData:fileData];

    //4.设置结束行
    NSString *endBoundary = [NSString stringWithFormat:@"\r\n--%@--\r\n", kBoundary];
    [bodyData appendData:[endBoundary dataUsingEncoding:NSUTF8StringEncoding]];
    return bodyData;
}

上传任务需要记住两点:

  1. 请求头的Content-Type需要设置成“multipart/form-data; charset=utf-8;boundary=token”;
  2. 请求体要按照固定格式,并转成NSData格式传进uploadTask中。

关于大文件分片上传过程,还有待深入理解。

5. NSURLSessionDownloadTask

一个简单的下载请求流程如下:

NSURL *url = [NSURL URLWithString:@"http://pic1.win4000.com/pic/b/03/21691230681.jpg"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
   //默认把数据写到磁盘中:tmp/...随时可能被删除
    NSLog(@"location= %@", location);
    
    //转移文件
    NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)  lastObject];
    NSString *filePath = [cache stringByAppendingPathComponent:response.suggestedFilename];
    NSLog(@"filePath = %@",filePath);
    NSURL *toURL = [NSURL fileURLWithPath:filePath];
    [[NSFileManager defaultManager] moveItemAtURL:location toURL:toURL error:nil];
}];
[downloadTask resume];

这里我们没有使用代理方法来监听网络请求的过程,而是直接使用了block方法来检测下载任务的完成。

  • 大文件下载

下面是用NSURLSessionDownloadTask下载大文件的过程,它能够解决内存飙升问题,系统已经实现了断点续传,资源会下载到一个临时文件,下载完成需将文件移动至想要的路径,系统会删除临时路劲文件。注意,断点续传是HTTP协议内功能

#import "ViewController.h"

@interface ViewController ()<NSURLSessionDownloadDelegate>

@property (nonatomic,strong) NSURLSession *session;
@property (nonatomic,strong) NSURLSessionDownloadTask *downloadTask;
@property (nonatomic,strong) NSData *resumeData ;

@end

@implementation ViewController

- (IBAction)startClick:(id)sender {
    
    [self.downloadTask resume];
}

- (IBAction)suspendClick:(id)sender {
    [self.downloadTask suspend];
}

- (IBAction)cancleClick:(id)sender {
    //用cancel的方法,操作不可以恢复
//    [self.downloadTask cancel];
    
    //取消,可以恢复的取消操作
    //resumeDta可以用来恢复下载的数据
    [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
        //data不是沙盒中已经下载好的数据
        self.resumeData = resumeData;
    }];
    
    self.downloadTask = nil;
}

- (IBAction)resumeClick:(id)sender {
    //在恢复下载的时候,判断是否可以用来进行恢复下载的数据,如果有那么就根据该数据创建一个新的网络请求
    if (self.resumeData) {
        //取消 再恢复,在恢复的时候,需要重新创建
        self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
    }
    [self.downloadTask resume];
}

- (NSURLSession *)session {
    if (!_session) {
        _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    return _session;
}

- (NSURLSessionDownloadTask *)downloadTask {
    if (!_downloadTask) {
        NSURL *url = [NSURL URLWithString:@"http://meiye-mbs.oss-cn-shenzhen.aliyuncs.com/mbsFiles/0e3d0e4a0d5d4da5963e9e7617e8de101565841097849.mp4"];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        _downloadTask = [self.session downloadTaskWithRequest:request];
    }
    return _downloadTask;
}

#pragma mark - 代理
//01 写数据的时候调用
/**
 bytesWritten:本次写入的数据大小
 totalBytesWritten:写入数据的总大小
 totalBytesExpectedToWrite:文件的总大小
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
    NSLog(@"111111=%f", 1.0 * totalBytesWritten / totalBytesExpectedToWrite);
}

//02 下载完成的时候调用
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    //转移文件
    NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)  lastObject];
    NSString *filePath = [cache stringByAppendingPathComponent:downloadTask.response.suggestedFilename];
    NSLog(@"filePath = %@",filePath);
    NSURL *toURL = [NSURL fileURLWithPath:filePath];
    [[NSFileManager defaultManager] moveItemAtURL:location toURL:toURL error:nil];
}

//03  整个请求结束或者失败的时候调用
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    NSLog(@"结束");
}
@end

系统会返回NSData对象(resumeData,需要每次存储这个resumeData的值),恢复下载时用这个data创建task。在实现后台传输的过程中对resumeData的处理需要尤为注意。

// 根据NSData对象创建,可以继续上次进度下载
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithResumeData:resumeData];
 
// 根据NSURLRequesta对象创建,开启新的下载
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:model.url]]];

// 开始、继续下载用NSURLSessionTask的resume方法
// 暂停下载用下面方法,这里拿到回调的NSData,保存,可以通过它来创建task实现继续下载
[downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
    model.resumeData = resumeData;
}];
  • 后台下载

实现后台下载,共分三步:

  1. 创建NSURLSession时,需要创建后台模式NSURLSessionConfiguration;
  2. 在AppDelegate中实现下面方法,并定义变量保存completionHandler
// 应用处于后台,所有下载任务完成调用
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler {
    _backgroundSessionCompletionHandler = completionHandler;
}
  1. 在下载类中实现NSURLSessionDelegate中方法,其实就是先执行完task的协议,保存数据、刷新界面之后再执行在AppDelegate中保存的代码块:
// 应用处于后台,所有下载任务完成及NSURLSession协议调用之后调用
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
    dispatch_async(dispatch_get_main_queue(), ^{
        AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
        if (appDelegate.backgroundSessionCompletionHandler) {
            void (^completionHandler)(void) = appDelegate.backgroundSessionCompletionHandler;
            appDelegate.backgroundSessionCompletionHandler = nil;
            
            // 执行block,系统后台生成快照,释放阻止应用挂起的断言
            completionHandler();
        }
    });
}
  • 程序终止,再次启动继续下载

应用被杀掉时,系统会自动保存应用下载session信息,重新启动应用时,如果创建和之前相同identifier的session,系统会找到对应的session数据,并响应-URLSession: task: didCompleteWithError:方法,打印error输出如下:
error: Error Domain=NSURLErrorDomain Code=-999 "(null)" UserInfo={NSURLErrorBackgroundTaskCancelledReasonKey=0, NSErrorFailingURLStringKey=https://www.apple.com/105/media/cn/iphone-x/2017/01df5b43-28e4-4848-bf20-490c34a926a7/films/feature/iphone-x-feature-cn-20170912_1280x720h.mp4, NSErrorFailingURLKey=https://www.apple.com/105/media/cn/iphone-x/2017/01df5b43-28e4-4848-bf20-490c34a926a7/films/feature/iphone-x-feature-cn-20170912_1280x720h.mp4, NSURLSessionDownloadTaskResumeData=<CFData 0x7ff401097c00 [0x104cb6bb0]>{length = 6176, capacity = 6176, bytes = 0x3c3f786d6c2076657273696f6e3d2231 ... 2f706c6973743e0a}}

可以看到,有几点有用的信息:
1)error.localizedDescription为"(null)",打印结果为"The operation couldn't be completed. (NSURLErrorDomain error -999.)"。
2)[error.userInfo objectForkey:NSURLErrorBackgroundTaskCancelledReasonKey]有值。
3)返回了NSURLSessionDownloadTaskResumeData。

综上所述,进程杀死后再次启动继续下载的思路就是,重启时,创建相同identifier的session,在-URLSession: task: didCompleteWithError:方法中拿到resumeData,用resumeData创建task,就可以恢复下载

  • 下载速度

声明两个变量,一个记录时间,一个记录在特定时间内接收到的数据大小,在接收服务器返回数据的-URLSession: downloadTask: didWriteData: totalBytesWritten: totalBytesExpectedToWrite:方法中,统计接收到数据的大小,达到时间限定时,下载速度=数据/时间,然后清空变量,为方便数据库存储,这里用的时间戳:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    // 记录在特定时间内接收到的数据大小
    model.intervalFileSize += bytesWritten;
    
    // 获取上次计算时间与当前时间间隔
    NSInteger intervals = [[NSDate date] timeIntervalSinceDate:[NSDate dateWithTimeIntervalSince1970:model.lastSpeedTime * 0.001 * 0.001]];
    if (intervals >= 1) {
        // 计算速度
        model.speed = model.intervalFileSize / intervals;
        
        // 重置变量
        model.intervalFileSize = 0;
        model.lastSpeedTime = [[NSNumber numberWithDouble:[[NSDate date] timeIntervalSince1970] * 1000 * 1000] integerValue];
    }
}

参考文章:
https://my.oschina.net/u/4581368/blog/4426757
https://blog.csdn.net/Apel0811/article/details/51597833
https://blog.csdn.net/hero_wqb/article/details/80407478
https://www.cnblogs.com/z-sm/p/12672126.html
https://blog.csdn.net/ye1992/article/details/49998511
https://blog.csdn.net/u013827143/article/details/86222486

相关文章

网友评论

      本文标题:NSURLSession 概要总结

      本文链接:https://www.haomeiwen.com/subject/nbeelltx.html