iOS之断点下载,使用NSURLSession简单封装
最近公司需要做个文件管理的功能模块,刚交到博主手上时,头都大了。因为没做过这方面的东西,只好咬牙加班,并请教某位大神,指点了一下,清楚研究方向,找了网上大量资料,最后实现简单的封装。
上代码:.h文件
#import <Foundation/Foundation.h> @interface DocDownloader : NSObject /** * 创建断点续传管理对象,启动下载请求 * * @param url 文件资源地址 * @param targetPath 文件存放路径 * @param success 文件下载成功的回调块 * @param failure 文件下载失败的回调块 * @param progress 文件下载进度的回调块 * * @return 断点续传管理对象 * */ +(DocDownloader*)resumeManagerWithURL:(NSURL*)url targetPath:(NSString*)targetPath success:(void (^)())success failure:(void (^)(NSError *error))failure progress:(void (^)(long long totalReceivedContentLength, long long totalContentLength))progress; /** * 启动断点续传下载请求(普通的静态下载链接或GET请求) */ -(void)start; /** * 启动断点续传下载请求(POST请求) * * @param params POST的内容 */ -(void)startWithParams:(NSString *)params; /** * 取消断点续传下载请求 */ -(void)cancel;
.m文件
#import "DocDownloader.h" typedef void (^completionBlock)(); typedef void (^progressBlock)(); @interface DocDownloader ()<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 long long totalContentLength; //文件总大小 @property long long totalReceivedContentLength; //已下载大小 /** * 设置成功、失败回调block * * @param success 成功回调block * @param failure 失败回调block */ - (void)setCompletionBlockWithSuccess:(void (^)())success failure:(void (^)(NSError *error))failure; /** * 设置进度回调block * * @param progress */ -(void)setProgressBlockWithProgress:(void (^)(long long totalReceivedContentLength, long long totalContentLength))progress; /** * 获取文件大小 * @param path 文件路径 * @return 文件大小 * */ - (long long)fileSizeForPath:(NSString *)path; @end @implementation DocDownloader /** * 设置成功、失败回调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 (^)(long long totalReceivedContentLength, long long 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 = [NSFileManager new]; // 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 断点续传管理对象 * */ +(DocDownloader*)resumeManagerWithURL:(NSURL*)url targetPath:(NSString*)targetPath success:(void (^)())success failure:(void (^)(NSError *error))failure progress:(void (^)(long long totalReceivedContentLength, long long totalContentLength))progress{ DocDownloader *manager = [[DocDownloader alloc]init]; manager.url = url; manager.targetPath = targetPath; [manager setCompletionBlockWithSuccess:success failure:failure]; [manager setProgressBlockWithProgress:progress]; manager.totalContentLength = 0; manager.totalReceivedContentLength = 0; return manager; } /** * 启动断点续传下载请求(普通的静态下载链接或GET请求) */ -(void)start{ NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:self.url]; long long downloadedBytes = self.totalReceivedContentLength = [self fileSizeForPath:self.targetPath]; if (downloadedBytes > 0) { NSString *requestRange = [NSString stringWithFormat:@"bytes=%llu-", downloadedBytes]; [request setValue:requestRange forHTTPHeaderField:@"Range"]; }else{ int fileDescriptor = open([self.targetPath UTF8String], O_CREAT | O_EXCL | O_RDWR, 0666); if (fileDescriptor > 0) { close(fileDescriptor); } } NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; NSOperationQueue *queue = [[NSOperationQueue alloc]init]; self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:queue]; NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request]; [dataTask resume]; } /** * 启动断点续传下载请求(POST请求) * * @param params POST的内容 */ -(void)startWithParams:(NSString *)params{ NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:self.url]; [request setHTTPMethod:@"POST"]; [request setHTTPBody:[params dataUsingEncoding:NSUTF8StringEncoding]]; long long downloadedBytes = self.totalReceivedContentLength = [self fileSizeForPath:self.targetPath]; if (downloadedBytes > 0) { NSString *requestRange = [NSString stringWithFormat:@"bytes=%llu-", downloadedBytes]; [request setValue:requestRange forHTTPHeaderField:@"Range"]; }else{ int fileDescriptor = open([self.targetPath UTF8String], O_CREAT | O_EXCL | O_RDWR, 0666); if (fileDescriptor > 0) { close(fileDescriptor); } } NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; NSOperationQueue *queue = [[NSOperationQueue alloc]init]; self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:queue]; NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request]; [dataTask resume]; } /** * 取消断点续传下载请求 */ -(void)cancel{ if (self.session) { [self.session invalidateAndCancel]; 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:(nullable NSError *)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{ //NSLog(@"dataLength = %lu",(unsigned long)data.length); //根据status code的不同,做相应的处理 NSHTTPURLResponse *response = (NSHTTPURLResponse*)dataTask.response; NSLog(@"response = %@",response); if (response.statusCode == 200) { NSString *contentRange = [response.allHeaderFields valueForKey:@"Content-Length"]; self.totalContentLength = [contentRange longLongValue]; }else if (response.statusCode == 206){ NSString *contentRange = [response.allHeaderFields valueForKey:@"Content-Range"]; if ([contentRange hasPrefix:@"bytes"]) { NSArray *bytes = [contentRange componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" -/"]]; if ([bytes count] == 4) { self.totalContentLength = [[bytes objectAtIndex:3] longLongValue]; } } }else if (response.statusCode == 416){ NSString *contentRange = [response.allHeaderFields valueForKey:@"Content-Range"]; if ([contentRange hasPrefix:@"bytes"]) { NSArray *bytes = [contentRange componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" -/"]]; if ([bytes count] == 3) { self.totalContentLength = [[bytes objectAtIndex:2] longLongValue]; if (self.totalReceivedContentLength == self.totalContentLength) { //说明已下完,更新进度 self.progressBlock(); }else{ //416 Requested Range Not Satisfiable self.error = [[NSError alloc]initWithDomain:[self.url absoluteString] code:416 userInfo:response.allHeaderFields]; } } } return; }else{ //其他情况还没发现 return; } NSFileManager *fileManager = [NSFileManager defaultManager]; //向文件追加数据 NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:self.targetPath]; [fileHandle seekToEndOfFile]; //将节点跳到文件的末尾 [fileHandle writeData:data];//追加写入数据 if ([fileManager fileExistsAtPath:self.targetPath]) { self.totalReceivedContentLength = [[fileManager attributesOfItemAtPath:self.targetPath error:nil] fileSize]; if (self.totalContentLength == self.totalReceivedContentLength) { NSLog(@"下载完了"); //下载完了,停止请求 [self cancel]; self.completionBlock(); } } [fileHandle closeFile]; self.progressBlock(); }
使用步骤:
1.
[DocDownloader resumeManagerWithURL:[NSURL URLWithString:urlStr] targetPath:self.targetPath success:^{ NSLog(@"WebRequestTypeDocDownload_success"); //下载完成,可以写入一些完成之后的操作 } failure:^(NSError *error) { NSLog(@"WebRequestTypeDocDownload_failure"); //下载失败,可以做相应的提示 } progress:^(long long totalReceivedContentLength, long long totalContentLength) { //回调totalReceivedContentLength和totalContentLength // 下载了多少:totalReceivedContentLength // 文件总大小: totalContentLength // 进度条可以这样表示: //progress = totalReceivedContentLength / totalContentLength }];
2.启动下载(POST请求)
[self.manager startWithParams:paramStr];
3.暂停下载
- (void)suspendWithCancel { [self.manager cancel]; self.manager = nil; }
那么问题来了,如果下载了一部分就暂停了,退出app,重新进来,文件数据呢???
这个其实我们已经写入文件了,最好写入Documents目录下。首先判断第一次进入时,检查文件路径是否存在,若存在就需要计算出文件大小,并与我们知道的文件的总大小做比较。
这里比较,可以分为两种情况:(这里以fileSize为文件计算出的大小,totalFileSize为文件的总大小)
第一种文件没有加密,这个很好处理,1.如果0<fileSize<totalFileSize,说明已经下载了一部分,下载进度为fileSize。2.如果fileSize==totalFileSize,说明已经下载完了,标记状态。
第二种文件下载完成后加密,加密之后的文件大小会比原来的大小大一些(由于博主的加密方式是sm4加密,不知道其他的加密方法会不会比原来的大一些),
1.如果0<fileSize<totalFileSize,说明已经下载了一部分,下载进度为fileSize。2.如果fileSize>=totalFileSize,说明已经下载完了,标记状态。