NSURLConnection 文件下载的BUG及解决思路、方案

本文详细探讨了使用NSURLConnection进行文件下载时可能遇到的问题及解决方案,包括如何避免内存暴涨、如何实现平滑的下载进度显示以及如何处理断点续传等问题。
NSURLConnection 文件下载的BUG及解决思路、方案


  // 一般在文件名下载的过程中 , 应该告诉用户下载进度 ( 进度条 ).
 思路:   NSUrlConnection :
下载 .
    {
       
小文件 : 直接利用 block 回调 ( 异步请求 , 下载好的文件就是 block 回调中的 data).
       
       
中文件 : 会造成内存暴涨 { 先将文件下载到内存中 (data), 然后再写入磁盘 }. 为了防止内存暴涨 , 不能直接使用 block 回调 .
       
       
大文件 : 会造成内存暴涨 { 先将文件下载到内存中 (data), 然后再写入磁盘 }. 为了防止内存暴涨 , 不能直接使用 block 回调 .
    }
}

NSUrlConnection
下载 Bug 汇总 :

1. 异步回调下载
{
   
如果下载大文件 : 内存暴涨 . ( 下载下来的内容存储在 data , 整个内容下载完毕之后 , 都存储在内存中 , 然后再一次性写入沙盒 .)
}

2. NSUrlConnectionDownloadDelegate
{
   
1. 可以监听下载进度 .
   
2. 找不到下载好的文件 .
}

3. NSUrlConnectionDataDelegate
{
   
1. 需要自己写业务逻辑监听下载进度 .
   
2. 将每次下载好的文件内容用一个属性保存起来 , 然后等文件都下载完毕之后再写入沙盒
    {
       
1 > 内存暴涨 , 原因同异步回调下载 .
       
       
2 > 如果不及时清除属性中保存的内容 , 内存占用量会一直很大 .
    }
}

4. 同时使用 NSUrlConnectionDownloadDelegate NSUrlConnectionDataDelegate, 持续下载数据的方法只会调用 NSUrlConnectionDownloadDelegate. 所以不能同时使用这两个代理 .

5. NSUrlConnectionDataDelegate
{
   
1. 下载进度不能平滑的显示 ( 一段一段的显示 ): 默认下载回调是在主线程进行的 , 下载过程阻塞了主线程 ,UI 不能够及时的显示 .
   
   
2. 边下载 , 边写入沙盒
    {
       
1. NSFileHandle
       
       
2. NSOutputStream
       
       
如果下载多次 , 都会造成本地下载好的文件变大 ...
    }
}

6. NSUrlConnectionDataDelegate
{
   
1. 设置 代理回调在子线程 : 代理方法的调用确实在子线程 , 并且是多条线程 . 但是 , 主线程在执行 UI 操作的时候 , 后台线程会卡住 ( 会阻塞下载 ).
   
   
2. 开启新的下载任务之前 , 要检查本地文件和服务器文件的大小 . 做业务逻辑的判断 .
}

7. NSUrlConnectionDataDelegate
{
   
1. 将网络连接放在子线程 , 这样和主线程就没有任何关系 . 但是需要开启子线程运行循环之后 , 才能够执行 下载的代理方法 . : NSUrlConnectionDataDelegate 是一个特殊的运行循环 .( 下载完毕之后 , 运行循环自动停止 .) --> 保证在执行 UI 操作的时候 , 后台可以继续进行下载 . 设置代理回调为非主队列 , 可以在多条线程同时下载 .
   
   
2. 下载业务逻辑
    {
       
1. 本地文件大小 > 服务器文件大小 : 1 > 删除本地文件 2 > 重新开始下载
       
       
2. 本地文件大小 < 服务器文件大小 :
        {
            *
1 如果本地文件大小 = 0 : 直接从 0 开始下载 ( 重新开始下载 ).
           
            *
2 如果本地文件大小 > 0 : 断点续传 :
            {
               
设置 Range 属性 , 告诉服务器断点续传开始的位置 .
               
                Range
格式 : [bytes=%ld-%ld,X,Y] , X 位置开始 , 下载 Y 个字节 .
               
               
设置 Range 属性之后 , 服务器返回的状态码会变成 206 .
            }
        }
       
    }
}



具体的方案:

所有的网络请求只能是异步请求,。。。。同步请求(不会创建子线程)会阻塞主线程。。。。

小文件:直接利用block回调,把(data 保存到本地的路径就好了)下载好的文件就是block回调中的data 。小文件没问题。
  
   
NSString * string = @"http://192.168.1.254/xiaowenjian2.zip" ;
   
   
NSURL * url = [ NSURL URLWithString :string];
   
   
NSURLRequest * request =[ NSURLRequest requestWithURL :url];
   
// 发送异步请求,下载文件
    [
NSURLConnection sendAsynchronousRequest :request queue :[ NSOperationQueue mainQueue ] completionHandler :^( NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
       
        [data
writeToFile : @"/Users/zzx/Desktop/xiaowenjian2.zip" atomically : YES ];
       
    }];
    


