iOS 开发 NSURLSession使用大全详解(包括请求,上传和断点下载)

NSURLSession基本特点

  • 用于替代 NSURLConnection
  • 支持后台运行的网络任务
  • 暂停、停止、重启网络任务,不再需要 NSOperation 封装
  • 请求可以使用同样的配置容器
  • 直接使用系统方法可以实现文件上传和下载
  • 通过代理方法可以获取文件上传和下载的进度
  • block 和代理都对文件上传和下载起作用
    • 当文件上传时,block和代理可以同时使用
    • 当文件下载时,block和代理不要同时使用

NSURLSession结构图

iOS 开发 NSURLSession使用大全详解(包括请求,上传和断点下载)_第1张图片

  • 为了方便程序员使用,苹果提供了一个全局 session.
  • 所有的 任务(Task) 都是由 session 发起的.
  • 所有的任务默认是挂起的,需要 resume.
  • session可以自定义,自定义的时候可以同时设置代理.
  • session : 如果你要监听上传进度,就不能使用单例session,因为监听上传进度需要使用代理.我们需要自定义session

NSURLSessionConfiguration

用于设置全局的网络会话属性,包括:身份验证,超时时长,缓存策略,Cookie 等.

  • 常用属性

iOS 开发 NSURLSession使用大全详解(包括请求,上传和断点下载)_第2张图片

  • 三个类构造方法,是为不同的案例设计的.

    • defaultSessionConfiguration 返回标准配置,具有共享 NSHTTPCookieStorage,NSURLCache 和 NSURLCredentialStorage.

    • ephemeralSessionConfiguration 返回一个预设配置,没有持久性存储的缓存,Cookie或证书。这对于实现像秘密浏览功能的功能来说,是很理想的.

    • backgroundSessionConfiguration,独特之处在于,会创建一个后台会话。后台会话不同于常规的,普通的会话,它甚至可以在应用程序挂起,退出,崩溃的情况下运行上传和下载任务。初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程提供上下文.

  • 常用 HTTPAdditionalHeaders

//接收数据类型,语言,设备类型
configuration.HTTPAdditionalHeaders = @{
@"Accept": @"application/json",
@"Accept-Language": @"en",
@"User-Agent": @"iPhone"
};
  • NSURLSessionConfiguration使用.

/// 自定义session,设置配置信息 @property (nonatomic, strong) NSURLSession
*session;

//开发中没有额外需求可以不用设置
- (NSURLSession *)session {
    if (_session == nil) {
        // 创建配置信息 : 开发中只需要使用默认的配置信息就好了
        // 当多个任务共享同一个session时候,我们只需要配置一次config,那么有session发起的所有任务,都具备相同的配置信息
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];

        // 请求的超时时长是1秒
        config.timeoutIntervalForRequest = 5.0;
        config.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
        // 告诉服务器我的设备
        config.HTTPAdditionalHeaders = @{@"User-Agent": @"iPhone"};

        _session = [NSURLSession sessionWithConfiguration:config];
    }
    return _session;
}

NSURLSession的GET请求

  1. 准备URL
  2. session
  3. 发起任务(task)
  4. 启动任务(resume)
  5. 处理响应
/// NSURLSession的GET请求 (默认也是GET请求)
- (void)demo {
    // URL
    NSURL *URL = [NSURL URLWithString:@"http://localhost/php/login/login.php?username=zhangsan&password=zhang"];
    // 获取session(单例)
    NSURLSession *session = [NSURLSession sharedSession];
    // 发起任务
    NSURLSessionDataTask *dataTask = [session dataTaskWithURL:URL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        // 处理响应
        if (error == nil && data != nil) {
            // 反序列化
            NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
            NSLog(@"%@",result);
        } else {
            NSLog(@"%@",error);
        }
    }];

    // 启动任务
    [dataTask resume];
}

NSURLSession的POST请求

  1. 准备URL
  2. 创建可变request (设置请求方法和请求体)
  3. session
  4. 发起任务(task)
  5. 启动任务(resume)
  6. 处理响应
