IOS基础网络:断点续传和后台下载

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、使用基于NSURLSession实现的下载工具类
    • 1、控制下载
    • 2、获取下载进度
    • 3、播放视频
  • 二、基于NSURLSession实现下载工具类
    • 1、控制下载进度
    • 2、实现NSURLSessionDelegate委托方法
    • 3、点击暂停保存按钮手动保存下载进度
    • 4、通过计时器自动保存下载进度
    • 5、任务被意外杀死或者处于后台下载的情况
  • 三、基于AFNetworking实现断点续传
    • 1、断点续传简介
    • 2、原理介绍
    • 3、运行效果
    • 4、创建键值对 url:resumeData 存储下载进度
    • 5、控制下载流程
    • 6、创建文件下载任务
    • 7、并发属性的使用场景
  • Demo
  • 参考文献

一、使用基于NSURLSession实现的下载工具类

1、控制下载

开始/继续下载
- (void)startNew
{
    NSString *fileUrl = @"https://pic.ibaotu.com/00/48/71/79a888piCk9g.mp4";
    if(self.fileDownloadNetwork == nil)
    {
        self.fileDownloadNetwork = [DownloadNetworkTool new];
        self.fileDownloadNetwork.tag = 1;
        self.fileDownloadNetwork.myDeleate = self;
    }
    [self.fileDownloadNetwork downFile:fileUrl];
}
暂停下载
- (void)pause
{
    [self.fileDownloadNetwork suspendDownload];
}
暂停并保存下载进度
- (void)suspendAndSaveFileDownload
{
    [self.fileDownloadNetwork suspendAndSaveFileDownload];
}

2、获取下载进度

每返回一个数据包就调用一次以显示下载进度
- (void)backDownprogress:(float)progress tag:(NSInteger)tag
{
    self.progressView.progress = progress;
    self.progressLabel.text = [NSString stringWithFormat:@"%0.1f%@",progress*100,@"%"];
}
下载成功
- (void)downSucceed:(NSURL*)url tag:(NSInteger)tag
{
    NSLog(@"下载成功,准备播放");
    [self paly: url];
    
    self.progressView.progress = 0;
    self.progressLabel.text = @"0.0%";
    self.fileDownloadNetwork = nil;
}
下载失败
- (void)downError:(NSError*)error tag:(NSInteger)tag
{
    NSLog(@"下载失败,请重新下载 :%@",error);
    
    self.fileDownloadNetwork = nil;
    self.progressView.progress = 0;
    self.progressLabel.text = @"0.0%";
}

3、播放视频

- (void)paly:(NSURL*)playUrl
{
    // 系统的视频播放器
    AVPlayerViewController *controller = [[AVPlayerViewController alloc]init];
    // 播放器的播放类
    AVPlayer * player = [[AVPlayer alloc]initWithURL:playUrl];
    controller.player = player;
    // 自动开始播放
    [controller.player play];
    // 展示视屏播放器
    [self presentViewController:controller animated:YES completion:nil];
}

二、基于NSURLSession实现下载工具类

1、控制下载进度

下载文件
- (void)downFile:(NSString*)fileUrl
{
    if (!fileUrl || fileUrl.length == 0 || ![self checkIsUrlAtString:fileUrl])
    {
        NSLog(@"fileUrl 无效");
        return ;
    }
    
    NSURL *url = [[NSURL alloc] initWithString:fileUrl];
    if (!self.session)
    {
        // 创建网络会话
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[self currentDateStr]];
        config.allowsCellularAccess = YES;
        config.timeoutIntervalForRequest = 30;
        self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    
    NSFileManager *fileManager = NSFileManager.defaultManager;
    if (![fileManager fileExistsAtPath:[self getTmpFileUrl]])
    {
        // 下载文件不存在则新建下载任务
        self.downloadTask = [self.session downloadTaskWithURL:url];
    }
    else
    {
        // 下载文件已经存在则继续
        [self downloadWithResumeData];
    }
    [self.downloadTask resume];
}
暂停下载
- (void)suspendDownload
{
    if (self.isSuspend)
    {
        [self.downloadTask resume];
    }
    else
    {
        [self.downloadTask suspend];
    }
    self.isSuspend = !self.isSuspend;
}
点击暂停的时候需要保存目前已经下载的data
- (void)suspendAndSaveFileDownload
{
    [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
        [resumeData writeToFile:[self getTmpFileUrl] atomically:YES];
        self.downloadTask = nil;
    }];
}

2、实现NSURLSessionDelegate委托方法

每传一个数据包则调用一次该函数
- (void)URLSession:(nonnull NSURLSession *)session downloadTask:(nonnull NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    // 根据tag记录每个任务的下载进度,用于继续下载
    float downloadProgress = 1.0 * totalBytesWritten/totalBytesExpectedToWrite;

    if(self.myDeleate && [self.myDeleate respondsToSelector:@selector(backDownprogress:tag:)])
    {
        [self.myDeleate backDownprogress:downloadProgress tag:self.tag];
    }
}
下载完成之后调用该方法更换下载文件路径
- (void)URLSession:(nonnull NSURLSession *)session downloadTask:(nonnull NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(nonnull NSURL *)location
{
    NSLog(@"下载文件的临时路径:%@",location.path);
    
    // 下载文件的更换路径
    NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES) lastObject];
    NSString *file = [documentsPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.mp4",self.fileName]];

    NSFileManager *manager = [NSFileManager defaultManager];
    if ([manager fileExistsAtPath: file])
    {
        // 如果文件夹下有同名文件则将其删除
        [manager removeItemAtPath:file error:nil];
    }
    
    // 将视频资源从原有路径移动到自己指定的路径
    NSError *saveError;
    [manager moveItemAtURL:location toURL:[NSURL URLWithString:file] error:&saveError];
    BOOL success = [manager copyItemAtPath:location.path toPath:file error:nil];
    if (success)
    {
        dispatch_async(dispatch_get_main_queue(), ^{
            // 回传移动后的文件路径的url
            NSURL *url = [[NSURL alloc] initFileURLWithPath:file];
            if(self.myDeleate && [self.myDeleate respondsToSelector:@selector(downSucceed:tag:)])
            {
                [self.myDeleate downSucceed:url tag:self.tag];
            }
        });
    }
    
    // 已经拷贝则删除临时缓存文件
    [manager removeItemAtPath:location.path error:nil];
    [manager removeItemAtPath:[self getTmpFileUrl] error:nil];
}
下载失败调用,根据不同错误反馈不同
- (void)URLSession:(nonnull NSURLSession *)session task:(nonnull NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error
{
    if(error && self.myDeleate && [self.myDeleate respondsToSelector:@selector(downError:tag:)] && error.code != -999)
    {
        [self.myDeleate downError:error tag:self.tag];
    }
}
后台任务下载完成
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
    NSLog(@"所有后台任务已经完成: %@",session.configuration.identifier);
}

