iOS 大文件下载、断点续传、后台下载 —— HERO博客

本篇简述一下实现文件下载功能,包含大文件下载,后台下载,杀死进程,重新启动时继续下载,设置下载并发数,监听网络改变等,并在最后附有Demo。


下载功能的实现:

使用的网络连接的类为NSURLSession。该类用以替代NSURLConnection,在iOS7时推出,至此iOS系统才有了后台传输。在初始化NSURLSession前,需要先创建NSURLSessionConfiguration,可以理解为是NSURLSession需要的一个配置。NSURLSessionConfiguration有三种模式:

1. default:可以使用缓存的Cache、Cookie、鉴权。

2. ephemeral,仅内存缓存,不使用缓存的Cache、Cookie、鉴权。

3. background,支持后台传输,需要一个identifier标识,用来重新连接session对象。

创建后台模式NSURLSessionConfiguration:

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"HWDownloadBackgroundSessionIdentifier"];

创建NSURLSession,设置配信息、代理、代理线程:

NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[[NSOperationQueue alloc] init]];

在实现下载前,还需要了解一个很重要的类,NSURLSessionTask,无论下载多少文件,我们只需要初始化一个NSURLSession即可,而每个task对应一个任务,需要通过task才能实现下载,NSURLSessionTask是一个基类,有四个子类:

1. NSURLSessionDataTask:下载时,内容以NSData对象返回,需要我们不断写入文件,但不支持后台传输,切换后台会终止下载,回到前台时在协议方法中输出error,下面贴一下用NSURLSessionDataTask实现断点续传的核心代码:

// 遵守协议

// 创建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];

// 接收到服务器响应
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    // 更新文件的总大小
    totalFileSize = response.expectedContentLength + tmpFileSize;
    
    // 创建输出流
    NSOutputStream *stream = [[NSOutputStream alloc] initToFileAtPath:fullPath append:YES];
    [stream open];
    
    // 允许处理服务器的响应,继续接收数据
    completionHandler(NSURLSessionResponseAllow);
}

// 接收到服务器返回数据,会被调用多次
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    // 写入数据
    [stream write:data.bytes maxLength:data.length];
    
    // 当前下载大小
    tmpFileSize += data.length;
    
    // 进度
    self.progressView.progress = 1.0 * tmpFileSize / totalFileSize;
}

// 当请求完成之后调用,如果错误,那么error有值
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    [stream close];
}

- (void)dealloc
{
    [session invalidateAndCancel];
}

有几点需要注意,调用cancel方法会立即进入-URLSession: task: didCompleteWithError这个回调;调用suspend方法,即使任务已经暂停,但达到超时时长,也会进入这个回调,可以通过error进行判断;当一个任务调用了resume方法,但还未开始接受数据,这时调用suspend方法是无效的。也可以通过cancel方法实现暂停,只是每次需要重新创建NSURLSessionDataTask。

2. NSURLSessionUploadTask:继承自NSURLSessionDataTask,内容以NSData对象返回,协议方法中可以查看请求时上传内容的过程,支持后台传输。

3. NSURLSessionStreamTask:建立了一个TCP/IP连接,替代NSInputStream/NSOutputStream,新的API可异步读写,自动通过HTTP代理连接远程服务器。

4. NSURLSessionDownloadTask:笔者推荐使用该task实现文件下载,断点续传系统帮我们做了,资源会下载到一个临时文件,下载完成需将文件移动至想要的路径,系统会删除临时路劲文件,暂停时,系统会返回NSData对象,恢复下载时用这个data创建task,支持后台传输,下面重点介绍一下NSURLSessionDownloadTask的使用:

创建NSURLSessionDownloadTask,有两种方式,后面会讲解NSData在哪里获取,其中需要注意一点,在iOS 10.0和iOS 10.1系统中,使用downloadTaskWithResumeData:会发生数据错误问题,需要进行额外处理,具体可以在Demo中查看:

// 根据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;
}];
遵守协议,实现相应协议方法:
NSURLSessionDownloadDelegate:
/**
 接收到服务器返回数据,会被调用多次,可获取文件大小,进度,计算速度等

 @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
{
    // 计算进度
    model.progress = 1.0 * totalBytesWritten / totalBytesExpectedToWrite;
}

// 下载完成
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
    // 移动文件,原路径文件由系统自动删除
    [[NSFileManager defaultManager] moveItemAtPath:[location path] toPath:localPath error:nil];
}
NSURLSessionTaskDelegate,注意调用cancel、cancelByProducingResumeData:方法也会调用:
// 请求完成,有错误时,error有值
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;

后台下载:

到这里,已经可以通过NSURLSessionDownloadTask实现断点续传了,下面介绍如何实现后台下载,其实非常简单,一共三步:
1. 创建NSURLSession时,需要创建后台模式NSURLSessionConfiguration,上面已经介绍过了。
2. 在AppDelegate中实现下面方法,并定义变量保存completionHandler代码块:
// 应用处于后台,所有下载任务完成调用
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler
{
    _backgroundSessionCompletionHandler = completionHandler;
}
3. 在下载类中实现下面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={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,就可以恢复下载。

再说明一下,另外一种不可取的思路,在appDelegate中进程杀死时会调用-applicationWillTerminate:方法,在这里task调用cancelByProducingResumeData:方法暂停正在下载的任务,但是这个方法的回调需要时间,还没有执行到代码块进程就已经终止了。


并发数设置:

下面介绍一下下载并发数的设置:NSURLSession本身就支持多任务同时下载,它会根据性能内部控制同时下载的个数,最多5个。一个任务对应一个NSURLSessionDownloadTask,所以想多任务同时下载,需要创建多个task,可以用数组或字典保存。我们定义变量去记录当前下载文件个数及用户设置的最大下载个数。


监听网络改变:用AFN监听,可以点击这里查看

为了增加用户体验,往往在设置中会给用户一个选项, 选择蜂窝网络下是否允许下载。NSURLSessionConfiguration本身就有一个属性allowsCellularAccess,默认为YES,允许蜂窝网络下载。如果不需要用户随时变更这个选项,是可以用这个属性。但是对于正在下载的任务,修改这个属性是无效的,即我们已经通过session创建了task对象,开启了任务,再试图用session.configuration.allowsCellularAccess = NO;去修改这个选项是无效的。如果一定要用这个属性修改这个选项,那么只能重新创建session:
// 重新创建后台NSURLSessionConfiguration,并且identifier需要改变,不能与之前一样
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"HWDownloadBackgroundSessionIdentifierNew"];
// 修改是否允许蜂窝网络下载
configuration.allowsCellularAccess = NO;
// 重新创建NSURLSession
_session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
所以我们创建NSURLSessionConfiguration时把allowsCellularAccess设为YES,然后定义一个变量去控制是否允许蜂窝网络下载,在网络状态改变及用户设置修改这个选项之后,调用暂停、开启任务。


数据保存:用FMDB存储数据,可以点击这里查看


下载速度计算:

声明两个变量,一个记录时间,一个记录在特定时间内接收到的数据大小,在接收服务器返回数据的-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];
    }
}

Demo效果图:

  iOS 大文件下载、断点续传、后台下载 —— HERO博客_第1张图片

这里在模型中加入了一个变量,记录任务加入准备下载的时间,用于计算任务开始的先后顺序,如上图3,开启任务08、09、10、11、12,暂停,依次开启10、11、12、08、09,然后将最大并发数由5改为2,暂停的应该为12、08、09三个任务,当10下载完成,开启的应该是12而不是08。


Demo下载链接:https://github.com/HeroWqb/HWDownloadDemo


写博客的初心是希望大家共同交流成长,博主水平有限难免有偏颇之处,欢迎批评指正。


你可能感兴趣的:(iOS,Objective-C技术分享,杀死进程继续下载,断点下载,多文件同时下载,NSURLSession,封装下载)