- (void)login {
    // URL
    NSURL *URL = [NSURL URLWithString:@"http://localhost/php/login/login.php"];

    // 可变请求对象
    NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:URL];
    // 设置请求方法
    requestM.HTTPMethod = @"POST";

    // 设置请求体信息
    NSString *body = @"username=zhangsan&password=zhang";
    requestM.HTTPBody = [body dataUsingEncoding:NSUTF8StringEncoding];

    // session
    NSURLSession *session = [NSURLSession sharedSession];

    // 发起任务
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:requestM completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        // 处理响应
        if (error == nil && data != nil) {

            // 反序列化
            id result = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
            NSLog(@"%@",result);

        } else {
            NSLog(@"%@",error);
        }
    }];

    // 启动任务
    [dataTask resume];
}

NSURLSession实现POST文件上传(拼接表单数据)

#import "ViewController.h"

@interface ViewController () 

/// 文件上传的session
@property (nonatomic, strong) NSURLSession *session;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

}

- (NSURLSession *)session {
    if (_session == nil) {
        // session的配置信息
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];

        // 自定义session的同时设置代理
        _session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
    }

    return _session;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    // 参数1
    NSString *URLString = @"http://localhost/php/upload/upload.php";
    // 参数2 服务器字段名
    NSString *serverFileName = @"userfile";
    // 参数3 本地文件路径
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"car.jpg" ofType:nil];

    [self uploadFileWithURLString:URLString serverFileName:serverFileName filePath:filePath];
}

/**
 *  单个文件上传的主方法
 *
 *  @param URLString      文件上传的路径
 *  @param serverFileName 服务器接收文件的字段名
 *  @param filePath       要上传的文件的路径(有了文件路径就可以获取文件名和文件的二进制)
 */
- (void)uploadFileWithURLString:(NSString *)URLString serverFileName:(NSString *)serverFileName filePath:(NSString *)filePath
{
    // URL
    NSURL *URL = [NSURL URLWithString:URLString];

    // 可变请求
    NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:URL];
    // 设置请求头信息 Content-Type:
    [requestM setValue:@"multipart/form-data; boundary=mac" forHTTPHeaderField:@"Content-Type"];
    // 设置请求方法
    requestM.HTTPMethod = @"POST";

    // session : 如果你要监听上传进度,就不能使用单例session,因为监听上传进度需要使用代理.我们需要自定义session
//    NSURLSession *session = [NSURLSession sharedSession];

    // 文件上传的二进制请求体
    NSData *data = [self getHTTPBodyWithServerFileName:serverFileName filePath:filePath];

    // 发起上传任务
    NSURLSessionUploadTask *uploadTask = [self.session uploadTaskWithRequest:requestM fromData:data completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        // 处理响应
        if (error == nil && data != nil) {

            // 反序列化
            id result = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
            NSLog(@"%@",result);
        } else {
            NSLog(@"%@",error);
        }
    }];

    // 启动任务
    [uploadTask resume];
}

#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 {

    // 计算进度
    float progress = (float)totalBytesSent / totalBytesExpectedToSend;
    NSLog(@"进度 %f",progress);
}


/**
 *  获取文件上传的请求体信息(二进制)
 *
 *  @param serverFileName 服务器接收文件的字段名
 *  @param filePath       要上传的文件的路径
 *
 *  @return 返回文件上传的请求体二进制信息
 */
- (NSData *)getHTTPBodyWithServerFileName:(NSString *)serverFileName filePath:(NSString *)filePath
{
    // 定义可变的二进制容器,拼接请求体的二进制信息
    NSMutableData *dataM = [NSMutableData data];

    // 定义可变字符串,拼接开始的请求体字符串信息
    NSMutableString *stringM = [NSMutableString string];
    // 拼接文件开始上传的分隔符
    [stringM appendString:@"--mac\r\n"];
    // 拼接表单数据
    [stringM appendFormat:@"Content-Disposition: form-data; name=%@; filename=%@\r\n",serverFileName,[filePath lastPathComponent]];
    // 拼接要上传的文件的类型 : 如果你不想告诉服务器你的文件类型具体是什么,就可以使用 "Content-Type: application/octet-stream"
    [stringM appendString:@"Content-Type: image/jpeg\r\n"];
    // 拼接单穿的换行
    [stringM appendString:@"\r\n"];

    // 在这里(拼接文件的二进制数据之前),把前面的请求体字符串转换成二进制,先拼接一次
    NSData *stringM_data = [stringM dataUsingEncoding:NSUTF8StringEncoding];
    [dataM appendData:stringM_data];

    // 拼接文件的二进制数据
    NSData *file_data = [NSData dataWithContentsOfFile:filePath];
    [dataM appendData:file_data];

    NSString *end = @"\r\n--mac--";
    [dataM appendData:[end dataUsingEncoding:NSUTF8StringEncoding]];

    return dataM.copy;
}