3、点击暂停保存按钮手动保存下载进度

通过之前保存的数据创建下载任务继续下载
- (void)downloadWithResumeData
{
    if (!self.session)
    {
        return;
    }
    
    NSFileManager *fileManager = NSFileManager.defaultManager;
    NSData *dowloadData = [fileManager contentsAtPath:[self getTmpFileUrl]];
    self.downloadTask = [self.session downloadTaskWithResumeData:dowloadData];
    self.resumeData = nil;
}
提供未下载完的临时文件的url地址
- (NSString*)getTmpFileUrl
{
    NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *filePath = [docPath stringByAppendingPathComponent:@"download.tmp"];
    NSLog(@"未下载完的临时文件的url地址:%@",filePath);

    return filePath;
}

4、通过计时器自动保存下载进度

预防下载过程中突然杀掉app需要开启定时器提前保存临时文件
- (void)saveTmpFile
{
    [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(downloadTmpFile) userInfo:nil repeats:YES];
}
由于通过计时器提前保存了临时文件,杀掉app后不至于下载的部分文件全部丢失
- (void)downloadTmpFile
{
    if (self.isSuspend)
    {
        return;
    }
    
    [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
        [resumeData writeToFile:[self getTmpFileUrl] atomically:YES];
        self.downloadTask = nil;
        self.downloadTask = [self.session downloadTaskWithResumeData:resumeData];
        [self.downloadTask resume];
    }];
}

5、任务被意外杀死或者处于后台下载的情况

使程序处于后台被杀死时调用到applicationWillTerminate:方法
- (void)applicationDidEnterBackground:(UIApplication *)application
{
    [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(){}];
}
将identifier与task绑定起来才知道每个被杀死或者处于后台下载的任务的进度情况
@property (nonatomic, copy) NSString *identifier;

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler
{
    self.identifier = identifier;
}
任务已经下载好了
- (void)applicationWillEnterForeground:(UIApplication *)application
{
    NSLog(@"任务已经下载好了: %@",self.identifier);
}
程序被杀死
- (void)applicationWillTerminate:(UIApplication *)application
{
    NSLog(@"程序被杀死,applicationWillTerminate");
}

三、基于AFNetworking实现断点续传

1、断点续传简介

断点续传的原因

如果是小文件的下载,比如图片和文字之类的,我们可以直接请求源地址,然后一次下载完毕。但是如果是下载较大的音频和视频文件,不可能一次下载完毕,用户可能下载一段时间,关闭程序,回家接着下载。这个时候,就需要实现断点续传的功能,让用户可以随时暂停下载,下次开始下载,还能接着上次的下载的进度。

在下载(或上传)过程中,如果网络故障、电量不足等原因导致下载中断,这也需要使用到断点续传功能。下次启动时,可以从记录位置(已经下载的部分)开始,继续下载未下载的部分,避免重复部分的下载。断点续传实质就是能记录上一次已下载完成的位置。

使用多线程断点续传下载的时候,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,多个线程并发可以占用服务器端更多资源,从而加快下载速度。

断点续传的过程
  1. 断点续传需要在下载过程中记录每条线程的下载进度
  2. 每次下载开始之前先读取数据库,查询是否有未完成的记录,有就继续下载,没有则创建新记录插入数据库
  3. 在每次向文件中写入数据之后,在数据库中更新下载进度
  4. 下载完成之后删除数据库中下载记录
封装一个断点续传的类实现如下功能
  • 使用者只需要调用一个接口即可以下载,同时可以获取下载的进度
  • 下载成功,可以获取文件存储的位置
  • 下载失败,给出失败的原因
  • 可以暂停下载,下次开始下载,接着上次的进度继续下载

2、原理介绍

a、服务器支持断点续传功能

如果使用断点续传,不仅仅是客户端的工作,还需要服务器支持断点续传功能,否则无法生成正确的resumeData。因为要实现断点续传的功能,通常都需要客户端记录下当前的下载进度,并在需要续传的时候通知服务端本次需要下载的内容片段。如何验证服务器是否支持断点续传呢?可以通过下载文件的时候的响应头来查看,只有满足以下条件,才能恢复下载。

  • 第一次请求资源以来,资源没有变化
  • 该任务是HTTPHTTPS GET请求,必须是get请求
  • 服务器在其响应头中提供Last-Modified
  • 服务器支持字节范围请求,响应头包含accept-range :Bytes
  • 系统尚未删除临时文件以响应磁盘空间压力,即本地缓存文件仍然存在

