ios断点续传:NSURLSession和NSURLSessionDataTask实现
苹果提供的NSURLSessionDownloadTask虽然能实现断点续传,但是有些情况是无法处理的,比如程序强制退出或没有调用
cancelByProducingResumeData取消方法,这时就无法断点续传了。
使用NSURLSession和NSURLSessionDataTask实现断点续传的过程是:
1、配置NSMutableURLRequest对象的Range请求头字段信息
2、创建使用代理的NSURLSession对象
3、使用NSURLSession对象和NSMutableURLRequest对象创建NSURLSessionDataTask对象,启动任务。
4、在NSURLSessionDataDelegate的didReceiveData方法中追加获取下载数据到目标文件。
下面是具体实现,封装了一个续传管理器。可以直接拷贝到你的工程里,也可以参考我提供的DEMO:http://pan.baidu.com/s/1c0BHToW
// // MQLResumeManager.h // // Created by MQL on 15/10/21. // Copyright © 2015年. All rights reserved. // #import <Foundation/Foundation.h> @interface MQLResumeManager : NSObject /** * 创建断点续传管理对象,启动下载请求 * * @param url 文件资源地址 * @param targetPath 文件存放路径 * @param success 文件下载成功的回调块 * @param failure 文件下载失败的回调块 * @param progress 文件下载进度的回调块 * * @return 断点续传管理对象 * */ +(MQLResumeManager*)resumeManagerWithURL:(NSURL*)url targetPath:(NSString*)targetPath success:(void (^)())success failure:(void (^)(NSError*error))failure progress:(void (^)(longlongtotalReceivedContentLength,longlong totalContentLength))progress; /** * 启动断点续传下载请求 */ -(void)start; /** * 取消断点续传下载请求 */ -(void)cancel; @end
// // MQLResumeManager.m // // Created by MQL on 15/10/21. // Copyright © 2015年. All rights reserved. // #import "MQLResumeManager.h" typedef void (^completionBlock)(); typedef void (^progressBlock)(); @interface MQLResumeManager ()<NSURLSessionDelegate,NSURLSessionTaskDelegate> @property (nonatomic,strong)NSURLSession *session; //注意一个session只能有一个请求任务 @property(nonatomic,readwrite,retain)NSError *error; //请求出错 @property(nonatomic,readwrite,copy)completionBlock completionBlock; @property(nonatomic,readwrite,copy)progressBlock progressBlock; @property (nonatomic,strong)NSURL *url; //文件资源地址 @property (nonatomic,strong)NSString *targetPath; //文件存放路径 @property longlong totalContentLength; //文件总大小 @property longlong totalReceivedContentLength; //已下载大小 /** * 设置成功、失败回调block * * @param success 成功回调block * @param failure 失败回调block */ - (void)setCompletionBlockWithSuccess:(void (^)())success failure:(void (^)(NSError *error))failure; /** * 设置进度回调block * * @param progress */ -(void)setProgressBlockWithProgress:(void (^)(longlong totalReceivedContentLength,longlong totalContentLength))progress; /** * 获取文件大小 * @param path 文件路径 * @return 文件大小 * */ - (long long)fileSizeForPath:(NSString *)path; @end @implementation MQLResumeManager /** * 设置成功、失败回调block * * @param success 成功回调block * @param failure 失败回调block */ - (void)setCompletionBlockWithSuccess:(void (^)())success failure:(void (^)(NSError *error))failure { __weak typeof(self) weakSelf =self; self.completionBlock = ^ { dispatch_async(dispatch_get_main_queue(), ^ { if (weakSelf.error) { if (failure) { failure(weakSelf.error); } } else { if (success) { success(); } } } ); } ; } /** * 设置进度回调block * * @param progress */ -(void)setProgressBlockWithProgress:(void (^)(longlong totalReceivedContentLength,longlong totalContentLength))progress { __weak typeof(self) weakSelf =self; self.progressBlock = ^ { dispatch_async(dispatch_get_main_queue(), ^ { progress(weakSelf.totalReceivedContentLength, weakSelf.totalContentLength); } ); } ; } /** * 获取文件大小 * @param path 文件路径 * @return 文件大小 * */ - (long long)fileSizeForPath:(NSString *)path { long long fileSize =0; NSFileManager *fileManager = [NSFileManagernew]; // not thread safe if ([fileManager fileExistsAtPath:path]) { NSError *error = nil; NSDictionary *fileDict = [fileManager attributesOfItemAtPath:path error:&error]; if (!error && fileDict) { fileSize = [fileDict fileSize]; } } return fileSize; } /** * 创建断点续传管理对象,启动下载请求 * * @param url 文件资源地址 * @param targetPath 文件存放路径 * @param success 文件下载成功的回调块 * @param failure 文件下载失败的回调块 * @param progress 文件下载进度的回调块 * * @return 断点续传管理对象 * */ +(MQLResumeManager*)resumeManagerWithURL:(NSURL*)url targetPath:(NSString*)targetPath success:(void (^)())success failure:(void (^)(NSError *error))failure progress:(void (^)(longlong totalReceivedContentLength,longlong totalContentLength))progress { MQLResumeManager *manager = [[MQLResumeManageralloc]init]; manager.url = url; manager.targetPath = targetPath; [manager setCompletionBlockWithSuccess:successfailure:failure]; [manager setProgressBlockWithProgress:progress]; manager.totalContentLength =0; manager.totalReceivedContentLength =0; return manager; } /** * 启动断点续传下载请求 */ -(void)start { NSMutableURLRequest *request = [[NSMutableURLRequestalloc]initWithURL:self.url]; longlong downloadedBytes =self.totalReceivedContentLength = [selffileSizeForPath:self.targetPath]; if (downloadedBytes > 0) { NSString *requestRange = [NSStringstringWithFormat:@"bytes=%llu-", downloadedBytes]; [request setValue:requestRange forHTTPHeaderField:@"Range"]; } else { int fileDescriptor = open([self.targetPathUTF8String],O_CREAT |O_EXCL |O_RDWR,0666); if (fileDescriptor > 0) { close(fileDescriptor); } } NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfigurationdefaultSessionConfiguration]; NSOperationQueue *queue = [[NSOperationQueuealloc]init]; self.session = [NSURLSessionsessionWithConfiguration:sessionConfigurationdelegate:selfdelegateQueue:queue]; NSURLSessionDataTask *dataTask = [self.sessiondataTaskWithRequest:request]; [dataTask resume]; } /** * 取消断点续传下载请求 */ -(void)cancel { if (self.session) { [self.sessioninvalidateAndCancel]; self.session =nil; } } #pragma mark -- NSURLSessionDelegate /* The last message a session delegate receives. A session will only become * invalid because of a systemic error or when it has been * explicitly invalidated, in which case the error parameter will be nil. */ - (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullableNSError *)error { NSLog(@"didBecomeInvalidWithError"); } #pragma mark -- NSURLSessionTaskDelegate /* Sent as the last message related to a specific task. Error may be * nil, which implies that no error occurred and this task is complete. */ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error { NSLog(@"didCompleteWithError"); if (error == nil &&self.error ==nil) { self.completionBlock(); } else if (error !=nil) { if (error.code != -999) { self.error = error; self.completionBlock(); } } else if (self.error !=nil) { self.completionBlock(); } } #pragma mark -- NSURLSessionDataDelegate /* Sent when data is available for the delegate to consume. It is * assumed that the delegate will retain and not copy the data. As * the data may be discontiguous, you should use * [NSData enumerateByteRangesUsingBlock:] to access it. */ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { //根据status code的不同,做相应的处理 NSHTTPURLResponse *response = (NSHTTPURLResponse*)dataTask.response; if (response.statusCode ==200) { self.totalContentLength = dataTask.countOfBytesExpectedToReceive; } else if (response.statusCode ==206) { NSString *contentRange = [response.allHeaderFieldsvalueForKey:@"Content-Range"]; if ([contentRange hasPrefix:@"bytes"]) { NSArray *bytes = [contentRangecomponentsSeparatedByCharactersInSet:[NSCharacterSetcharacterSetWithCharactersInString:@" -/"]]; if ([bytes count] == 4) { self.totalContentLength = [[bytesobjectAtIndex:3]longLongValue]; } } } else if (response.statusCode ==416) { NSString *contentRange = [response.allHeaderFieldsvalueForKey:@"Content-Range"]; if ([contentRange hasPrefix:@"bytes"]) { NSArray *bytes = [contentRangecomponentsSeparatedByCharactersInSet:[NSCharacterSetcharacterSetWithCharactersInString:@" -/"]]; if ([bytes count] == 3) { self.totalContentLength = [[bytesobjectAtIndex:2]longLongValue]; if (self.totalReceivedContentLength ==self.totalContentLength) { //说明已下完 //更新进度 self.progressBlock(); } else { //416 Requested Range Not Satisfiable self.error = [[NSErroralloc]initWithDomain:[self.urlabsoluteString]code:416userInfo:response.allHeaderFields]; } } } return; } else { //其他情况还没发现 return; } //向文件追加数据 NSFileHandle *fileHandle = [NSFileHandlefileHandleForUpdatingAtPath:self.targetPath]; [fileHandle seekToEndOfFile]; //将节点跳到文件的末尾 [fileHandle writeData:data]; //追加写入数据 [fileHandle closeFile]; //更新进度 self.totalReceivedContentLength += data.length; self.progressBlock(); } @end
经验证,如果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