移动开发网络手机网络

iOS 移动开发网络 part2:NSURLSession

2017-10-24  本文已影响54人  破弓

1.准备

The NSURLSession class and related classes provide an API for downloading content via HTTP. 
This API provides a rich set of delegate methods for supporting authentication 
and gives your app the ability to perform background downloads when your app is not running or, in iOS, while your app is suspended.

NSURLSession及其相关类是iOS系统提供给我们用于通过HTTP协议完成数据请求与下载的.
NSURLSession及其相关类有丰富的代理方法支持身份认证,后台下载.

1.1 类组成

nsobject_hierarchy.png

除了上面这些完成网络服务必须的类之外,NSURLSession还有自己特有的网络任务类==>NSURLSessionTask.如下:

class_relationship.png

1个NSURLSession可以带有多个NSURLSessionTask.

[NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
[NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]];
[NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"string unique"]];

NSURLSession的类型完全取决于创建时传入NSURLSessionConfiguration的类型.

defaultSession:将请求的数据缓存在硬盘上,并会将请求时获取到的证书放到keychain内.
ephemeralSession:请求到的数据,请求时获取到的证书都只存在于内存中,app一关闭,数据与证书都会被清除.
backgroundSession:与defaultSession很相似,唯一不同的就是backgroundSession可以用于后台下载.注意创建时必须填入的Identifier.关于后台下载的具体流程后文会说.

NSURLSession *session = [NSURLSession sharedSession];

以上方法是全局提供的一个session单例,不想自己创建可以用这个,当然session单例是defaultSession类型.

NSURLSessionDataTask:网络请求
NSURLSessionDownloadTask:下载,特别是后台下载
NSURLSessionUploadTask:上传,支持后台上传
NSURLSessionStreamTask:流交互

特别备注:本文会多处提到与Stream相关的内容(NSURLSessionStreamTask,NSURLSessionStreamDelegate),但由于笔者并没有流交互的开发经验,对这一块也不甚了解,所以全文对Stream的相关内容描述得会非常简略,还请见谅.

1.2 协议组成

NSURLSession对应一个协议.
NSURLSessionTask对应一个协议.
不同的NSURLSessionTask的子类对应不同协议.

protocol_relationship.png
NSURLSessionDelegate

/* 
当前这个session已经失效时,该代理方法被调用.
如果你使用finishTasksAndInvalidate函数使该session失效,
那么session首先会先完成最后一个task,然后再调用这个方法,
如果你调用invalidateAndCancel方法来使session失效,那么该session会立即调用下面的代理方法.
*/
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error;

/*
session级别的HTTPS认证
*/
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;

/* 
后台下载的时候,在任务进行中,处于后台的app会因为网络验证的等原因获取到焦点,系统会调用以下方法.
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler;
传入的identifier代表某一个session,如果这个session不存在,则要创建这个session.同时需要保存传入的completionHandler.

上面的session需要处理的消息被派发后,会调用下面的代理方法,我们就在这个时候执行上面保存的completionHandler.
(后台下载本身就非常复杂,后文会细说)
 */
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session;
NSURLSessionTaskDelegate(方法比较多,只说常用的几个,其他的太冷门了)

/* 
重定向
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                     willPerformHTTPRedirection:(NSHTTPURLResponse *)response
                                     newRequest:(NSURLRequest *)request
                              completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler;

/*
task级别的HTTPS认证
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                            didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge 
                              completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler;

/*
当一个task需要发送一个新的request body stream到服务器端的时候,调用该代理方法
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                              needNewBodyStream:(void (^)(NSInputStream * _Nullable bodyStream))completionHandler;

/*
周期性地通知代理发送到服务器端数据的进度
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                                didSendBodyData:(int64_t)bytesSent
                                 totalBytesSent:(int64_t)totalBytesSent
                       totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend;

/* 
task完成之后的回调,成功和失败都会回调这里
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                           didCompleteWithError:(nullable NSError *)error;
/*
NSURLSessionDataDelegate

task收到响应,completionHandler可以控制接下来是:取消请求,允许请求,切换成下载的task,切换成流交互的task,
typedef NS_ENUM(NSInteger, NSURLSessionResponseDisposition) {
    NSURLSessionResponseCancel = 0,
    NSURLSessionResponseAllow = 1,
    NSURLSessionResponseBecomeDownload = 2,
    NSURLSessionResponseBecomeStream
};
*/
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                                 didReceiveResponse:(NSURLResponse *)response
                                  completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;