b、一个最简单的断点续传实现
  • 客户端下载一个1024K的文件,已经下载了其中512K
  • 网络中断,客户端请求续传,因此需要在HTTP头中申明本次需要续传的片段:Range:bytes=512000-
  • 这个头通知服务端从文件的512K位置开始传输文件
  • 服务端收到断点续传请求,从文件的512K位置开始传输,并且在HTTP头中增加:Content-Range:bytes 512000-/1024000
  • 并且此时服务端返回的HTTP状态码应该是206,而不是200或者其他状态码,否则客户端会从头下载

3、运行效果

点击开始下载,输出结果
2020-07-28 09:36:54.961071+0800 Demo[83999:23608303] 处于-[NetworkServerDownLoadTool init]方法中,self.manager.session = <__NSURLBackgroundSession: 0x7fa2ac6104e0> 支持后台下载/上传
2020-07-28 09:36:54.961199+0800 Demo[83999:23608303] 之前下载好的部分数据长度为 0
2020-07-28 09:36:54.961275+0800 Demo[83999:23608303] 开辟新任务
2020-07-28 09:36:55.237086+0800 Demo[83999:23608922] 下载进度 0.000018
2020-07-28 09:36:55.237111+0800 Demo[83999:23608303] 当前的进度 = 0.000018
...
2020-07-28 09:36:58.188459+0800 Demo[83999:23608919] 下载进度 0.008947
2020-07-28 09:36:58.188491+0800 Demo[83999:23608303] 当前的进度 = 0.008947
点击暂停,输出结果
2020-07-28 09:36:58.453978+0800 Demo[83999:23608919] 已经下载好的data = {length = 5286, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 00001404 }
2020-07-28 09:36:58.458186+0800 Demo[83999:23608303] downloadTaskWithRequest的completionHandler得到回调
2020-07-28 09:36:58.458200+0800 Demo[83999:23608917] manager的setTaskDidCompleteBlock:downLoadTask的成功 失败 中断(Home键、闪退、崩溃)等都会回调该代码块,该Task为:BackgroundDownloadTask .<1>
2020-07-28 09:36:58.458366+0800 Demo[83999:23608303] 下载尚未完成,下载的data被downLoad工具暂存了起来,下次可以继续下载
tmp文件

AF在下载的时候(其实是调用的系统的下载方法),会将文件先下载到沙盒目录下的temp文件夹中,生成一个后缀为tmp的文件。