中文件、大文件:BUG 1.会造成内存暴涨(先将文件下载到内存中,然后再写入磁盘) 为了防止内存暴涨,不能直接使用block回调.——>解决方法,用NSURLConnectionDownloadDelegate。
- ( void )touchesBegan:( NSSet < UITouch *> *)touches withEvent:( UIEvent *)event
{
   
// NSURLConnectionDownloadDelegate
   
// 可以监听下载进度 .
   
// 但是 , 文件下载完毕之后 , 找不到下载好的文件在哪里 .(destinationURL 的位置没有文件 )
   
   
   
NSLog ( @"touchesBegan" );
   
   
// 文件下载 :
   
// http://127.0.0.1/xiaowenjian1.jpg : 网络接口地址 ( 文件下载地址 )
   
// http://127.0.0.1/xiaowenjian2.zip
   
// http://127.0.0.1/zhongwenjian.zip
   
   
NSString *urlString = @"http://127.0.0.1/dawenjian.zip" ;
   
   
// 1. 创建请求
   
NSURL *url = [ NSURL URLWithString :urlString];
   
   
NSURLRequest *request = [ NSURLRequest requestWithURL :url];
   
   
// 下载 , 代理 .
   
   
// 创建网络连接 , 设置代理对象
   
NSURLConnection *conn = [[ NSURLConnection alloc ] initWithRequest :request delegate : self ];
   
   
// 代理对象创建完毕之后 , 就会自动调用代理方法 . 不需要手动启动的 .
    [conn
start ];
   
   
}

#pragma NSURLConnectionDownloadDelegate
// 频繁地从服务器下载资源 ( 文件 ). 知道文件下载完毕 .( 取消下载图片的操作 , 这一个方法是判断取消的具体位置 .)
// bytesWritten: 本次调用方法下载的数据量 .
// totalBytesWritten: 已经下载的总数据量 .
// expectedTotalBytes: : 需要下载的数据总量 ( 下载资源的大小 .)
- (
void )connection:( NSURLConnection *)connection didWriteData:( long long )bytesWritten totalBytesWritten:( long long )totalBytesWritten expectedTotalBytes:( long long ) expectedTotalBytes
{
   
// 下载单位 : 字节
   
NSLog ( @" 本次下载的大小 :%lld 已经下载了 :%lld 总共需要下载 :%lld %@" ,bytesWritten,totalBytesWritten,expectedTotalBytes, [ NSThread currentThread ]);
}

- (
void )connectionDidResumeDownloading:( NSURLConnection *)connection totalBytesWritten:( long long )totalBytesWritten expectedTotalBytes:( long long ) expectedTotalBytes
{
   
NSLog ( @" 断点续传的方法 , 不使用 .%@" ,[ NSThread currentThread ]);
}

- (
void )connectionDidFinishDownloading:( NSURLConnection *)connection destinationURL:( NSURL *) destinationURL
{
   
// destinationURL: 文件下载完毕之后 , 保存的地址 .
   
NSLog ( @" 文件下载完毕 ,%@ %@" ,destinationURL,[ NSThread currentThread ]);
}


  2.NSURLConnectionDownloadDelegate可以监听,但找不到tmp里面下载好的文件(解决方法:换代理方法用NSURLConnectionDataDelegate)

代理对象创建完毕后,代理方法会自动调用自动下载,不需要手动下载数据。

实体内容就是我们想要的数据,
响应头最先调用返回的是数据类型和大小,服务器信息

步骤:1.实例化一个NSMutableData,以保存下载好的数据
2。在 接收到数据(实体内容)拼接数据
3.在数据下载完毕后写入本地磁盘,用MD5校验两个文件时否一致,用终端,

#import "ViewController.h"

@interface ViewController ()< NSURLConnectionDataDelegate , NSURLConnectionDownloadDelegate >

@property ( nonatomic , strong ) NSMutableData *fileData;

@end

@implementation ViewController

-(
NSMutableData *)fileData
{
   
if (! _fileData ) {
       
_fileData = [ NSMutableData data ];
    }
   
return _fileData ;
}

- (
void )viewDidLoad {
    [
super viewDidLoad ];
   
   
// NSURLConnectionDataDelegate
   
   
// 直接用一个可变二进制数据文件接收下载好的数据 , 数据接收完毕之后 , 在本地保存 : 内存依然暴涨 . 文件写入本地磁盘之后 , 如果不及时释放下载好的文件 , 会造成内存一个很大 .
   
   
// NSURLConnectionDataDelegate : 检测下载进度 , 需要自己写业务逻辑 .
   
   
// NSURLConnectionDownloadDelegate 处理下载进度 , NSURLConnectionDataDelegate 处理下载数据 .
   
// NSURLConnectionDownloadDelegate NSURLConnectionDataDelegate 方法 ( 持续下载数据的方法 ) 不可以同时调用 .

}