@end

NSURLSession实现PUT文件上传

- (void)demo {
    // 1. URL---上传的文件路径
    NSString *urlStr = @"http://192.168.3.251/uploads/123.jpg";
    NSURL *url = [NSURL URLWithString:urlStr];

    // 2. Request -> PUT,request的默认操作是GET
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:5.0f];
    request.HTTPMethod = @"PUT";

    // *** 设置网络请求的身份验证! ***
    // 1> 授权字符串
    NSString *authStr = @"admin:123456";
    // 2> BASE64的编码,避免数据在网络上以明文传输
    // iOS中,仅对NSData类型的数据提供了BASE64的编码支持
    NSData *authData = [authStr dataUsingEncoding:NSUTF8StringEncoding];
    NSString *encodeStr = [authData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn];
    NSString *authValue = [NSString stringWithFormat:@"Basic %@", encodeStr];
    [request setValue:authValue forHTTPHeaderField:@"Authorization"];

    // 3. Session
    NSURLSession *session = [NSURLSession sharedSession];

    // 4. UploadTask
    NSData *imageData = UIImageJPEGRepresentation(self.imageView.image, 0.75);

    NSURLSessionUploadTask *upload = [session uploadTaskWithRequest:request fromData:imageData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

        if (error != nil) {
            NSLog(@"ERROR -> %@", error.localizedDescription);
        } else {

        }
    }];
    [upload resume];
}

NSURLSession实现文件下载和断点下载

  • NSURLSession实现文件下载时,使用代理去下载,内存就不暴涨,而且可以直接的检测到文件下载的进度
  • NSURLSessionDownloadTask : 在使用它的时候,如果你遵守了代理,就不能使用回调,一旦使用了回调,代理就无效了.
  • 提示 : 代理和回调只能二选一.
  • 续传数据时,依然不能使用回调
  • 使用session实现文件下载时,文件下载结束之后,默认会删除,所以文件下载结束之后,需要我们手动的保存一份
  • 一旦指定了 session 的代理,session会对代理进行强引用,如果不主动取消 session,会造成内存泄漏!
  • 下载任务 一旦调用了cancel方法之后,就真的取消了.所以继续下载其实是新建一个下载任务去继续下载文件.

iOS 开发 NSURLSession使用大全详解(包括请求,上传和断点下载)_第3张图片
iOS 开发 NSURLSession使用大全详解(包括请求,上传和断点下载)_第4张图片

#import "ViewController.h"

@interface ViewController () <NSURLSessionDownloadDelegate>

/// 自定义session,设置代理
@property (nonatomic, strong) NSURLSession *downloadSession;
/// 全局的下载任务
@property (nonatomic, strong) NSURLSessionDownloadTask *downloadTask;
/// 保存续传数据
@property (nonatomic, strong) NSData *resumeData;

@end

@implementation ViewController

#pragma mark - 懒加载downloadSession
- (NSURLSession *)downloadSession{
    if (_downloadSession == nil) {
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        // nil : nil的效果跟 [[NSOperationQueue alloc] init] 是一样的
        _downloadSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
    }
    return _downloadSession;
}

- (void)viewDidLoad {
    [super viewDidLoad];
}

#pragma mark - 下载
- (IBAction)downloadClick:(id)sender {
    // 1. URL
    NSURL *URL = [NSURL URLWithString:@"http://localhost/sogou.zip"];
    // 2. 发起下载任务
    /*
     NSURLSessionDownloadTask : 在使用它的时候,如果你遵守了代理,就不能使用回调,一旦使用了回调,代理就无效了.
     提示 : 代理和回调只能二选一
     */
    self.downloadTask = [self.downloadSession downloadTaskWithURL:URL];
    // 3. 启动下载任务
    [self.downloadTask resume];
}