断点续传
resumeData的内容



    YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICV8QG05TS2V5ZWRBcmNoaXZlUm9vdE9iamVjdEtleYABrxAUCwwjJCUmJygpKissMDg7PD0+P0BVJG51bGzTDQ4PEBkiV05TLmtleXNaTlMub2JqZWN0c1YkY2xhc3OoERITFBUWFxiAAoADgASABYAGgAeACIAJqBobHB0eHyAhgAqAC4ANgA6AD4AQgBGAEoATXxAbTlNVUkxTZXNzaW9uUmVzdW1lQnl0ZVJhbmdlXxAgTlNVUkxTZXNzaW9uUmVzdW1lQ3VycmVudFJlcXVlc3RfECFOU1VSTFNlc3Npb25SZXN1bWVPcmlnaW5hbFJlcXVlc3RfEBdOU1VSTFNlc3Npb25Eb3dubG9hZFVSTF8QIk5TVVJMU2Vzc2lvblJlc3VtZUluZm9UZW1wRmlsZU5hbWVfEB9OU1VSTFNlc3Npb25SZXN1bWVCeXRlc1JlY2VpdmVkXxAdTlNVUkxTZXNzaW9uUmVzdW1lSW5mb1ZlcnNpb25fECROU1VSTFNlc3Npb25SZXN1bWVTZXJ2ZXJEb3dubG9hZERhdGVYMzYzOTU1Mi3SLQ8uL1dOUy5kYXRhTxELUmJwbGlzdDAw1AECAwQFBgcKWCR2ZXJzaW9uWSRhcmNoaXZlclQkdG9wWCRvYmplY3RzEgABhqBfEA9OU0tleWVkQXJjaGl2ZXLRCAlfEBtOU0tleWVkQXJjaGl2ZVJvb3RPYmplY3RLZXmAAa8QHwsMQFVbXGJjZGU9ZkFnaHx9fn+AgYKDhIWGh4iJio9VJG51bGzfECUNDg8QERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0MzMzODk6MzM5PT4/QDlBQj1DQEVGM0U+QEtAS04+UEIzU18QIF9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzE4XxAgX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfMjBfECBfX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8xOV8QIF9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzIxXxAfX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfNF8QGGJvdW5kSW50ZXJmYWNlSWRlbnRpZmllcl8QH19fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzZfEB5hbGxvd3NDb25zdHJhaW5lZE5ldHdvcmtBY2Nlc3NfEB9fX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8xXxAfX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfOF8QGl9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3BzXxAUYWxsb3dlZFByb3RvY29sVHlwZXNfEBpwYXlsb2FkVHJhbnNtaXNzaW9uVGltZW91dF8QH19fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzNWJGNsYXNzXxAecmVxdWlyZXNTaG9ydENvbm5lY3Rpb25UaW1lb3V0XxAcYWxsb3dzRXhwZW5zaXZlTmV0d29ya0FjY2Vzc1IkMF8QH19fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzVfEBBzdGFydFRpbWVvdXRUaW1lUiQxXxAhc2NoZW1lV2FzVXBncmFkZWREdWVUb0R5bmFtaWNIU1RTXxAfX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfMFIkMl8QH19fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzdfECBfX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8xMF8QIF9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzExWmlnbm9yZUhTVFNfECBfX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8xMl8QEnByZXZlbnRIU1RTU3RvcmFnZV8QIF9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzEzXxAfX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfMl8QIF9fbnN1cmxyZXF1ZXN0X3Byb3RvX3Byb3Bfb2JqXzE0XxAgX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfMTVfEB9fX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial85XxAgX19uc3VybHJlcXVlc3RfcHJvdG9fcHJvcF9vYmpfMTZfECBfX25zdXJscmVxdWVzdF9wcm90b19wcm9wX29ial8xN4ANgACADoAAgACAAIAJEACAA4AAgAAjAAAAAAAAAACAB4AeCBACgAgQCQiAAhAWgACAAoAHCIAKCIAKgAaAB4ALgAiAAIAMCNNWG1czWVpXTlMuYmFzZVtOUy5yZWxhdGl2ZYAAgAWABF8Qjmh0dHBzOi8vd3d3LmFwcGxlLmNvbS8xMDUvbWVkaWEvY24vaXBob25lLXgvMjAxNy8wMWRmNWI0My0yOGU0LTQ4NDgtYmYyMC00OTBjMzRhOTI2YTcvZmlsbXMvZmVhdHVyZS9pcGhvbmUteC1mZWF0dXJlLWNuLTIwMTcwOTEyXzEyODB4NzIwaC5tcDTSXV5fYFokY2xhc3NuYW1lWCRjbGFzc2VzVU5TVVJMol9hWE5TT2JqZWN0I0BOAAAAAAAAEAAJECQT//////////9TR0VU02lqG2tze1dOUy5rZXlzWk5TLm9iamVjdHOnbG1ub3BxcoAPgBCAEYASgBOAFIAVp3R1dnd4eXqAFoAXgBiAGYAagBuAHIAdVkFjY2VwdFhfX2hoYWFfX1VSYW5nZVhJZi1SYW5nZVpVc2VyLUFnZW50XxAPQWNjZXB0LUxhbmd1YWdlXxAPQWNjZXB0LUVuY29kaW5nUyovKl8RAeQNCg0KWW5Cc2FYTjBNRERXQVFJREJBVUdCd2tMRFE4UldsVnpaWEl0UVdkbGJuUlZVbUZ1WjJWV1FXTmpaWEIwWHhBUFFXTmpaWEIwTFV4aGJtZDFZV2RsV0VsbUxWSmhibWRsWHhBUFFXTmpaWEIwTFVWdVkyOWthVzVub1FoZkVHc2xSVFVsT1RBbE9EZ2xSVFVsUWprbFFqWWxSVFFsUWpnbFFUUWxSVFFsUWpnbFFVRWxSVFlsT1VNbE9Ea2xSVFVsUWtFbE9FWWxSVGtsT1RNbFFrVWxSVGdsUVRFbFFUZ3ZNU0JEUms1bGRIZHZjbXN2TVRFeU9DNHdMakVnUkdGeWQybHVMekU1TGpVdU1LRUtYbUo1ZEdWelBUTTJNemsxTlRJdG9ReFRLaThxb1E1VlpXNHRkWE9oRUY4UUhWUjFaU3dnTVRJZ1UyVndJREl3TVRjZ01ESTZNakk2TXpNZ1IwMVVvUkpmRUJGbmVtbHdMQ0JrWldac1lYUmxMQ0JpY2dBSUFCVUFJQUFtQUMwQVB3QklBRm9BWEFES0FNd0Eyd0RkQU9FQTR3RHBBT3NCQ3dFTkFBQUFBQUFBQWdFQUFBQUFBQUFBRXdBQUFBQUFBQUFBQUFBQUFBQUFBU0U9XmJ5dGVzPTM2Mzk1NTItXxAdVHVlLCAxMiBTZXAgMjAxNyAwMjoyMjozMyBHTVRfEGslRTUlOTAlODglRTUlQjklQjYlRTQlQjglQTQlRTQlQjglQUElRTYlOUMlODklRTUlQkElOEYlRTklOTMlQkUlRTglQTElQTgvMSBDRk5ldHdvcmsvMTEyOC4wLjEgRGFyd2luLzE5LjUuMFVlbi11c18QEWd6aXAsIGRlZmxhdGUsIGJy0l1ei4xfEBNOU011dGFibGVEaWN0aW9uYXJ5o42OYV8QE05TTXV0YWJsZURpY3Rpb25hcnlcTlNEaWN0aW9uYXJ50l1ekJFfEBNOU011dGFibGVVUkxSZXF1ZXN0o5KTYV8QE05TTXV0YWJsZVVSTFJlcXVlc3RcTlNVUkxSZXF1ZXN0AAgAEQAaACQAKQAyADcASQBMAGoAbACOAJQA4QEEAScBSgFtAY8BqgHMAe0CDwIxAk4CZQKCAqQCqwLMAusC7gMQAyMDJgNKA2wDbwORA7QD1wPiBAUEGgQ9BF8EggSlBMcE6gUNBQ8FEQUTBRUFFwUZBRsFHQUfBSEFIwUsBS4FMAUxBTMFNQU3BTgFOgU8BT4FQAVCBUMFRQVGBUgFSgVMBU4FUAVSBVQFVQVcBWQFcAVyBXQFdgYHBgwGFwYgBiYGKQYyBjsGPQY+BkAGSQZNBlQGXAZnBm8GcQZzBnUGdwZ5BnsGfQaFBocGiQaLBo0GjwaRBpMGlQacBqUGqwa0Br8G0QbjBucIzwjeCP4JbAlyCYYJiwmhCaUJuwnICc0J4wnnCf0AAAAAAAACAQAAAAAAAACUAAAAAAAAAAAAAAAAAAAKCoAM0jEyMzRaJGNsYXNzbmFtZVgkY2xhc3Nlc11OU011dGFibGVEYXRhozU2N11OU011dGFibGVEYXRhVk5TRGF0YVhOU09iamVjdNItDzkvTxCgYnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICV8QG05TS2V5ZWRBcmNoaXZlUm9vdE9iamVjdEtleYAAoQtVJG51bGwIERokKTI3SUxqbG4AAAAAAAABAQAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAdIAMXxCOaHR0cHM6Ly93d3cuYXBwbGUuY29tLzEwNS9tZWRpYS9jbi9pcGhvbmUteC8yMDE3LzAxZGY1YjQzLTI4ZTQtNDg0OC1iZjIwLTQ5MGMzNGE5MjZhNy9maWxtcy9mZWF0dXJlL2lwaG9uZS14LWZlYXR1cmUtY24tMjAxNzA5MTJfMTI4MHg3MjBoLm1wNF8QHENGTmV0d29ya0Rvd25sb2FkX1J6aEVNVC50bXASAEBEixAFXxAdVHVlLCAxMiBTZXAgMjAxNyAwMjoyMjozMyBHTVTSMTJBQl8QE05TTXV0YWJsZURpY3Rpb25hcnmjQ0Q3XxATTlNNdXRhYmxlRGljdGlvbmFyeVxOU0RpY3Rpb25hcnkACAARABoAJAApADIANwBJAEwAagBsAIMAiQCQAJgAowCqALMAtQC3ALkAuwC9AL8AwQDDAMwAzgDQANIA1ADWANgA2gDcAN4A/AEfAUMBXQGCAaQBxAHrAfQB+QIBDVcNWQ1eDWkNcg2ADYQNkg2ZDaINpw5KDkwO3Q78DwEPAw8jDygPPg9CD1gAAAAAAAACAQAAAAAAAABFAAAAAAAAAAAAAAAAAAAPZQ==