- (
void )touchesBegan:( NSSet < UITouch *> *)touches withEvent:( UIEvent *)event
{
   
NSLog ( @"touchesBegan" );
   
   
// 文件下载 :
   
// http://127.0.0.1/xiaowenjian1.jpg : 网络接口地址 ( 文件下载地址 )
   
// http://127.0.0.1/xiaowenjian2.zip
   
// http://127.0.0.1/zhongwenjian.zip
   
   
NSString *urlString = @"http://127.0.0.1/dawenjian.zip" ;
   
   
// 1. 创建请求
   
NSURL *url = [ NSURL URLWithString :urlString];
   
   
NSURLRequest *request = [ NSURLRequest requestWithURL :url];
   
   
// 下载 , 代理 .
   
   
// 创建网络连接 , 设置代理对象
   
NSURLConnection *conn = [[ NSURLConnection alloc ] initWithRequest :request delegate : self ];
   
   
// 代理对象创建完毕之后 , 就会自动调用代理方法 . 不需要手动启动的 .
    [conn
start ];
   
}


#pragma NSURLConnectionDataDelegate
// 接收到 响应头信息的时候就会调用 .( 最先调用的方法 .), 只会调用一次 .
- (
void )connection:( NSURLConnection *)connection didReceiveResponse:( NSURLResponse *)response
{
   
NSLog ( @"%@ %@" ,response, [ NSThread currentThread ]);
}


// 接收到 数据 ( 实体内容 ) 的时候就会调用 . 也会调用多次 .
- (
void )connection:( NSURLConnection *)connection didReceiveData:( NSData *)data
{
   
NSLog ( @" 本次接收到 %ld 的数据 %@" ,data. length ,[ NSThread currentThread ]);
   
// 拼接下载好的数据
    [
self . fileData appendData :data];
   
}

// 网络完成之后 ( 数据下载完毕 ), 就会调用 .
- (
void )connectionDidFinishLoading:( NSURLConnection *)connection
{
   
NSLog ( @" 下载完毕 ...%@" ,[ NSThread currentThread ]);
   
   
// 数据拼接完毕 , 保存在本地 .
    [
self . fileData writeToFile : @"/Users/teacher/Desktop/111.zip" atomically : YES ];
   
   
self . fileData = nil ;
}


#pragma NSURLConnectionDownloadDelegate

- (
void )connection:( NSURLConnection *)connection didWriteData:( long long )bytesWritten totalBytesWritten:( long long )totalBytesWritten expectedTotalBytes:( long long ) expectedTotalBytes
{
   
NSLog ( @"-------------------1111" );
}


- (
void )connectionDidFinishDownloading:( NSURLConnection *)connection destinationURL:( NSURL *) destinationURL
{
   
NSLog ( @"-------------%@" ,destinationURL);
}



@end

!!!!内存还是暴涨!监听下载进度条!  需要自己写业务逻辑
内存还是暴涨的解决方法:数据追加 边下载边保存 ( 写入本地 ). 还要保证后续下载的数据追加在之前下载数据的后面 .
第一种方法:数据追加:
1.NSFileHandle:文件操作句柄,用来操作文件内部——>造成的BUG,如果多次下载,会造成下载完毕后的文件变大,需要做业务逻辑
2.NSFileManager:用来操作文件(获取文件信息/删除/移动/复制。。。)
#import "ViewController.h"

@interface ViewController ()< NSURLConnectionDataDelegate >

// 文件下载完毕之后 , 保存的路径 .
@property ( nonatomic , copy ) NSString *filePath;

@end

@implementation ViewController


- (
void )viewDidLoad {
    [
super viewDidLoad ];

   
// 1. 下载过程中 , 内存不能变大 .
   
   
// 边下载边保存 ( 写入本地 ). 还要保证后续下载的数据追加在之前下载数据的后面 .
   
// 数据追加 :
   
// 1. NSFileHandle: 文件操作句柄 , 用来操作文件内部
   
// NSFileManager: 用来操纵文件 ( 获得文件信息 / 删除 / 移动 / 复制 ...)
   
// 如果多次下载 , 会造成下载完毕的文件变大 ...(2/3/4/5 倍增加 .), 需要做业务逻辑处理 .
   
   
// 创建文件句柄
   
// 根据文件路径 , 实例化文件操作句柄 ( 写入 )
   
// 如果传入的路径不存在 , 文件句柄会实例化失败 . nil.
   
// 如果传入的文件路径存在 , 文件句柄会实例化成功 , 并且指向这个需要操作的文件 .
   
NSFileHandle *handle = [ NSFileHandle fileHandleForWritingAtPath : @"" ];
   
   
   
   
// 2. 监听下载进度 .
   
}

