本文将提供网络编程中文件的下载并存储在手机沙盒中的思路。
小文件下载:
如果文件比较小,下载方式会比较多:
直接用NSData的+ (id)dataWithContentsOfURL:(NSURL *)url;
利用NSURLConnection发送一个HTTP请求去下载
如果是下载图片,还可以利用SDWebImage框架
大文件下载:
如果大文件也用上面方法下载,由于文件很大,会有等待时间,又要将下载的文件存储到沙盒中,又要等待,这会给用户不好的体验。
我们仍然使用NSURLConnection的代理方法来下载文件。但是思路跟下载小文件是不一样的。如下:
- (void)viewDidLoad { [super viewDidLoad]; // 获取资源路径url NSURL *url = [NSURL URLWithString:KURLString]; // 创建请求 NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url]; // 建立连接,发送请求 [NSURLConnection connectionWithRequest:request delegate:self]; } /** * 数据接受失败 */ - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { NSLog(@"失败"); } /** * 接受服务器响应 */ - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { NSLog(@"刚接受响应"); // 获取沙盒路径 NSString *docuPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory , NSUserDomainMask, YES) lastObject]; // 拼接文件名 NSString *fileName = [docuPath stringByAppendingPathComponent:@"test.exe"]; // 创建文件管理器 NSFileManager *manage = [NSFileManager defaultManager]; // 创建目标文件路径(存储在沙盒中) [manage createFileAtPath:fileName contents:nil attributes:nil]; // 创建写数据的文件句柄 self.writeHandle = [NSFileHandle fileHandleForWritingAtPath:fileName]; self.totalLength = response.expectedContentLength; NSLog(@"完整文件大小:%lld", self.totalLength); } /** * 接受数据(会频繁调用) */ - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { NSLog(@"didReceiveData--%lu", data.length); // 先将句柄移动到文件最尾部 [self.writeHandle seekToEndOfFile]; // 开始写数据 [self.writeHandle writeData:data]; } /** * 数据接受完成 */ - (void)connectionDidFinishLoading:(NSURLConnection *)connection { NSLog(@"成功"); // 关闭文件句柄 [self.writeHandle closeFile]; self.writeHandle = nil; }
看看打印情况:
在沙盒中生成的文件:
当客户端开始接受服务器响应的时候,会调用didReceiveResponse:方法,在该方法中创建目标文件(和服务器上的文件类型相同),再创建文件句柄,用于写数据。
当有数据返回的时候,会调用didReceiveData:方法,在该方法中,将句柄移动到已写文件的尾部,再继续向文件中写数据。
当数据接受完的时候,会调用connectionDidFinishLoading:方法,在该方法中将句柄关闭,防止数据泄露。
将下载的大文件存储在Caches文件夹下,该文件夹下的文件不会被备份,也不会被随机删除。
下面给下载文件增加一个功能,就是暂停\开始下载功能,并实现下载进度。实现思路:暂停的时候调用记录已经文件的大小,再开始下载的时候就从当前文件尾部开始下载文件数据。代码如下:
- (IBAction)start { if (self.isDownloading) {// 点击暂停 self.downloading = NO; [self.downloadBtn setTitle:@"开始" forState:UIControlStateNormal]; // 取消下载操作 <span style="color:#FF0000;">[self.conn cancel];// conn已取消,该conn就不存在了,下次下载就必须重新创建 self.conn = nil;</span> }else {// 点击开始 self.downloading = YES; [self.downloadBtn setTitle:@"暂停" forState:UIControlStateNormal]; // 获取资源路径url NSURL *url = [NSURL URLWithString:KURLString]; // 创建请求 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; // 设置请求头,设置下载文件的起点 <span style="color:#FF0000;">NSString *value = [NSString stringWithFormat:@"bytes=%lld-", self.currentlength]; [request setValue:value forHTTPHeaderField:@"Range"];</span> // 建立连接,发送请求 self.conn = [NSURLConnection connectionWithRequest:request delegate:self]; } } /** * 数据接受失败 */ - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { NSLog(@"失败"); } /** * 接受服务器响应 */ - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { // response.suggestedFilename:文件名 NSLog(@"刚接受响应:%@", response.suggestedFilename); if (self.totalLength) return;// 判断是否是第一次下载 // 获取沙盒路径 NSString *docuPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory , NSUserDomainMask, YES) lastObject]; // 拼接文件名 NSString *fileName = [docuPath stringByAppendingPathComponent:response.suggestedFilename]; // 创建文件管理器 <span style="color:#FF0000;"> NSFileManager *manage = [NSFileManager defaultManager];</span> // 创建目标文件路径(存储在沙盒中) <span style="color:#FF0000;">[manage createFileAtPath:fileName contents:nil attributes:nil];</span> // 创建写数据的文件句柄 <span style="color:#FF0000;">self.writeHandle = [NSFileHandle fileHandleForWritingAtPath:fileName];</span> // 完整文件大小 <span style="color:#FF0000;"> self.totalLength = response.expectedContentLength;</span> NSLog(@"完整文件大小:%lld", self.totalLength); } /** * 接受数据(会频繁调用) */ - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { NSLog(@"didReceiveData--%lu", data.length); // 记录当前下载的文件大小 self.currentlength += data.length; self.progressView.progress = (double)self.currentlength / self.totalLength; // 先将句柄移动到文件最尾部 <span style="color:#FF0000;">[self.writeHandle seekToEndOfFile];</span> // 开始写数据 <span style="color:#FF0000;">[self.writeHandle writeData:data];</span> } /** * 数据接受完成 */ - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self.downloadBtn setTitle:@"完成" forState:UIControlStateNormal]; // 重置数据 self.currentlength = 0; self.totalLength = 0; // 关闭文件句柄 <span style="color:#FF0000;">[self.writeHandle closeFile];</span> <span style="color:#FF0000;">self.writeHandle = nil;</span> }
通过设置请求头Range可以指定每次从网路下载数据包的大小
Range示例:
bytes=0-499 从0到499的头500个字节
bytes=500-999 从500到999的第二个500字节
bytes=500- 从500字节以后的所有字节
bytes=-500 最后500个字节
bytes=500-599,800-899 同时指定几个范围
Range小结:
“-”用于分隔,前面的数字表示起始字节数;后面的数组表示截止字节数,没有表示到末尾。
“,”用于分组,可以一次指定多个Range,不过很少用
断点续传用到了NSFileManage和NSFileHandle两个类,一个负责创建文件,一个负责向文件中写数据。数据下载完之后,一定要将句柄关闭[self.writeHandle closeFile]。
获取完整文件大小的方法不只这一种,如下:
1. 在didReceiveData:房中累加data的长度,最后就是完整文件的大小
2. 还可以利用response的真实类型NSHTTPURLResponse对象的allHeaderFields属性获取文件大小。