可以看到,虽然resumeData是一个data类型,但是本质是一个xml文件,其中包括下载的url,当前接受的文件大小,第几次下载,还有临时文件名等信息。

点击继续下载,输出结果
2020-07-28 09:41:26.595378+0800 Demo[83999:23608303] 之前下载好的部分数据长度为 5286
2020-07-28 09:41:26.595550+0800 Demo[83999:23608303] 继续旧任务
2020-07-28 09:41:26.710125+0800 Demo[83999:23608921] 下载进度 0.008947
2020-07-28 09:41:26.710158+0800 Demo[83999:23608303] 当前的进度 = 0.008947
...
回到Home,停一段时间后再回到APP,输出结果
2020-07-28 09:41:57.916591+0800 Demo[82972:23456508] 下载进度 0.251849
2020-07-28 09:41:57.916628+0800 Demo[82972:23440779] 当前的进度 = 0.251849
2020-07-28 09:41:12.191663+0800 Demo[82972:23456509] 下载进度 0.401489
2020-07-28 09:41:12.191682+0800 Demo[82972:23440779] 当前的进度 = 0.401489

可以看到直接从0.2跳到了0.4,说明可以继续之前的进行下载。同理,回到上个视图再进入,或者APP突然闪退、崩溃等也可以继续。但是如果把APP删除了则会把tmp文件删除,一切从头开始。

点击暂停所有下载任务后的输出结果
2020-07-28 09:51:16.688357+0800 Demo[83999:23617896] 下载进度 0.199756
2020-07-28 09:51:16.688377+0800 Demo[83999:23608303] 当前的进度 = 0.199756
2020-07-28 09:51:16.804138+0800 Demo[83999:23608303] 暂停所有下载任务,能够获取到每个任务的resumeData
2020-07-28 09:51:16.809438+0800 Demo[83999:23612187] 可以主动去将task提前结束来触发通知或者完成的回调,一般不允许蜂窝下载的时候,会用到这个方法
2020-07-28 09:51:16.810733+0800 Demo[83999:23608303] AFDownLoadFileWithUrl的completionHandler:downLoadTask的成功 失败 中断(Home键、闪退、崩溃)等都会回调该代码块
2020-07-28 09:51:16.810744+0800 Demo[83999:23612187] manager的setTaskDidCompleteBlock:downLoadTask的成功 失败 中断(Home键、闪退、崩溃)等都会回调该代码块,该Task为:BackgroundDownloadTask <813BFDFE-A94F-40B9-AC82-22314BB896B6>.<3>
2020-07-28 09:51:16.810850+0800 Demo[83999:23608303] 下载尚未完成,下载的data被downLoad工具暂存了起来,下次可以继续下载
点击继续,直到下载完成,输出结果
2020-07-28 09:58:51.482124+0800 Demo[83999:23620038] 下载进度 0.999901
2020-07-28 09:58:51.482203+0800 Demo[83999:23608303] 当前的进度 = 0.999901
2020-07-28 09:58:51.520497+0800 Demo[83999:23620047] 下载进度 1.000000
2020-07-28 09:58:51.520564+0800 Demo[83999:23608303] 当前的进度 = 1.000000
2020-07-28 09:58:51.523004+0800 Demo[83999:23620047] manager的setTaskDidCompleteBlock:downLoadTask的成功 失败 中断(Home键、闪退、崩溃)等都会回调该代码块,该Task为:BackgroundDownloadTask <4A717F1B-E655-4FAD-9F4F-39009B43CED5>.<4>
2020-07-28 09:58:51.523157+0800 Demo[83999:23620047] 下载全部完成才会进入这里
2020-07-28 09:58:51.523347+0800 Demo[83999:23608303] AFDownLoadFileWithUrl的completionHandler:downLoadTask的成功 失败 中断(Home键、闪退、崩溃)等都会回调该代码块
2020-07-28 09:58:51.523914+0800 Demo[83999:23608303] 下载成功 下载的文档路径是 file:///Users/xiejiapei/Library/Developer/CoreSimulator/Devices/4B1F0517-19D5-45E0-A0F4-7E4B55191EE0/data/Containers/Data/Application/59BBB0D2-AE9A-4666-98A2-36D815FDBA75/Documents/iphoneX.mp4, 