- (
void )touchesBegan:( NSSet < UITouch *> *)touches withEvent:( UIEvent *)event
{
   
NSLog ( @"touchesBegan" );
   
   
NSString *urlString = @"http://127.0.0.1/dawenjian.zip" ;
   
   
// 1. 创建请求
   
NSURL *url = [ NSURL URLWithString :urlString];
   
   
NSURLRequest *request = [ NSURLRequest requestWithURL :url];
   
   
// 下载 , 代理 .
   
   
// 创建网络连接 , 设置代理对象
   
NSURLConnection *conn = [[ NSURLConnection alloc ] initWithRequest :request delegate : self ];
   
   
// 代理对象创建完毕之后 , 就会自动调用代理方法 . 不需要手动启动的 .
    [conn
start ];
   
}


#pragma NSURLConnectionDataDelegate
// 接收到 响应头信息的时候就会调用 .( 最先调用的方法 .), 只会调用一次 .
- (
void )connection:( NSURLConnection *)connection didReceiveResponse:( NSURLResponse *)response
{
   
NSLog ( @"%@ %@" ,response, [ NSThread currentThread ]);
   
   
   
// 准备下载文件数据之前 , 实例化文件下载路径 .
   
self . filePath = [ NSString stringWithFormat : @"/Users/teacher/Desktop/%@" ,response. suggestedFilename ];
   
}


// 接收到 数据 ( 实体内容 ) 的时候就会调用 . 也会调用多次 .
- (
void )connection:( NSURLConnection *)connection didReceiveData:( NSData *)data
{
   
NSLog ( @" 本次接收到 %ld 的数据 %@" ,data. length ,[ NSThread currentThread ]);

   
// 实例化文件句柄 , 操纵文件
   
NSFileHandle *handle = [ NSFileHandle fileHandleForWritingAtPath : self . filePath ];
   
   
if (handle) {
       
       
// 往文件后面追加文件内容 .
       
       
// 1. 将文件句柄移动到文件最后 ( 末尾 )
        [handle
seekToEndOfFile ];
       
       
// 2. 追加文件
        [handle
writeData :data];
       
        [handle
closeFile ];
       
    }
else
    {
       
// 第一次实例化路径 ( 创建文件 )
       
// 如果这个路径下有文件了 , 会自动覆盖 ; 如果没有文件 , 会创建一个文件 .
        [data
writeToFile : self . filePath atomically : YES ];
    }
   
}

// 网络完成之后 ( 数据下载完毕 ), 就会调用 .
- (
void )connectionDidFinishLoading:( NSURLConnection *)connection
{
   
NSLog ( @" 下载完毕 ...%@" ,[ NSThread currentThread ]);
}

@end



第二种方法:文件的数据流/输入输出流:NSOutputStream—>负责建立一个“管道”,让数据流顺着这个“管道”流入指定的文件——>造成的BUG,如果多次下载,会造成下载完毕后的文件变大,需要做业务逻辑。
  // 实例化对象
    // 如果这个文件路径不存在,会自动创建一个空文件.如果文件存在,就直接在文件后面追加文件.   // NSOutputStream *stream = [[NSOutputStream alloc] initToFileAtPath:self.filePath append:YES];

NSOutputStream稳定性不如NSFileHandle

步骤:注意,管道式需要手动开启的 open

#import "ViewController.h"

@interface ViewController ()< NSURLConnectionDataDelegate >

// 文件下载完毕之后 , 保存的路径 .
@property ( nonatomic , copy ) NSString *filePath;

// 文件输入输出流管道 .
@property ( nonatomic , strong ) NSOutputStream *stream;

@end

@implementation ViewController
- ( void )touchesBegan:( NSSet < UITouch *> *)touches withEvent:( UIEvent *)event
{
   
NSLog ( @"touchesBegan" );
   
// http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.0.4.dmg
   
   
NSString *urlString = @"http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.0.4.dmg" ;
   
   
// 1. 创建请求
   
NSURL *url = [ NSURL URLWithString :urlString];
   
   
NSURLRequest *request = [ NSURLRequest requestWithURL :url];
   
   
// 下载 , 代理 .
   
   
// 创建网络连接 , 设置代理对象
   
NSURLConnection *conn = [[ NSURLConnection alloc ] initWithRequest :request delegate : self ];
   
   
// 代理对象创建完毕之后 , 就会自动调用代理方法 . 不需要手动启动的 .
    [conn
start ];
   
}


#pragma NSURLConnectionDataDelegate
// 接收到 响应头信息的时候就会调用 .( 最先调用的方法 .), 只会调用一次 .
- (
void )connection:( NSURLConnection *)connection didReceiveResponse:( NSURLResponse *)response
{
   
NSLog ( @"%@ %@" ,response, [ NSThread currentThread ]);
   

   
// 准备下载文件数据之前 , 实例化文件下载路径 .
   
self . filePath = [ NSString stringWithFormat : @"/Users/teacher/Desktop/%@" ,response. suggestedFilename ];
   
   
// 创建管道 .
   
self . stream = [[ NSOutputStream alloc ] initToFileAtPath : self . filePath append : YES ];
   
   
// 管道是需要手动开启的 .
    [
self . stream open ];
}