#pragma mark - 暂停
- (IBAction)pauseClick:(id)sender {
    [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
        // resumeData : 续传数据,当暂停下载之后,会把续传的数据回调出去,方便我们做断点下载
        // resumeData : 已经下载的字节数...
        self.resumeData = resumeData;

        // 拿到续传数据之后,把续传数据保存在沙盒中,APP重启之后,就可以继续下载
        NSString *fullPath = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:@"resume.data"];
        [resumeData writeToFile:fullPath atomically:YES];

        // 当我们第一次点击暂停时,会成功的回调resumeData数据,但是再次点击时,就回调一个空的resumeData
        NSLog(@"%tu",resumeData.length);

        // 为了避免第二次点击暂停时,resumeData为空,
        self.downloadTask = nil;
    }];

    // suspend有时候不太靠谱,而且如果成功的暂停了,程序退出,再启动,不能实现继续下载
//    [self.downloadTask suspend];
    NSLog(@"暂停");
}

#pragma mark - 断点下载
- (IBAction)resumeClick:(id)sender {
    // 0.541026 0.543065
    // 当内存中没有续传数据时,重新启动程序时
    if (self.resumeData == nil) {
        NSString *fullPath = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:@"resume.data"];
        NSData *resume_data = [NSData dataWithContentsOfFile:fullPath];
        if (resume_data == nil) {
            // 即没有内存续传数据,也没有沙盒续传数据,就续传了
            return;
        } else {
            // 当沙盒有续传数据时,在内存中保存一份
            self.resumeData = resume_data;
        }
    }

    // 续传数据时,依然不能使用回调
    // 续传数据时起始新发起了一个下载任务,因为cancel方法是把之前的下载任务干掉了 (类似于NSURLConnection的cancel)
    // resumeData : 当新建续传数据时,resumeData不能为空,一旦为空,就崩溃
    // downloadTaskWithResumeData :已经把Range封装进去了

    if (self.resumeData != nil) {
        self.downloadTask = [self.downloadSession downloadTaskWithResumeData:self.resumeData];
        // 重新发起续传任务时,也要手动的启动任务
        [self.downloadTask resume];
        NSLog(@"继续");
    }
}

#pragma NSURLSessionDownloadDelegate--监听下载进度
/// 监听文件下载进度的代理方法
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
      totalBytesWritten:(int64_t)totalBytesWritten
      totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
    // 计算进度
    float progress = (float)totalBytesWritten / totalBytesExpectedToWrite;
    NSLog(@"%f",progress);
}

#pragma mark - 文件下载结束时的代理方法 (必须实现的)--将文件拷贝到其他地方
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location {
    // location : 文件下载结束之后的缓存路径
    // location.path : 把带协议头的路径转换成字符串路径 (去掉协议头之后的路径)
    // 使用session实现文件下载时,文件下载结束之后,默认会删除,所以文件下载结束之后,需要我们手动的保存一份
    NSLog(@"%@",location.path);
    NSString *path = [[NSHomeDirectory() stringByAppendingPathComponent:@"Documents"] stringByAppendingPathComponent:@"demo.zip"];
    // 文件下载结束之后,需要立即把文件拷贝到一个不会销毁的地方
    [[NSFileManager defaultManager] copyItemAtPath:location.path toPath:path error:NULL];
}

@end

NSURLSession断点下载时的循环引用

  • 一旦指定了 session 的代理,session会对代理进行强引用,如果不主动取消 session,会造成内存泄漏!
    iOS 开发 NSURLSession使用大全详解(包括请求,上传和断点下载)_第5张图片
//两种方法解除循环引用,取其一即可
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    // 在控制器即将销毁时,将sessio立即置为无效
    [self.downloadSession invalidateAndCancel];

// 在控制器即将销毁时,当下载任务执行结束之后再把session置为无效
//    [self.downloadSession finishTasksAndInvalidate];
}

参考: NSURLSessionConfiguration笔记

你可能感兴趣的:(网络编程与通信,iOS开发)