iOS之[文件下载 / 大文件下载 / 断点下载]
援引:https://my.oschina.net/u/2462423/blog/602519
1.NSData(小文件下载) [NSData dataWithContentsOfURL:] 就是一种文件下载方式,Get请求 但是这种下载方式需要放到子线程中 NSURL *url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/test.png"]; NSData *data = [NSData dataWithContentsOfURL:url]; 2.NSURLConnection 2.1小文件下载(NSURLConnection) 通过NSURLConnection发送一个异步的Get请求,一次性将整个文件返回 NSURL* url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/test.png"]; [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:url] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { //atomically:原子性 [data writeToFile:filePath atomically:YES]; }]; 2.2 大文件下载(NSURLConnection) 大文件下载不能一次性返回整个文件,否则会造成内存泄漏,系统崩溃 // 发送请求去下载 (创建完conn对象后,会自动发起一个异步请求) IOS9已经废弃:[NSURLConnection connectionWithRequest:request delegate:self]; 需要实现NSURLConnectionDataDelegate 协议,常用的几个如下: /** * 请求失败时调用(请求超时、网络异常) */ - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error; /** * 1.接收到服务器的响应就会调用 */ - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; /** * 2.当接收到服务器返回的实体数据时调用(具体内容,这个方法可能会被调用多次) */ - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data; /** * 3.加载完毕后调用(服务器的数据已经完全返回后) */ - (void)connectionDidFinishLoading:(NSURLConnection *)connection; didReceiveData方法会被频繁的调用,每次都会传回来一部分data 最终我们把每次传回来的数据合并成一个我们需要的文件。 通常合并文件是定义一个全局的NSMutableData,通过[mutableData appendData:data];来合并,最后将Data写入沙盒 代码如下: @property (weak, nonatomic) IBOutlet UIProgressView *progressView; @property (nonatomic, strong) NSURLRequest *request; @property (nonatomic, strong) NSURLResponse *response; @property (nonatomic, strong) NSMutableData *fileData; @property (nonatomic, assign) long long fileLength; //>>文件长度 @property (nonatomic, strong) NSString *fileName; //>>文件名 - (void)viewDidLoad { [super viewDidLoad]; _request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/objective-c.pdf"]]; //IOS9.0已经被废弃 [NSURLConnection connectionWithRequest:_request delegate:self]; } /** *接收到服务器的响应 */ - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{ self.response = response; self.fileData = [NSMutableData data]; //获取下载文件大小 self.fileLength = response.expectedContentLength; //获取文件名 self.fileName = response.suggestedFilename; } /** *接收到服务器返回的数据(可能被调用多次) */ - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{ [self.fileData appendData:data]; //更新画面中的进度 double progress = (double)self.fileData.length/self.fileLength; self.progressView.progress = progress; } /** *服务器返回数据完了 */ - (void)connectionDidFinishLoading:(NSURLConnection *)connection{ NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *filePath = [cache stringByAppendingPathComponent:_fileName]; [self.fileData writeToFile:filePath atomically:YES]; } 但是有个致命的问题,内存!用来接受文件的NSMutableData一直都在内存中,会随着文件的下载一直变大, 内存 合理的方式在我们获取一部分data的时候就写入沙盒中,然后释放内存中的data 要用到NSFilehandle这个类,这个类可以实现对文件的读取、写入、更新 每次接收到数据的时候就拼接文件后面,通过- (unsigned long long)seekToEndOfFile;方法 /** *接收到服务器的响应 */ - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { // 文件路径 NSString* caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString* filepath = [ceches stringByAppendingPathComponent:response.suggestedFilename]; // 创建一个空的文件到沙盒中 NSFileManager* fileManager = [NSFileManager defaultManager]; [fileManager createFileAtPath:filepath contents:nil attributes:nil]; // 创建一个用来写数据的文件句柄对象:用来填充数据 self.writeHandle = [NSFileHandle fileHandleForWritingAtPath:filepath]; // 获得文件的总大小 self.fileLength = response.expectedContentLength; } /** * 2.当接收到服务器返回的实体数据时调用(具体内容,这个方法可能会被调用多次) */ - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { // 移动到文件的最后面 [self.writeHandle seekToEndOfFile]; // 将数据写入沙盒 [self.writeHandle writeData:data]; // 累计写入文件的长度 self.currentLength += data.length; // 下载进度 self.pregressView.progress = (double)self.currentLength / self.fileLength; } /** * 3.加载完毕后调用(服务器的数据已经完全返回后) */ - (void)connectionDidFinishLoading:(NSURLConnection *)connection { self.currentLength = 0; self.fileLength = 0; // 关闭文件 [self.writeHandle closeFile]; self.writeHandle = nil; } 下载过程中内存就会一直很稳定了,并且下载的文件也是没问题的 内存正常 2.3 断点下载(NSURLConnection) 断点续传的response状态码为206 暂停/继续下载也是现在下载中必备的功能 NSURLConnection 只提供了一个cancel方法,这并不是暂停,而是取消下载任务。如果要实现断点下载必须要了解HTTP协议中请求头的Range 通过设置请求头的Range我们可以指定下载的位置、大小 Range实例: bytes = 0-499 从0到499的头500个字节 bytes = 500-999 从500到999的第二个500个字节 bytes = 500- 500之后的所有字节 bytes = -500 最后500个字节 bytes = 0-599,700-899 同时指定多个范围 || pragma mark --按钮点击事件, - (IBAction)btnClicked:(UIButton *)sender { // 状态取反 sender.selected = !sender.isSelected; // 断点下载 if (sender.selected) { // 继续(开始)下载 // 1.URL NSURL *url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/Code.pdf"]; // 2.请求 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; // 设置请求头 NSString *range = [NSString stringWithFormat:@"bytes=%lld-", self.currentLength]; [request setValue:range forHTTPHeaderField:@"Range"]; // 3.下载 self.connection = [NSURLConnection connectionWithRequest:request delegate:self]; } else { // 暂停 [self.connection cancel]; self.connection = nil; } } 2.4 断点下载的全部代码 @interface NSURLConnectionViewController ()<NSURLConnectionDataDelegate> @property (weak, nonatomic) IBOutlet UIProgressView *progressView; @property (nonatomic, strong) NSURL *url; @property (nonatomic, strong) NSURLRequest *request; @property (nonatomic, strong) NSHTTPURLResponse *response; @property (nonatomic, strong) NSURLConnection *connection; @property (nonatomic, strong) NSFileHandle *fileHandle; @property (nonatomic, assign) long long currentLength; //>>写入文件的长度 @property (nonatomic, assign) long long fileLength; //>>文件长度 @property (nonatomic, strong) NSString *fileName; //>>文件名 @end @implementation NSURLConnectionViewController - (void)viewDidLoad { [super viewDidLoad]; NSLog(@"%@", NSHomeDirectory()); self.url = [NSURL URLWithString:@"http://10.167.20.151:8080/AdminConsole/help/objective-c.pdf"]; } #pragma mark - NSURLConnectionDataDelegate /** *请求失败 */ -(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{ NSLog(@"error"); } /** *接收到服务器的响应 */ - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{ self.response = (NSHTTPURLResponse *)response; if (self.response.statusCode == 206) {//!!!断点续传的状态码为206 if (self.currentLength) { return; } //获取下载文件大小 self.fileLength = response.expectedContentLength; //获取文件名 self.fileName = response.suggestedFilename; //文件路径 NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *filePath = [caches stringByAppendingPathComponent:_fileName]; //创建一个空的文件到沙盒 NSFileManager *fileManager = [NSFileManager defaultManager]; [fileManager createFileAtPath:filePath contents:nil attributes:nil]; //创建一个用来写数据的文件句柄 self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath]; }else{ [self.connection cancel]; self.connection = nil; NSLog(@"该文件不存在"); } } /** *接收到服务器返回的数据(可能被调用多次) */ - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{ //移动到文件末尾 [self.fileHandle seekToEndOfFile]; //写入数据到文件 [self.fileHandle writeData:data]; self.currentLength += data.length; //更新画面中的进度 double progress = (double)self.currentLength/self.fileLength; self.progressView.progress = progress; } /** *服务器返回数据完了 */ - (void)connectionDidFinishLoading:(NSURLConnection *)connection{ self.currentLength = 0; self.fileLength = 0; [self.fileHandle closeFile]; self.fileHandle = nil; } - (IBAction)pauseDownLoad:(UIButton *)sender { //暂停<->开始转换 sender.selected = !sender.isSelected; if (sender.selected) {//开始下载 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_url]; //设置请求头(GET) NSString *range = [NSString stringWithFormat:@"bytes=%lld-", self.currentLength]; [request setValue:range forHTTPHeaderField:@"Range"]; self.connection = [NSURLConnection connectionWithRequest:request delegate:self]; }else{ //暂停 [self.connection cancel]; self.connection = nil; } } @end 在下载过程中,为了提高效率,充分利用cpu性能,通常会执行多线程下载。。。待更新!!! 4. NSURLSession 生命周期的两种方式-1:系统代理-Block方式(流程简单-优先) -2:指定代理类:delegate(流程复杂) 上面这种下载文件的方式确实比较复杂,要自己去控制内存写入相应的位置 iOS7推出了一个新的类NSURLSession,它具备了NSURLConnection所具备的方法,同时也比它更强大 NSURLSession 也可以发送Get/Post请求,实现文件的下载和上传。 在NSURLSesiion中,任何请求都可以被看做是一个任务。其中有三种任务类型 NSURLSessionDataTask : 普通的GET\POST请求 NSURLSessionDownloadTask : 文件下载 NSURLSessionUploadTask : 文件上传(很少用,一般服务器不支持) 4.1 NSURLSession 简单使用 NSURLSession发送请求非常简单,与connection不同的是,任务创建后不会自动发送请求,需要手动开始执行任务 // 1.得到session对象 NSURLSession* session = [NSURLSession sharedSession]; NSURL* url = [NSURL URLWithString:@""]; // 2.创建一个task,任务(GET) NSURLSessionDataTask* dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { // data 为返回数据 }]; // 3.开始任务 [dataTask resume]; POST请求:可以自定义请求头 - (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; 4.2 NSURLSession文件下载 使用NSURLSession就非常简单了,不需要去考虑什么边下载边写入沙盒的问题,苹果都帮我们做好了 只用将下载好的文件通过NSFileManager剪切到指定位置 需要用到NSURLSession的子类:NSURLSessionDownloadTask NSURL* url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/test.png"]; // 得到session对象 NSURLSession* session = [NSURLSession sharedSession]; // 创建任务 NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { //将文件迁移到指定路径下 NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *file = [caches stringByAppendingPathComponent:response.suggestedFilename]; // 将临时文件剪切或者复制Caches文件夹 NSFileManager *fileManager = [NSFileManager defaultManager]; // AtPath : 剪切前的文件路径 // ToPath : 剪切后的文件路径 [fileManager moveItemAtPath:location.path toPath:file error:nil]; }]; // 开始任务 [downloadTask resume]; location就是下载好的文件写入沙盒的地址 该方式无法监听下载进度 若要监听进度需要实现< NSURLSessionDownloadDelegate >协议,不能使用Block方式 /** * 下载完毕会调用 */ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {...} /** * 每次写入沙盒完毕调用 * 在这里面监听下载进度,totalBytesWritten/totalBytesExpectedToWrite * * @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 {...} /** * 恢复下载后调用 */ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes {...} 4.2 NSURLSession断点下载 :fa-font:1.暂停下载 resumeData,该参数包含了继续下载文件的位置信息 resumeData只包含了url跟已经下载了多少数据,不会很大,不用担心内存问题 - (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler; !!!需要注意的是Block中循环引用的问题 __weak typeof(self) selfVc = self; [self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) { selfVc.resumeData = resumeData; selfVc.downloadTask = nil; }]; :fa-bold:2.恢复下载 - (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData; 4.3 NSURLSession断点下载所有代码 @interface NSURLSessionController () <NSURLSessionDownloadDelegate> @property (weak, nonatomic) IBOutlet UIProgressView *myPregress; //下载任务 @property (nonatomic, strong) NSURLSessionDownloadTask* downloadTask; //resumeData记录下载位置 @property (nonatomic, strong) NSData* resumeData; @property (nonatomic, strong) NSURLSession* session; @end @implementation NSURLSessionController /** * session的懒加载 */ - (NSURLSession *)session { if (nil == _session) { NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration defaultSessionConfiguration]; self.session = [NSURLSession sessionWithConfiguration:cfg delegate:self delegateQueue:[NSOperationQueue mainQueue]]; } return _session; } - (void)viewDidLoad { [super viewDidLoad]; } /** * 从0开始下载 */ - (void)startDownload { NSURL* url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/objective-c.pdf"]; // 创建任务 self.downloadTask = [self.session downloadTaskWithURL:url]; // 开始任务 [self.downloadTask resume]; } /** * 恢复下载 */ - (void)resume { // 传入上次暂停下载返回的数据,就可以恢复下载 self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData]; [self.downloadTask resume]; // 开始任务 self.resumeData = nil; } /** * 暂停 */ - (void)pause { __weak typeof(self) selfVc = self; [self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) { // resumeData : 包含了继续下载的开始位置\下载的url selfVc.resumeData = resumeData; selfVc.downloadTask = nil; }]; } #pragma mark -- NSURLSessionDownloadDelegate /** * 下载完毕会调用 * * @param location 文件临时地址 */ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { //文件路径 NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *file = [caches stringByAppendingPathComponent:downloadTask.response.suggestedFilename]; // 将临时文件剪切或者复制Caches文件夹 NSFileManager *fileManager = [NSFileManager defaultManager]; // AtPath : 剪切前的文件路径 // ToPath : 剪切后的文件路径 [fileManager moveItemAtPath:location.path toPath:file error:nil]; NSLog(@"下载完成"); } /** * 每次写入沙盒完毕调用 * 在这里面监听下载进度,totalBytesWritten/totalBytesExpectedToWrite * * @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 { self.myPregress.progress = (double)totalBytesWritten/totalBytesExpectedToWrite; } /** * 恢复下载后调用, */ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { } #pragma mark --按钮点击事件 - (IBAction)btnClicked:(UIButton *)sender { // 按钮状态取反 sender.selected = !sender.isSelected; if (nil == self.downloadTask) { // 开始(继续)下载 if (self.resumeData) { // 继续下载 [self resume]; }else{ // 从0开始下载 [self startDownload]; } }else{ // 暂停 [self pause]; } }