// 接收到 数据 ( 实体内容 ) 的时候就会调用 . 也会调用多次 .
- (
void )connection:( NSURLConnection *)connection didReceiveData:( NSData *)data
{
   
NSLog ( @" 本次接收到 %ld 的数据 %@" ,data. length ,[ NSThread currentThread ]);
   
   
// 顺着管道 , 流入数据 .
    [
self . stream write :[data bytes ] maxLength :data. length ];
   
   
// NSLog(@"%@",data);

}

// 网络完成之后 ( 数据下载完毕 ), 就会调用 .
- (
void )connectionDidFinishLoading:( NSURLConnection *)connection
{
   
NSLog ( @" 下载完毕 ...%@" ,[ NSThread currentThread ]);
   
   
// 关闭管道 .
    [
self . stream close ];
}



显示问题 )监听下载进度条的解决方法:
1.需要我那件总大小(定义一个属性 =respond。expectedContentLenght),当前下载 的数据量(定义一个属性 = data。length)——>BUG:直接设置进度条,进度条不能平滑是显示下载进度
#import "ViewController.h"

@interface ViewController ()< NSURLConnectionDataDelegate >

// 文件下载完毕之后 , 保存的路径 .
@property ( nonatomic , copy ) NSString *filePath;

// 文件输入输出流管道 .
@property ( nonatomic , strong ) NSOutputStream *stream;

// 文件总大小
@property ( nonatomic , assign ) long long expectedLength;

// 当前已经下载的数据量
@property ( nonatomic , assign ) long long totalBytes;

// 下载进度条
@property ( nonatomic , strong ) UIProgressView *progressView;

@end

@implementation ViewController

-(
UIProgressView *)progressView
{
   
if (! _progressView ) {
       
_progressView = [[ UIProgressView alloc ] initWithFrame : CGRectMake ( 20 , 50 , 335 , 2 )];
       
// 进度条颜色
       
_progressView . tintColor = [ UIColor greenColor ];
       
        [
self . view addSubview : _progressView ];
    }
   
return _progressView ;
}


- (
void )viewDidLoad {
    [
super viewDidLoad ];

   
// 1. 下载过程中 , 内存不能变大 .
   
   
// 2. 监听下载进度 .
   
// 1. 文件总大小 , 当前下载的数据量 .
   
// 直接设置进度条 , 进度条不能平滑显示下载进度 ( 为什么 ?)
   
}

- (
void )touchesBegan:( NSSet < UITouch *> *)touches withEvent:( UIEvent *)event
{
   
NSLog ( @"touchesBegan" );
   
// http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.0.4.dmg
   
//
   
   
NSString *urlString = @"http://127.0.0.1/dawenjian.zip" ;
   
   
// 1. 创建请求
   
NSURL *url = [ NSURL URLWithString :urlString];
   
   
NSURLRequest *request = [ NSURLRequest requestWithURL :url];
   
   
// 下载 , 代理 .
   
   
// 创建网络连接 , 设置代理对象
   
NSURLConnection *conn = [[ NSURLConnection alloc ] initWithRequest :request delegate : self ];
   
   
// 代理对象创建完毕之后 , 就会自动调用代理方法 . 不需要手动启动的 .
    [conn
start ];
   
}


#pragma NSURLConnectionDataDelegate
// 接收到 响应头信息的时候就会调用 .( 最先调用的方法 .), 只会调用一次 .
- (
void )connection:( NSURLConnection *)connection didReceiveResponse:( NSURLResponse *)response
{
   
NSLog ( @"%@ %@" ,response, [ NSThread currentThread ]);
   
   
// 记录文件总大小
   
self . expectedLength = response. expectedContentLength ;
   

   
// 准备下载文件数据之前 , 实例化文件下载路径 .
   
self . filePath = [ NSString stringWithFormat : @"/Users/teacher/Desktop/%@" ,response. suggestedFilename ];
   
   
// 创建管道 .
   
self . stream = [[ NSOutputStream alloc ] initToFileAtPath : self . filePath append : YES ];
   
   
// 管道是需要手动开启的 .
    [
self . stream open ];
}


// 接收到 数据 ( 实体内容 ) 的时候就会调用 . 也会调用多次 .
- (
void )connection:( NSURLConnection *)connection didReceiveData:( NSData *)data
{
  
// NSLog(@" 本次接收到 %ld 的数据 %@",data.length,[NSThread currentThread]);
   
   
// 记录已经下载的文件大小 .
   
self . totalBytes += data. length ;
   
   
NSLog ( @"%lld %lld %@" , self . totalBytes , self . expectedLength ,[ NSThread currentThread ]);
   
   
self . progressView . progress = ( float ) self . totalBytes / self . expectedLength ;
   
   
// 顺着管道 , 流入数据 .
    [
self . stream write :[data bytes ] maxLength :data. length ];
   
   
// NSLog(@"%@",data);

}