下载完的时候,系统会将tmp文件删除掉释放占用的内存,然后移动文件到我们下载时设置的路径下,这样下载任务就完成了。

下载完成

4、创建键值对 url:resumeData 存储下载进度

a、下载进度问题
问题

在控制器中点击开始下载可以获取当前下载进度,但是当我退出这个页面再次进入的时候,如何自动获取最新的下载进度?

解答

将下载功能交给一个单例管理类管理,下载的功能由它控制。进入下载页面的时候,读取他的信息,然后显示UI。存储最新数据的方式是把这些任务task放到容器(数组或者字典)里面存起来,等进来的时候,再来取了。


b、获取到网络请求单例对象
static NetworkServerDownLoadTool* tool = nil;
+ (instancetype)sharedTool
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        tool =  [[self alloc] init];
    });
    return tool;
}

c、将下载进度值放进字典中

当我们需要下载的时候,主动去调用downloadTaskWithRequest方法,根据下载地址url,去判断本地是否有resumeData,这个类似SDWebImage的实现,我们自己去维护这个对应关系,可以存到本地一个plist文件里面,键值对对应url:resumeData。于是写一个方法将keyresumeData存到本地,用作下载续传时候用到的值。

- (void)saveHistoryWithKey:(NSString *)key DownloadTaskResumeData:(NSData *)data
{
    ...
}

开始下载的时候,去判断当前是否存在这个url:resumeData,存在的话看一下长度是否大于零。当用户开始下载的时候,如果什么都没下载到,就中断下载,此时resumeDatanil,如果直接写入plist是会崩溃的,可以写一个NSString对象,虽然不是一个Data类型,但是length都可以使用,不会有太大影响。

// 开始下载的时候,去判断当前是否存在这个url:resumeData
if (!data)
{
    NSString *emptyData = [NSString stringWithFormat:@""];
    [self.downLoadHistoryDictionary setObject:emptyData forKey:key];
}

有数据就直接写入即可。

else
{
    [self.downLoadHistoryDictionary setObject:data forKey:key];
}

再将下载好的部分存储到本地。

[self.downLoadHistoryDictionary writeToFile:self.fileHistoryPath atomically:NO];
NSLog(@"文件路径%@",self.fileHistoryPath);

5、控制下载流程

点击开始/继续下载
- (void)startNew
{
    // 正处于下载之中则直接返回
    if (self.isDownLoading)
    {
        return;
    }
    
    self.isDownLoading = YES;
    // 根据封装的断点续传工具类创建下载任务
    NSURLSessionDownloadTask *tempTask = [[NetworkServerDownLoadTool sharedTool] AFDownLoadFileWithUrl:self.downLoadUrl progress:^(CGFloat progress) {
        // 回到主线程刷新显示进度
        dispatch_async(dispatch_get_main_queue(), ^{
            self.progressView.progress = progress;
            NSLog(@"当前的进度 = %f",progress);
            self.progressLabel.text = [NSString stringWithFormat:@"进度:%.3f",progress];
        });
    } fileLocalUrl:self.fileUrl success:^(NSURL *fileUrlPath, NSURLResponse *response) {
        NSLog(@"下载成功 下载的文档路径是 %@, ",fileUrlPath);
    } failure:^(NSError *error, NSInteger statusCode) {
        NSLog(@"下载尚未完成,下载的data被downLoad工具暂存了起来,下次可以继续下载");
    }];
    self.downloadTask = tempTask;
}
暂停下载任务
- (void)pause
{
    // 可以在这里存储resumeData ,也可以去NetworkServerDownLoadTool里面,根据那个通知去处理,都会回调
    if (self.isDownLoading)
    {
        [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
            NSLog(@"已经下载好的data = %@", resumeData);
        }];
    }
    self.isDownLoading = NO;
}
取消下载任务
- (void)cancel
{
    if (self.isDownLoading)
    {
        NSLog(@"暂停所有下载任务,能够获取到每个任务的resumeData");
        [[NetworkServerDownLoadTool sharedTool] stopAllDownLoadTasks];
    }
    self.isDownLoading = NO;
}
停止所有任务

可以主动去将task提前结束来触发通知或者完成的回调,一般不允许蜂窝下载的时候,会用到这个方法。

- (void)stopAllDownLoadTasks
{
    if ([[self.manager downloadTasks] count] == 0)
    {
        return;
    }
    
    //停止所有的下载
    for (NSURLSessionDownloadTask *task in  [self.manager downloadTasks])
    {
        // 处于允许状态中的下载任务
        if (task.state == NSURLSessionTaskStateRunning)
        {
            // 取消掉
            [task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
                NSLog(@"可以主动去将task提前结束来触发通知或者完成的回调,一般不允许蜂窝下载的时候,会用到这个方法");
            }];
        }
    }
}

6、创建文件下载任务

准备工作
typedef void (^DonwLoadSuccessBlock)(NSURL* fileUrlPath ,NSURLResponse* response);
typedef void (^DownLoadfailBlock)(NSError* error ,NSInteger statusCode);
typedef void (^DowningProgress)(CGFloat progress);

AFURLSessionManager开始一个下载的时候,有两个方法,分别是开始下载(重头下载),继续下载(需要传入一个resumeData)。

- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
                                             progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
                                          destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                    completionHandler:(nullable void (^)(NSURLResponse *response, NSURL * _Nullable filePath, NSError * _Nullable error))completionHandler;
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData
                                                progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
                                             destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                       completionHandler:(nullable void (^)(NSURLResponse *response, NSURL * _Nullable filePath, NSError * _Nullable error))completionHandler;
正文内容
  • urlHost:下载地址
  • progress:下载进度
  • localUrl:本地存储路径
  • success:下载成功
  • failure:下载失败
