苹果提供的NSURLSessionDownloadTask虽然能实现断点续传,但是有些情况是无法处理的,比如程序强制退出或没有调用
cancelByProducingResumeData取消方法,这时就无法断点续传了。
使用NSURLSession和NSURLSessionDataTask实现断点续传的过程是:
1、配置NSURLRequest对象的Range请求头字段信息
2、创建使用代理的NSURLSession对象
3、创建NSURLSessionDataTask对象,启动任务。
4、在NSURLSessionDataDelegate的didReceiveData方法中追加获取下载数据到目标文件。
直接上代码吧(MQLResumeDataTask类封装了断点续传,具体差看头问价及实现文件)
//
// MQLResumeDataTask.h
//
#import
NS_ASSUME_NONNULL_BEGIN
@interface MQLResumeDataTask : NSObject
/// 断点续传请求
- (void)resumeDataTaskWithURL:(NSString *)URLString
parameters:(id)parameters
savePath:(NSString*)savePath
downloadProgress:(void(^)(NSProgress *downloadProgress)) downloadProgressBlock
completionHandler:(void(^)(NSURLResponse *response, NSError *error))completionHandler;
/// 取消断点续传请求
- (void)cancelResumeDataTask;
@end
NS_ASSUME_NONNULL_END
//
// MQLResumeDataTask.m
//
#import "MQLResumeDataTask.h"
@interface MQLResumeDataTask ()
@property (nonatomic, strong) NSFileManager *fileManager; //用其获取文件大小
@property (nonatomic, strong) NSString *savePath; //文件存放位置
@property (nonatomic, strong) NSURLSession *session; //请求会话
@property (nonatomic, strong) NSOutputStream *outStream; //输出流
@property (nonatomic, strong) NSURLSessionDataTask *dataTask; //下载任务
@property (nonatomic, strong) NSProgress *downloadProgress; //记录下载进度
@property (nonatomic, copy) void (^downloadProgressBlock)(NSProgress*); //下载进度回调
@property (nonatomic, copy) void (^completionHandler)(NSURLResponse*, NSError*);//下载完成回调
@end
@implementation MQLResumeDataTask
- (instancetype)init
{
self = [super init];
if (self) {
_fileManager = [NSFileManager defaultManager];
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
_session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
_downloadProgress = [[NSProgress alloc] initWithParent:nil userInfo:nil];
[_downloadProgress addObserver:self
forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
options:NSKeyValueObservingOptionNew
context:NULL];
}
return self;
}
#pragma mark - NSProgress Tracking
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([object isEqual:self.downloadProgress]) {
if (self.downloadProgressBlock) {
self.downloadProgressBlock(object);
}
}
}
-(void)dealloc{
[self.downloadProgress removeObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted))];
}
/// 断点续传请求
- (void)resumeDataTaskWithURL:(NSString *)URLString
parameters:(id)parameters
savePath:(NSString*)savePath
downloadProgress:(void(^)(NSProgress *downloadProgress)) downloadProgressBlock
completionHandler:(void(^)(NSURLResponse *response, NSError * error))completionHandler {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URLString]];
// 设置请求头for断点续传
NSString *range = [NSString stringWithFormat:@"bytes=%lld-", [self caculateFileSizeWithPath:savePath]];
[request setValue:range forHTTPHeaderField:@"Range"];
//记录保存路径、下载进度回调、完成回调
self.savePath = savePath;
self.downloadProgressBlock = downloadProgressBlock;
self.completionHandler = completionHandler;
//创建任务并启动
self.dataTask = [self.session dataTaskWithRequest:request];
[self.dataTask resume];
}
/// 取消断点续传请求
- (void)cancelResumeDataTask{
if (self.dataTask) {
[self.dataTask cancel];
self.dataTask = nil;
}
}
//获取文件大小
- (int64_t)caculateFileSizeWithPath:(NSString *)filePath {
if (![_fileManager fileExistsAtPath:filePath]) return 0;
return [[_fileManager attributesOfItemAtPath:filePath error:nil] fileSize];
}
#pragma mark -- NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
//error.code == -999 || error.code == -1011) //cancel || timed out
self.completionHandler(task.response, error);
if (self.outStream) {
[self.outStream close];
self.outStream = nil;
}
}
#pragma mark -- NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
//计算起始进度
self.downloadProgress.totalUnitCount = response.expectedContentLength + [self caculateFileSizeWithPath:self.savePath];
self.downloadProgress.completedUnitCount = [self caculateFileSizeWithPath:self.savePath];
//创建输出流并打开
_outStream = [[NSOutputStream alloc] initToFileAtPath:_savePath append:YES];
[_outStream open];
//允许继续请求
NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;
completionHandler(disposition);
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data
{
//追加数据
[_outStream write:data.bytes maxLength:data.length];
//变更已下载数据大小
self.downloadProgress.completedUnitCount = [self caculateFileSizeWithPath:self.savePath];
}
@end
下面是使用上的演示:
#import "MQLResumeDataTask.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
@property (nonatomic, strong) MQLResumeDataTask *task;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *file = [caches stringByAppendingPathComponent:@"test.mp4"];
NSLog(@"%@", file);
_task = [MQLResumeDataTask new];
__weak typeof(self) weakSelf = self;
[_task resumeDataTaskWithURL:@"http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4" parameters:@{} savePath:file downloadProgress:^(NSProgress * _Nonnull downloadProgress) {
NSLog(@"%.2f", downloadProgress.fractionCompleted);
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.progressView.progress = downloadProgress.fractionCompleted;
});
} completionHandler:^(NSURLResponse * _Nonnull response, NSError * _Nullable error) {
NSLog(@"%@", error);
}];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[_task cancelResumeDataTask];
}
经验证,如果app后台能运行,datatask是支持后台传输的。
让您的app成为后台运行app非常简单:
#import "AppDelegate.h"
static UIBackgroundTaskIdentifier bgTask;
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
return YES;
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
[self getBackgroundTask];
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
[self endBackgroundTask];
}
/**
* 获取后台任务
*/
-(void)getBackgroundTask{
NSLog(@"getBackgroundTask");
UIBackgroundTaskIdentifier tempTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
}];
if (bgTask != UIBackgroundTaskInvalid) {
[self endBackgroundTask];
}
bgTask = tempTask;
[self performSelector:@selector(getBackgroundTask) withObject:nil afterDelay:120];
}
/**
* 结束后台任务
*/
-(void)endBackgroundTask{
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
@end