// 网络完成之后 ( 数据下载完毕 ), 就会调用 .
- (
void )connectionDidFinishLoading:( NSURLConnection *)connection
{
   
NSLog ( @" 下载完毕 ...%@" ,[ NSThread currentThread ]);
   
   
// 关闭管道 .
    [
self . stream close ];
}

@end

  解决   BUG:直接设置进度条,进度条不能平滑是显示下载进度
下载和刷新进度条都在主线程中,所以把下载进度条放在子线程中操作。优先保证主线程的运行———>还是有坑,解决思路————>关于网络连接放到子线程中处理(还是有坑,因为NSURLConnectDelegate是一个特殊的事件源, 要手动开启运行循环,CFRunLoopRun();先添加事件源,再开启运行循环 )
- ( void )touchesBegan:( NSSet < UITouch *> *)touches withEvent:( UIEvent *)event{
   
//
   
// 1. 下载过程中 , 内存不能变大 .
   
   
// 2. 监听下载进度 .
   
// 1. 文件总大小 , 当前下载的数据量 .
   
// 直接设置进度条 , 进度条不能平滑显示下载进度 ( 为什么 ?)
   
   
// 把网络连接添加到子线程中,就等于把代理添加到了子线程中。 NSUrlConnectionDelegate 是一个特殊的事件源 . 代理方法想要执行 , 必须运行循环来执行 . 手动开启运行循环 : 先添加事件源 , 再开启运行循环 .
 
   
dispatch_async ( dispatch_get_global_queue ( 0 , 0 ), ^{
       
       
       
NSString *urlString = @"http://127.0.0.1/dawenjian.zip" ;
       
       
// 1. 创建请求
       
NSURL *url = [ NSURL URLWithString :urlString];
       
       
NSURLRequest *request = [ NSURLRequest requestWithURL :url];
       
       
// 下载 , 代理 .
       
       
// 创建网络连接 , 设置代理对象
       
NSURLConnection *conn = [[ NSURLConnection alloc ] initWithRequest :request delegate : self ];
       
       
       
// 将代理回调设置子线程。相当于 NSDefaltModes 模式将定时器添加在线程中 . 优先保证主线程的运行 .
        [conn
setDelegateQueue :[[ NSOperationQueue alloc ] init ]];
       
       
       
// 代理对象创建完毕之后 , 就会自动调用代理方法 . 不需要手动启动的 .
        [conn
start ];
       
// NSUrlConnectionDelegate 是一个特殊的事件源 . 代理方法想要执行 , 必须运行循环来执行 .
       
       
// 手动开启运行循环 : 先添加事件源 , 再开启运行循环 .
       
CFRunLoopRun ();
       
       
// [[NSRunLoop currentRunLoop] run];
       
    });
  
}

#pragma mark - NSURLConnectionDataDelegate
//// 接收到 响应头信息的时候就会调用 .( 最先调用的方法 .), 只会调用一次 .
- (
void )connection:( NSURLConnection *)connection didReceiveResponse:( NSURLResponse *)response{
   
      
// 记录文件总大小
   
self . expectedLength = response. expectedContentLength ;
   
   
NSLog ( @" 响应头信息 :%@---%@" ,response,[ NSThread currentThread ]);
   
// // 准备下载文件数据之前 , 实例化文件下载路径 .
   
self . filePath = [ NSString stringWithFormat : @"/Users/zzx/Desktop/%@" ,response. suggestedFilename ];
   
   
// 实例化 管道
   
self . stream = [ NSOutputStream outputStreamToFileAtPath : self . filePath append : YES ];
   
// 开启管道
   
// 管道是需要手动开启的 .
    [
self . stream open ];
   
}
// 接收到 数据 ( 实体内容 ) 的时候就会调用 . 也会调用多次 .
- (
void )connection:( NSURLConnection *)connection didReceiveData:( NSData *)data{
   
//    NSLog(@"%@ ---%@",[NSThread currentThread],data);
   
// 每次调用,都把数据保存到定义的数据属性中
   
NSLog ( @" 本次接收到 %ld 的数据 %@" ,data. length ,[ NSThread currentThread ]);
   
    
// 记录已经下载的文件大小 .
   
self . totalBytes += data. length ;
   
   
dispatch_async ( dispatch_get_main_queue (), ^{
       
       
// 进度条进度
       
self . progress . progress = ( float ) self . totalBytes / self . expectedLength ;
    });
   
   
  
// 顺着管道 , 流入数据 .
    [
self . stream write :[data bytes ] maxLength :data. length ];
   
}
// 网络完成之后 ( 数据下载完毕 ), 就会调用 .
- (
void )connectionDidFinishLoading:( NSURLConnection *)connection
{
   
   
NSLog ( @" 下载完毕 ...%@" ,[ NSThread currentThread ]);
// 关闭管道
    [
self . stream close ];
}