/* 
收到通知dataTask需要变成downloadTask(本协议第一个方法返回NSURLSessionResponseBecomeDownload)
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                              didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask;

/* 
收到通知dataTask需要变成streamTask(本协议第一个方法返回NSURLSessionResponseBecomeStream)
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                                didBecomeStreamTask:(NSURLSessionStreamTask *)streamTask;

/*
请求收到数据
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                                     didReceiveData:(NSData *)data;


/*
completionHandler内传入想缓存的响应,传入nil就不保存响应
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                                  willCacheResponse:(NSCachedURLResponse *)proposedResponse 
                                  completionHandler:(void (^)(NSCachedURLResponse * _Nullable cachedResponse))completionHandler;
NSURLSessionDownloadDelegate

/* 
单个task下载完成,系统给出下载文件的临时地址,我们需要将文件转移到我们自己设定的地址上
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
                              didFinishDownloadingToURL:(NSURL *)location;

/* 
周期性汇报下载进度
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
                                           didWriteData:(int64_t)bytesWritten
                                      totalBytesWritten:(int64_t)totalBytesWritten
                              totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;

/* 
当下载被取消或者出错会调用
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error,
error的字典内用NSURLSessionDownloadTaskResumeData作键保存了下载到中途的数据.
我们用downloadTaskWithResumeData:方法可以重新开始相同的下载任务,重新开始下载的同时就会调用这个代理方法
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
                                      didResumeAtOffset:(int64_t)fileOffset
                                     expectedTotalBytes:(int64_t)expectedTotalBytes;
NSURLSessionStreamDelegate 学习中

1.3 代理 or block

在用NSURLSession完成网络任务的时候,可以有两种方式得到返回数据:代理,block.
block的方式相对来说比较简单,但其实是系统内部提供了代理.

如果需要用NSURLSession完成后台下载,自定义验证,自定义缓存,限制重定向.....,则必须提供代理,并实现代理方法,以便对整个网络请求的流程了如指掌.

Uses background sessions to download or upload content while your app is not running.
Performs custom authentication.
Performs custom SSL certificate verification.
Decides whether a transfer should be downloaded to disk or displayed based on the MIME type returned by the server or other similar criteria.
Uploads data from a body stream (as opposed to an NSData object).
Limits caching programmatically.
Limits HTTP redirects programmatically.

本文代码参考demo

2.请求

- (void)reqest{
    NSURL *url = [NSURL URLWithString:@"http://c.3g.163.com/photo/api/list/0096/4GJ60096.json"];
    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url
            completionHandler:^(NSData *data,
                                NSURLResponse *response,
                                NSError *error) {
                id result = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
                NSLog(@"result: %@", result);
            }];
    [dataTask resume];
}
@interface NSURLSessionController ()<NSURLSessionDataDelegate>
{
    NSMutableData * _allData;
}
@end

@implementation NSURLSessionController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self reqest];
}

- (void)reqest{
    _allData = [NSMutableData data];
    
    NSURLSessionConfiguration *configura = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configura delegate:self delegateQueue:nil];
    NSURL *url = [NSURL URLWithString:@"http://c.3g.163.com/photo/api/list/0096/4GJ60096.json"];
    NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url];
    [dataTask resume];
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    [_allData appendData:data];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    if (error) {
        NSLog(@"%@",[error localizedDescription]);
        return;
    }
    
    NSError *er = nil;
    id result = [NSJSONSerialization JSONObjectWithData:_allData options:NSJSONReadingMutableContainers error:&er];
    NSLog(@"result: %@", result);
}

@end

3.下载

前面已经说过后台下载必须用backgroundSession.而且用到了backgroundSession,就必须用代理的方式去获取下载到的数据,否则跑起来崩溃.

3.1 普通下载

如果你的app不想支持后台下载,只用到普通下载的话,以下代码就足够了.

- (void)download_code{
    NSString * libPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];
    NSString * fullPath  =[NSString stringWithFormat:@"%@/dl.data", libPath];
    
    NSURLSessionConfiguration *sessionConfig =[NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession * session = [NSURLSession sessionWithConfiguration:sessionConfig];
    NSURL * url = [NSURL URLWithString:@"http://dl1sw.baidu.com/soft/38/10041/xcn-dvd-creator_6.1.4.0124.exe?version=3807236358"];
    NSURLSessionDownloadTask * downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if(error){
            NSLog(@"%@",[error localizedDescription]);
        }
        if (location) {
            [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:fullPath] error:nil];
        }
    }];
    [downloadTask resume];
}
[_downloadTask suspend];
[_downloadTask resume];
__weak __typeof(&*self)weakSelf = self
[_downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
    weakSelf.keepData = resumeData;
}];
_downloadTask = [session downloadTaskWithResumeData:_keepData];
[_downloadTask resume];

以上两段代码都能达到暂停与重新开始的功能.而第二段代码利用下载到一半的数据继续下载的方式会在后台下载中的断点续传中起到很重要的作用.

当然对于大文件(图片不算是大文件)来说,厘定普通下载后台下载这两个概念本身就是件很搞笑的事.因为没有任何开发者可以确定自己定义的大文件下载任务一定能在app处于前台的情况下完成.

其实我想说的只是:大文件下载不支持后台下载,那只能证明==>作为开发者的你一点自尊心都没有,曾经我就是,😁 😁 😁 .

3.2 后台下载

温馨提示:后台下载的内容是本文最重要,最难也是最精彩,最有用的部分.一定要好好看.

前面已经说过后台下载必须用backgroundSession.而且后台下载必须用代理的方式去获取下载到的数据,否则会崩溃.

建议用真机测试,模拟器会有些问题.

3.1.1 单段后台下载
AppDelegate
//后台下载完成,会调用这个方法,传入后台下载session的identifier和一个completionHandler.
//completionHandler得保存下来,后面方法会执行,以告知系统后台下载session的消息处理完毕
//一个identifier对应一个completionHandler,是因为有可能出现有多个后台下载session的情况,这样就可以区分开
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler{
    [_muDic setObject:completionHandler forKey:identifier];
}
DownloadController

//单段后台下载
- (void)back_download_code_1{
    NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"zc string 1"];
    NSURLSession * session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
    NSURL * url = [NSURL URLWithString:@"http://dl1sw.baidu.com/soft/38/10041/xcn-dvd-creator_6.1.4.0124.exe?version=3807236358"];
    NSURLSessionDownloadTask * downloadTask = [session downloadTaskWithURL:url];
    [downloadTask resume];
}

#pragma mark NSURLSessionDelegate
//调用上面提到的completionHandler
typedef void(^CompletionHandlerType)();
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
    NSLog(@"zc NSURLSessionDelegate 3");
    CompletionHandlerType block = ((AppDelegate *)[UIApplication sharedApplication].delegate).muDic[session.configuration.identifier];
    if (block) {
        block();
    }
}

#pragma mark NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error{
    NSLog(@"zc NSURLSessionTaskDelegate 8");
}

#pragma mark NSURLSessionDownloadDelegate
//下载完成,转移临时文件到自己想要的目录
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location{
    NSLog(@"zc NSURLSessionDownloadDelegate 1");
    NSString * libPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];
    NSString * fullPath  =[NSString stringWithFormat:@"%@/dl.data", libPath];
     [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:fullPath] error:nil];
}
//下载中,汇报进度
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
    NSLog(@"zc NSURLSessionDownloadDelegate 2 %f",totalBytesWritten/(totalBytesExpectedToWrite*1.0));
}
//下载取消或者报错,利用resumeData重启下载任务都会由这个方法传出
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
 didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes{
    NSLog(@"zc NSURLSessionDownloadDelegate 3");
}

@end
//后台下载时候,多个任务完成后,方法调用顺序:
 - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler;
 - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location;
 - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session;

后台下载的主干方法就是这些,接下来的介绍是对这些方法的延伸.

3.1.2 多段后台下载

多个url都要进行后台下载,方案与结果如下:

plan a:一个session,一个task下载好,再建一个task,再下载 可以 V
plan b:一个session,task全部建好,一个task下载好,开始下载下一个 不可以 X
plan c:一个session,task全部建好,全部开始下载  可以 V
plan d:多个session,session与task一一对应,task全部建好,全部开始下载 可以 V

总结就一句话:所有建好的task都要立马开始下载,否则就失败.

考虑到内存开销,我们自然不会用多个session的方案.那么留给我们的只有:

plan a:一个session,一个task下载好,再建一个task,再下载 可以 V
plan c:一个session,task全部建好,全部开始下载 可以 V

对比之下,plan a更省内存开销,那我们就选plan a?
答:NO.

当一个session内所有的task后台下载全部完成,app在后台会重启一次(并不是app显示到前台,而是获取到焦点)并调用如下方法列表:

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler;
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location;
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session;

100个url,开始就创建100个task由1个session包含,100个task全完成后会重启app;==>重启1次
100个url,开始就创建1个task由1个session包含,完成1个task再创建1个task,每一次task完成,app都会重启一次;==>重启100次

但苹果系统对滥用后台重启app是有限制的.(https://forums.developer.apple.com/thread/14854#42352)

plan a的代码跑起来会是这样:
task1完成==>task2开始 间隔1个单位时间
task2完成==>task3开始 间隔2个单位时间
task3完成==>task4开始 间隔4个单位时间
以此类推.

以上是我拿血换来的教训.

你没的选了,就是这个了.

plan c:一个session,task全部建好,全部开始下载 V

但想想如果资源分段真的很多怎么办?1000url,真的要创建1000个task,还要全部都开启下载,好可怕啊!!!

In that case it’s much better to change your design to use fewer, 
larger transfers.

在回答中苹果的客服建议:资源如果要分段,那么分段应该尽可能少,每一段应该尽可能大一些.所以众多视频网站的做法是:播放用m3u8(可以多达近千个分段),下载是另外的一组url(十个分段左右).这是我拿越狱机看youku下载好电视剧的文件夹里的文件才知道的.

多段后台下载:一个session,task全部建好,全部开始下载.资源分段应该尽可能少,每一段应该尽可能大一些.

3.1.3 断点续传

app在进入后台,正进行着后台下载,还有可能被杀掉,那么如何找回下载到一半的数据,然后接着下载呢?

我们假定下载资源是分为10段,有10个url.

1.10个url,创建1个backgroundSession+10个task开始下载.开始下载的同时,本地静态化保存要下载的url数组.如果后台下载任务全部完成,则以下方法链会被调用,特别注意第二个方法会被调用10次,我们可以在这个方法中一个个的删除保存下来的要下载的url.

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler;
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location;//10次
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session;

2.app退到后台,杀掉app.
3.app再打开时,检测是否有下载到一半的任务(即:本地静态化保存要下载的url数组是否存在).
4.有,就用相同的identifier重建backgroundSession.
5.下面的方法就会被调用,方法2中error的userInfo会以NSURLSessionDownloadTaskResumeData键保存着下载到一半的数据.10个url,如果5个已经完成下载,方法1会被调用5次,方法2会被调用10次(10次里有5次error为空).

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location;//5次
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error;//10次,5次error为空

6.拿到resumeData,用以下方法就可以让下载到一半的task继续(跟上面提到暂停+重新开始很相像对吧).

NSURLSessionDownloadTask * downloadTask = [session downloadTaskWithResumeData:resumeData];
[downloadTask resume];

iOS10系统中downloadTaskWithResumeData:方法有点小问题,Hank同学改进的这个方法,大家请放心使用.

NSURLSession+CorrectedResumeData.h
- (NSURLSessionDownloadTask *)downloadTaskWithCorrectResumeData:(NSData *)resumeData;

详细实践细节见于:ResumeMultiDownloadController.

3.1.4 换新的url

这个需求不一定谁家都有,但像视频大厂,单段视频的url是有有效期的,所以的再生成断点续传的task后还要换一下url.

NSURLSessionDownloadTask * downloadTask = nil;
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 10.0 && [[[UIDevice currentDevice] systemVersion] floatValue] < 11.0) { {
    downloadTask = [session downloadTaskWithCorrectResumeData:resumeData];
} else {
    downloadTask = [session downloadTaskWithResumeData:resumeData];
}

NSString * freshUrlString = @"";
NSURL * downloadURL = [NSURL URLWithString:freshUrlString];
NSURLRequest * request = [NSURLRequest requestWithURL:downloadURL];
[downloadTask setValue:request forKey:@"originalRequest"];

[downloadTask resume];

4.上传

试了试不用AFN,做上传,结果各种不会!没有AFN的我就好像白痴!!!看别人写了文章,最后梳理出一下代码:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 参数1
    NSString *URLString = @"http://192.168.8.11/upload.php";
    // 参数2
    NSString *serverFileName = @"zc[]";
    // 参数3
    NSString *filePath1 = [[NSBundle mainBundle] pathForResource:@"on_show_1.png" ofType:nil];
    NSString *filePath2 = [[NSBundle mainBundle] pathForResource:@"on_show_2.png" ofType:nil];
    NSArray *filePaths = @[filePath1,filePath2];
    // 参数4
    NSDictionary *textDict = @{@"kkk":@"vvv"};
    
    // 调用文件上传的主方法
    [self uploadFilesWithURLString:URLString serverFileName:serverFileName filePaths:filePaths textDict:textDict];
}

- (void)uploadFilesWithURLString:(NSString *)URLString serverFileName:(NSString *)serverFileName filePaths:(NSArray *)filePaths textDict:(NSDictionary *)textDict
{
    // URL
    NSURL *URL = [NSURL URLWithString:URLString];
    
    // 可变请求
    NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:URL];
    // 设置请求头
    [requestM setValue:@"multipart/form-data; boundary=itcast" forHTTPHeaderField:@"Content-Type"];
    // 设置请求方法
    requestM.HTTPMethod = @"POST";
    // 设置请求体
    requestM.HTTPBody = [self getHTTPBodyWithServerFileName:serverFileName filePaths:filePaths textDict:textDict];
    
    // 发送请求实现文件上传
    [[[NSURLSession sharedSession] dataTaskWithRequest:requestM completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        if (error == nil && data != nil) {
            NSLog(@"%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
            NSLog(@"44");
        } else {
            NSLog(@"%@",error);
        }
    }] resume];
}

- (NSData *)getHTTPBodyWithServerFileName:(NSString *)serverFileName filePaths:(NSArray *)filePaths textDict:(NSDictionary *)textDict
{
    // 定义dataM拼接请求体二进制数据
    NSMutableData *dataM = [NSMutableData data];
    
    // 循环拼接文件二进制信息
    [filePaths enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        // 用于字符串信息
        NSMutableString *stringM = [NSMutableString string];
        // 拼接文件开始的分隔符
        [stringM appendString:@"--itcast\r\n"];
        // 拼接表单数据
        [stringM appendFormat:@"Content-Disposition: form-data; name=%@; filename=%@\r\n",serverFileName,[obj lastPathComponent]];
        // 拼接文件类型
        [stringM appendString:@"Content-Type: image/png\r\n"];
        // 拼接单纯的换行
        [stringM appendString:@"\r\n"];
        // 把前面的字符串信息拼接到请求体里面
        [dataM appendData:[stringM dataUsingEncoding:NSUTF8StringEncoding]];
        
        // 拼接文件的二进制数据到dataM
        [dataM appendData:[NSData dataWithContentsOfFile:obj]];
        
        // 拼接二进制数据后面的换行
        [dataM appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
        
    }];
    
    // 拼接文件的文本信息
    [textDict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        // 用于拼接文本信息
        NSMutableString *stringM = [NSMutableString string];
        // 拼接文本信息的开始分割符
        [stringM appendString:@"--itcast\r\n"];
        // 拼接表单数据
        [stringM appendFormat:@"Content-Disposition: form-data; name=%@\r\n",key];
        // 拼接单纯的换行
        [stringM appendString:@"\r\n"];
        // 拼接文本信息
        [stringM appendFormat:@"%@\r\n",obj];
        
        // 把文本信息拼接到请求体
        [dataM appendData:[stringM dataUsingEncoding:NSUTF8StringEncoding]];
    }];
    
    // 拼接文件上传的结束分隔符
    [dataM appendData:[@"--itcast--" dataUsingEncoding:NSUTF8StringEncoding]];
    
    return dataM.copy;
}

最终发给服务端的HTTPBody,长的会是下面这个样子.

--itcast
Content-Disposition: form-data; name=zc[]; filename=on_show_1.png
Content-Type: image/png
[image]
--itcast
Content-Disposition: form-data; name=zc[]; filename=on_show_2.png
Content-Type: image/png
[image]
--itcast
Content-Disposition: form-data; name=kkk
vvv
--itcast--

上面的代码是拿block实现的,上传如果要用代理来实现就得用NSURLSessionDataDelegate协议.

//汇报上传进度
-(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);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    [_allData appendData:data];
}
- (void)URLSession:(__unused NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
    NSLog(@"%@",[[NSString alloc]initWithData:_allData encoding:NSUTF8StringEncoding]);
}

除了一个进度汇报,上传的代理方法实现与请求的是一样的.

5.流交互

学习中,暂且不表

6.其他注意

@property (nullable, readonly, retain) id <NSURLSessionDelegate> delegate;

NSURLSession内部对delegate保持着强引用,所以当不在需要NSURLSession做任何事情的时候,必须将NSURLSession置为失效,否则会造成内存泄漏.

invalidateAndCancel //NSURLSession立刻失效
finishTasksAndInvalidate //NSURLSession完成现有的task后再失效

文章参考:
URL Session Programming Guide
NSURLSession上传的基本用法

上一篇下一篇

猜你喜欢

热点阅读