- (NSURLSessionDownloadTask *)AFDownLoadFileWithUrl:(NSString *)urlHost
                                           progress:(DowningProgress)progress
                                       fileLocalUrl:(NSURL *)localUrl
                                            success:(DonwLoadSuccessBlock)success
                                            failure:(DownLoadfailBlock)failure
{
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlHost]];
    NSURLSessionDownloadTask *downloadTask = nil;
    ...
    // 启动下载任务
    [downloadTask resume];
    return downloadTask;
}

❶ 取出之前下载好的部分

NSData *downLoadHistoryData = [self.downLoadHistoryDictionary objectForKey:urlHost];
NSLog(@"之前下载好的部分数据长度为 %ld",downLoadHistoryData.length);

❷ 其length大于零就使用resumeData调用downloadTaskWithResumeData这个方法,去继续我们的下载任务

if (downLoadHistoryData.length > 0)
{
    NSLog(@"继续旧任务");
    downloadTask = [self.manager downloadTaskWithResumeData:downLoadHistoryData progress:^(NSProgress * _Nonnull downloadProgress) {
        // 调用下载进度block
        if (progress)
        {
            progress(1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount);
            NSLog(@"下载进度 %F",(1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount));
        }
    } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
        // 返回本地存储路径
        return localUrl;
    } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
        ...
    }];
}

downLoadTask的成功、失败、中断(Home键、闪退、崩溃)等都会回调该代码块:

completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {

    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
    
    // 特殊处理404,删除垃圾文件
    if (httpResponse.statusCode == 404)
    {
        [[NSFileManager defaultManager] removeItemAtURL:filePath error:nil];
    }
    
    if (error)
    {
        // 调用下载失败block
        if (failure)
        {
            //将下载失败存储起来,提交到下面的的网络监管类里面
            failure(error, httpResponse.statusCode);
        }
    }
    else
    {
        // 调用下载成功block
        if (success)
        {
            //将下载成功存储起来,提交到下面的的网络监管类里面
            success(filePath, response);
        }
    }
}];

否则调用downloadTaskWithRequest去开辟新任务:

else
{
    NSLog(@"开辟新任务");
    downloadTask = [self.manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
        同上
    } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
        同上
    } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
        同上
    }];
}

6、后台下载

- (instancetype)init
{
    self = [super init];
    if (self)
    {
        ...
    }
    return self;
}
a、原理介绍

初始化manger的时候,配置那个后台配置的时候已经实现了后台下载的过程。你可以尝试下来到后台,发现你的下载还在继续,只是这个时候,你的session被操作系统接过去了,由操作系统去管理了。一旦你下载完成,就会通知你(如果你的app还活着的话)。那么你的app死了的时候怎么办呢?操作系统会帮我们持有这个session,然后把ResumeData存着,等你的app再次复活的时候,会再次创建NSURLSessionConfiguration,此时有后台任务并且后台的backgroundSessionConfigurationWithIdentifier一致的话,操作系统会去调用你的原来的NSURLSessionTaskDelegate代理中的taskdidCompleteWithError的方法。

由于我们的使用的是AF封装的,所以那个代理被AF接到并且提供了回调的Block,同时也通过通知处理了,所以还是会在我们之前监听的那个通知里面,和我们app活着的时候一样去发送那个AFNetworkingTaskDidCompleteNotification的通知,那么我们原来的处理逻辑并不需要改变。

上面讲的是我们进入后台,app慢慢的被系统杀死或者没杀死的时候。那么,如果我正在下载着app崩溃,或者被用户手动强退怎么办? 经过验证其实是一样的,出现这个情况后,操作系统监听到被杀死或者被中断,也会帮我们持有这个session,然后把崩溃的一瞬间的ResumeData存着,等待我们再次创建相同后缀的session,那时候在调用回调---->AF再发通知---->我们再存起来。


b、创建SessionManager对象

SessionConfiguration对象允许HTTPHTTPS在后台进行下载或者上传。如果是做下载工具或者使用的默认的defaultConfig的话,iOS默认限制同一个服务器tcp连接的并发数量限制,所以你会发现 ,无论创建多少下载任务,都是4个或者6个任务在运行,其他的任务在排队,甚至最后有的任务还直接超时了。默认配置下,iOS对于同一个IP服务器的并发最大为4,如果用户设置了最大并发数,则按照用户设置的最大并发数执行(我设置最大20,最小为1,均可以执行)。

configuration.HTTPMaximumConnectionsPerHost = 8;

// 设置请求超时为10秒钟
configuration.timeoutIntervalForRequest = 10;

// 在蜂窝网络情况下是否继续请求(上传或下载)
configuration.allowsCellularAccess = NO;

// 创建SessionManager对象
self.manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];

c、设置请求任务完成时的回调
[self.manager setTaskDidCompleteBlock:^(NSURLSession * _Nonnull session, NSURLSessionTask * _Nonnull task, NSError * _Nullable error) {
    NSLog(@"manager的setTaskDidCompleteBlock:downLoadTask的成功 失败 中断(Home键、闪退、崩溃)等都会回调该代码块,该Task为:%@",task);
    ...
}];

判断是否有错误,有错误说明下载没有完成就被中断了

if (error)
{
    if (error.code == -1001)
    {
        NSLog(@"下载出错,请查看一下网络是否正常");
    }
    
    // 可以通过key:NSURLSessionDownloadTaskResumeData去获取续传时候的data,因为AF源码中将其存储进入了这个key中
    NSData *resumeData = [error.userInfo objectForKey:@"NSURLSessionDownloadTaskResumeData"];
    
    // 当我退出这个页面再次进入的时候,如何自动获取最新的下载进度?
    // 将urlHost和resumeData存到本地,用作下载续传时候用到的值
    [weakSelf saveHistoryWithKey:urlHost DownloadTaskResumeData:resumeData];
}