3.BUG——>下载过程中,下载到本地的文件不能变大。
下载业务逻辑
    {
       
1. 本地文件大小 > 服务器文件大小 : 1 > 删除本地文件 2 > 重新开始下载
       
       
2. 本地文件大小 < 服务器文件大小 :
        {
            *
1 如果本地文件大小 = 0 : 直接从 0 开始下载 ( 重新开始下载 ).
           
            *
2 如果本地文件大小 > 0 : 断点续传 :
            {
               
设置 Range 属性 , 告诉服务器断点续传开始的位置 .
               
            Range 格式 :
           
            bytes=x-y   
x 字节开始下载,下载 y 个字节
            bytes=x-    
x 字节开始下载,下载到文件末尾
            bytes=-x     文件开始下载,下载 x 字节
        
               
                设置 Range 属性之后,服务器返回的状态码会变成 206 .



#import "ViewController.h"

@interface ViewController ()<NSURLConnectionDataDelegate>

// 定义一个数据保存到本地的地址属性
@property ( nonatomic , copy ) NSString * filePath;

// 文件输入输出流
@property ( nonatomic , strong ) NSOutputStream * steam;

// 下载进度条
@property ( nonatomic , strong ) UIProgressView * progressView;

// 已经下载的数据量
@property ( nonatomic , assign ) long long totalBtyes;

// 文件总数据量
@property ( nonatomic , assign ) long long expectedBtyesLength;

// 本地文件大小
@property ( nonatomic , assign ) long long localFileLength;

@end

@implementation ViewController



-(
UIProgressView *)progressView{
   
   
if (! _progressView ) {
       
       
_progressView = [[ UIProgressView alloc ] initWithFrame : CGRectMake ( 20 , 50 , 335 , 2 )];
       
       
_progressView . tintColor = [ UIColor redColor ];
       
        [
self . view addSubview : _progressView ];
    }
   
return _progressView ;
}

- (
void )viewDidLoad {
    [
super viewDidLoad ];
   
   
NSString *urlString = @"http://127.0.0.1/dawenjian.zip" ;
   
   
self . filePath = @"/Users/zzx/Desktop/dawenjian" ;
   
//      1. 先检查服务器文件大小 .
    [
self checkServerFileWithUrlString :urlString];
   
       
// 2. 检查本地文件大小
    [
self getFilepathWithUrlString : self . filePath ];
//    主线程中执行 UI 操作
     
self . progressView . progress = ( float ) self . totalBtyes / self . expectedBtyesLength ;
   
}

- (
void )touchesBegan:( NSSet < UITouch *> *)touches withEvent:( UIEvent *)event{
   
   
NSLog ( @"touchesBegan" );
   
   
NSString *urlString = @"http://127.0.0.1/dawenjian.zip" ;
   
   
// 1. 先检查服务器文件大小 .
    [
self checkServerFileWithUrlString :urlString];
   
   
// 2. 检查本地文件大小
    [
self getFilepathWithUrlString : self . filePath ];
   
   
if ( self . localFileLength >= self . expectedBtyesLength ) { // 删除本地文件 , 并且重新开始下载
      
NSLog ( @" 删除本地文件 , 开始新文件的下载 " );
       
       
// 1. 删除本地文件
        [[
NSFileManager defaultManager ] removeItemAtPath : self . filePath error : NULL ];
       
       
// 2. 重新开始下载
        [
self getFilepathWithUrlString :urlString];
       
       
return ;
    }

   
   
if ( self . localFileLength < self . expectedBtyesLength ) { // 本地保存的文件小于服务器的文件
       
       
if ( self . localFileLength > 0 ) {
           
           
NSLog ( @" 断点续传 " );
        }
else
        {
           
NSLog ( @" 重新开始下载 " );
        }
       
// 断点续传 ( 包含了两个过程 : 1. 0 开始下载 2. 从断点开始下载 )
       
dispatch_async ( dispatch_get_global_queue ( 0 , 0 ), ^{
           
           
// 1. 创建请求
           
NSURL *url = [ NSURL URLWithString :urlString];
           
           
NSMutableURLRequest *request = [ NSMutableURLRequest requestWithURL :url];
           
// X 位置 开始 ,下载到 文件末尾
           
NSString * range = [ NSString stringWithFormat : @"bytes=%lld-" , self . localFileLength ];
           
// 告诉服务器,从哪里开始下载
            [request
setValue :range forHTTPHeaderField : @"Range" ];
           
           
// 创建网络连接。设置代理对象
           
NSURLConnection * conn = [[ NSURLConnection alloc ] initWithRequest :request delegate : self ];
           
           
// 将代理回调设置子线程 . 相当于 NSDefaltModes 模式将定时器添加在线程中 . 优先保证主线程的运行 .
            [conn
setDelegateQueue :[[ NSOperationQueue alloc ] init ]];
           
            [conn
start ];
          
// 手动开启运行循环 : 先添加事件源 , 再开启运行循环 .
           
CFRunLoopRun ();
        });

    }
}

#pragma mark - 检查服务器文件大小 ( 同步 还是 异步 ?)  -- 同步请求 .
- ( void )checkServerFileWithUrlString:( NSString *)urlString{
   
    
// 只获取 response , 不要 data.
   
   
   
NSURL *url = [ NSURL URLWithString :urlString];
   
   
NSMutableURLRequest *request = [ NSMutableURLRequest requestWithURL :url];
   
// 设置请求方法 :
   
// HEAD http 请求规定的方法 . file 协议没有这个方法 .
   
// 这个方法只获得响应头信息 , 不会获取具体的文件内容 . 速度比较快 , 一般是使用同步请求发送的 .
      request.
HTTPMethod = @"HEAD" ;
   
   
NSURLResponse * response = nil ;
   
    [
NSURLConnection sendSynchronousRequest :request returningResponse :&response error : NULL ];
   
   
NSLog ( @"%lld" ,response. expectedContentLength );
   
   
self . expectedBtyesLength = response. expectedContentLength ;
 
}
#pragma mark - 发送本地请求,获知本地文件大小

-(
void )getFilepathWithUrlString:( NSString *) urlString{
   
// 利用 NSFileManager 来检查本地文件大小 .
   
   
// 获取文件管理器对象
   
// 检查这个路径下 , 是否有这个文件 .
 
BOOL is_YES = [[ NSFileManager defaultManager ] fileExistsAtPath : self . filePath ];
   
   
if (is_YES) {
       
       
NSLog ( @" 本地文件存在 " );
       
       
// 获取本地文件信息 , 文件信息中不包含文件类型 .
       
       
NSDictionary * dict = [[ NSFileManager defaultManager ] attributesOfItemAtPath : self . filePath error : NULL ];
       
// 直接通过字典属性取值 , 得不到数字 ( 得到是对象 ), 没法对比 .
       
NSLog ( @"%@" ,dict);
       
       
// 记录本地文件的大小
       
self . localFileLength = [dict[ NSFileSize ] integerValue ];
       
    }
else {
       
       
self . localFileLength = 0 ;
    
       
NSLog ( @" 本地文件不存在 " );
    }
   
   
}



#pragma mark -NSURLConnectionDataDelegate
// 接收响应头信息时会调用,只调用一次
- (
void )connection:( NSURLConnection *)connection didReceiveResponse:( NSURLResponse *)response{
   
   
    
NSLog ( @"%@ %@" ,response, [ NSThread currentThread ]);
   
// 获取总数据量
//    self.expectedBtyesLength = response.expectedContentLength;
   
//    实例化接收数据的地址 地址名称随机最优
//    self.filePath = [NSString stringWithFormat:@"/Users/zzx/Desktop/%@",response.suggestedFilename];
   
// 实例化管道
   
self . steam = [ NSOutputStream outputStreamToFileAtPath : self . filePath append : YES ];
   
// 打开管道
    [
self . steam open ];
   
}

// 发送网络请求 , 下载数据 .
- (
void )getServerDataWithUrlString:( NSString *)urlString{
   
   
   
// 异步网络请求
   
dispatch_async ( dispatch_get_global_queue ( 0 , 0 ), ^{
       
       
NSURL * url  =[ NSURL URLWithString :urlString];
       
       
NSURLRequest * request = [ NSURLRequest requestWithURL :url];
       
       
// 设置请求代理
       
NSURLConnection * conn = [ NSURLConnection connectionWithRequest :request delegate : self ];
       
       
// 对代理回调在子线程中执行
        [conn
setDelegateQueue :[[ NSOperationQueue alloc init ]];
       
       
// 开启代理方法 ,也会自动开启
        [conn
start ];
       
       
// 手动开启运行循环 : 先添加事件源 , 再开启运行循环 .
       
CFRunLoopRun ();
    });
   
   
   
}
// 接收数据时会调用,持续调用,直到完成
- (
void )connection:( NSURLConnection *)connection didReceiveData:( NSData *)data{
    
// 记录已经下载的文件大小 .
   
self . totalBtyes += data. length ;
   
   
   
NSLog ( @"%lld %lld %@" , self . localFileLength , self . expectedBtyesLength ,[ NSThread currentThread ]);

   
dispatch_async ( dispatch_get_main_queue (), ^{
       
       
self . progressView . progress = ( float ) self . totalBtyes / self . expectedBtyesLength ;
    });
   
   
// 顺着管道 , 流入数据 .
    [
self . steam write :[data bytes ] maxLength :data. length ];
}
// 下载结束后调用,
- (
void )connectionDidFinishLoading:( NSURLConnection *)connection{
   
   
// 关闭管道
    [
self . steam close ];
}

@end



大文件:   会造成内存暴涨 { 先将文件下载到内存中 (data), 然后再写入磁盘 }. 为了防止内存暴涨 , 不能直接使用 block 回调 .
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值