iOS开发之文件下载

时间:2021-06-23 20:59:07

本文将提供网络编程中文件的下载并存储在手机沙盒中的思路。

小文件下载:

如果文件比较小,下载方式会比较多:

直接用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;
}


看看打印情况:

iOS开发之文件下载

在沙盒中生成的文件:

iOS开发之文件下载

当客户端开始接受服务器响应的时候,会调用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属性获取文件大小。