判断当前是否存在这个urlHost:resumeData,存在的话看一下长度是否大于零

// 下载地址
NSString *urlHost = [task.currentRequest.URL absoluteString];

else
{
    NSLog(@"下载全部完成才会进入这里");
    if ([weakSelf.downLoadHistoryDictionary valueForKey:urlHost])
    {
        // 下载完成移除存储容器中的内容
        [weakSelf.downLoadHistoryDictionary removeObjectForKey:urlHost];
        // AF在下载的时候(其实是调用的系统的下载方法),会将文件先下载到沙盒目录下的temp文件夹中,生成一个后缀为tmp的文件
        // 下载完的时候,系统会将tmp文件删除掉释放占用的内存,然后移动文件到我们下载时设置的路径下,这样下载任务就完成了
        [weakSelf.downLoadHistoryDictionary writeToFile:weakSelf.fileHistoryPath atomically:YES];
    }
}

d、文件本地存储路径
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
self.fileHistoryPath = [path stringByAppendingPathComponent:@"fileDownLoadHistory.plist"];
if ([[NSFileManager defaultManager] fileExistsAtPath:self.fileHistoryPath])// 文件已经存在
{
    // 用之前存在的文件来初始化downLoadHistoryDictionary,即是之前的下载历史记录
    self.downLoadHistoryDictionary = [NSMutableDictionary dictionaryWithContentsOfFile:self.fileHistoryPath];
}
else
{
    // 新创建downLoadHistoryDictionary
    self.downLoadHistoryDictionary = [NSMutableDictionary dictionary];
    // 将dictionary中的数据写入plist文件中,此时也新建了plist文件
    [self.downLoadHistoryDictionary writeToFile:self.fileHistoryPath atomically:YES];
}
NSLog(@"处于%s方法中,self.manager.session = %@ 支持后台下载/上传",__func__,self.manager.session);

7、并发属性的使用场景

a、HTTPMaximumConnectionsPerHost的值

如果我们在一个网络请求并发很多的app内,共用一个session,且未设置最大并发数的时候,尤其是包含一些请求响应时间不给力的请求,可能会影响我们的其他的网络请求,这个时候可以通过设置最大并发数来增加并发数(讨论的是一个服务器域名的 ),就是迅雷或者腾讯视频的多任务下载。

默认配置(defaultConfig)下,iOS对于同一个IP服务器的并发最大为4,即限制同一个服务器tcp连接的并发数量限制。如果用户设置了最大并发数,则按照用户设置的最大并发数执行(我设置最大20,最小为1,均可以执行)。否则会发现 ,无论创建多少下载任务,都是4个在运行,其他的在排队,甚至有的任务最后还直接超时了。


b、尝试创建多个NSURLSession来增加并发

问题:对于我们在高并发的请求,期望快速做出反应的时候,除了设置HTTPMaximumConnectionsPerHost的值为一个我们能够接受的比较大的值外,是否也可以创建2个甚至多个NSURLSession来增加并发呢? 为此进行了一个验证。基于AFNetworking创建了2个manager,分别对应不同的config,通过打印mangersession实例,发现的确不是一个session

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];

输出结果为:

2020-07-28 13:46:42.213683+0800 Demo[84680:23730915] 处于-[NetworkServerDownLoadTool init]方法中,self.manager.session = <__NSURLSessionLocal: 0x7ff51de08580>  
2020-07-28 13:46:43.209552+0800 Demo[84680:23730915] 处于-[TestNetworkServerDownLoadTool init]方法中,self.manager.session = <__NSURLSessionLocal: 0x7ff51de04e40> 

ephemeralSessionConfigurationdefaultSessionConfiguration混合,并且使用默认值的时候,并发数量却没有按照我们想象的,各自持有一个并发数,而是按照顺序创建task,直到满足6个默认值,就停止了。可见,无论创建多少个NSURLSessionsession之间共用的都是一个最大并发的配置,最后一次的配置为所有session使用的最终的配置。

2020-07-28 13:54:21.838852+0800 DownLoadTest[84708:23735491] 增加一个并发 taskIdentifier = 1 总数 1 -[ShowViewController downLoadWithTask:FileUrl:]_block_invoke_2
2020-07-28 13:54:38.780335+0800 DownLoadTest[84708:23735491] 增加一个并发 taskIdentifier = 2 总数 2 -[ShowViewController downLoadWithTask:FileUrl:]_block_invoke_2
2020-07-28 13:54:39.260323+0800 DownLoadTest[84708:23735491] 增加一个并发 taskIdentifier = 3 总数 3 -[ShowViewController downLoadWithTask:FileUrl:]_block_invoke_2
2020-07-28 13:54:40.412324+0800 DownLoadTest[84708:23735491] 增加一个并发 taskIdentifier = 4 总数 4 -[ShowViewController downLoadWithTask:FileUrl:]_block_invoke_2
2020-07-28 13:54:40.844415+0800 DownLoadTest[84708:23735491] 增加一个并发 taskIdentifier = 5 总数 5 -[ShowViewController downLoadWithTask:FileUrl:]_block_invoke_2
2020-07-28 13:54:44.515921+0800 DownLoadTest[84708:23735491] 增加一个并发 taskIdentifier = 6 总数 6 -[ShowViewController downLoadWithTask:FileUrl:]_block_invoke_2

那么创建2个甚至多个NSURLSession是否还有什么意义呢?官方解释说创建多个Session的目的,不是为了增加并发,而是为了对不同的Task使用不同的策略来实现更符合我们需求的交互。


Demo

Demo在我的Github上,欢迎下载。
BasicsDemo

你可能感兴趣的:(IOS基础网络:断点续传和后台下载)