1.使用GET请求直接去服务器下载文件
1.1 内存暴涨
1.2 下载进度无法检测
2.解决内存暴涨的问题 : 使用NSURLConnection的代理去实现下载
3.获取文件下载进度 : 使用NSURLConnection的代理去实现下载
文件的总大小 : response
文件的当前下载的总大小 : 拼接每次下载的数据的大小
4.解决代理方法执行的线程的问题
4.1 获取数据的代理方法频繁调用,不能在主线程执行
4.2 我们查看设置代理的方法的头文件,发现,设置代理的线程和代理方法调用的线程是一样的
4.3 为了让代理方法在子线程执行,我们需要子线程中设置代理
4.4 要让子线程中的代理方法执行,需要子线程开启运行循环,子线程的运行循环默认不开启,我们需要手动开启.
5.实现压缩包的沙盒缓存 (千万不要实现内存缓存,因为压缩包非常大)
NSMutableData
NSFileHandle
NSOutputStream OK
6.实现断点下载,在实现断点下载之前,我们做了HEAD请求的演练
6.1 HEAD请求的作用 : 只能获取到响应头,不能获取响应体;
6.2 异步的HEAD请求
我们可以在获取响应头的同时,还可以做其他事情
6.3 同步的HEAD请求
只有同步任务执行完,我们才能做其他事情
为什么同步的HEAD请求没有回调?
因为任务是同步的,任务执行完,我们"自然而然"就可以得到数据(不需要异步监听任务什么时候执行完)
为什么同步任务有返回值?而异步任务没有?
因为异步任务不确定什么时候能够执行完,所以需要传入代码块,什么时候执行完,就什么时候回调数据
为什么同步任务需要定义输出参数?
因为一个方法只有一个返回值,如果你的同步方法需要返回多个值,其他的就需要定义成输出参数
总结:
a).同步的请求有返回值
b).异步的请求没有回调
c).参数
参数1(输出参数) : 响应对象的地址
参数2(输出参数) : 错误信息的地址
d).为什么同步的请求有data返回值?
因为同步的任务,不要做异步的监听,代码执行完就有值,所以data直接可以放在方法的返回值里面
e).为什么response要设计成输出参数?
因为同步任务不需要做异步监听,而且方法的返回有且只能有一个,没办法同时返回多个,如果一个方法中有多个需 要返回的值,其他的就需要定义成输出参数
7.分析了断点下载的原理和步骤
7.1 获取服务器文件总大小
7.2 获取本地文件总大小
7.3 本地文件大小和服务器文件大小进行比较
7.4 提示 : 比较的时候,我们需要这个比较的结果.
7.5 为什么需要比较的结果?
因为比较的结果,我们可以拿来设置到requestM里面的Range字段,告诉服务器从哪个位置开始给客户端续传数据
7.6 比较的过程
本地文件 == 服务器文件 (文件已经正确的下载完,再次点击下载按钮时,直接return)
本地文件 > 服务器文件 (文件下载错误,删除,从0开始下载)
本地文件 < 服务器文件 (文件没有下载完,需要继续下载,就是断点下载 / 断点续传)
8.下载过程的回调
文件下载完成的回调
文件下载进度的回调
文件下载出错的回调
在使用代码块时,我们需要注意循环引用
9.文件下载器管理类
9.1 设计成单例模式;因为文件下载器有可能在APP的很多地方都要用到
9.2 管理文件下载
9.3 管理文件暂停下载
9.4 解决重复下载的问题
设计下载器缓存池
文件下载完需要移除下载器
文件下载出错需要移除下载器
文件下载被暂停后需要移除下载器
10.自定义NSOperation实现了文件下载器
10.1 在重写main方法时,发送HEAD请求获取响应头信息
10.2 在重写main方法时,比较了本地文件和服务器文件大小
10.3 在重写main方法时,设置了NSURLConnection的代理
10.4 现在FileDownloader继承自NSOperation的,那么在使用的时候,需要把它添加到队列
// 三.————————–NSURLConnectionDataDelegate—————————-
代理方法介绍
获取响应
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
获取本次接收的文件数据,一点儿一点儿接收文件数据
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
文件下载完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
监听下载是否出错 : NSURLConnectionDelegate(父协议)的代理方法
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
data.length//获取二进制数据的长度
观察结果_问题1
文件下载是耗时操作且调用频繁,就要留意这个方法执行的线程了,如果是在主线程中执行的.就会卡死主界面.打印线程发现,这个代理方法是在主线程中执行的.
// 四.——————————NSFileHandle———————————-
NSFileHandle 使用步骤
1.获取文件保存的路径
2.创建NSFileHandle对象.
3.判断要下载的文件是否存在.
4.如果文件不存在就新建一个文件,并保存数据.
5.如果文件存在就继续在当前文件下保存.并确定从哪儿开始保存.
6.开始保存.
7.保存完了以后要关闭NSFileHandle.
使用步骤:每写入一次就需要创建一个handle对象
// 文件保存的路径
NSString *filePath = @"/Users/zhangjie/Desktop/sogou.zip";
// 1. 创建NSFileHandle对象.
NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:filePath];
// 2. 判断要下载的文件是否存在.
if (handle == nil) {
// 3. 如果文件不存在就新建一个文件,并保存数据.
// 这中方式也是可以方便快捷的创建一个文件的
[data writeToFile:filePath atomically:YES];
} else {
// 4. 如果文件存在就继续在当前文件下保存.并确定从哪儿开始保存.
// 每次都从文件的末尾开始保存,就不会出错.
[handle seekToEndOfFile];
// 5. 开始保存
[handle writeData:data];
// 6. 保存完了以后要关闭NSFileHandle.
[handle closeFile];
}
// 五.——————————NSOutputStream———————————-
NSOutputStream使用步骤 :
1.获取文件保存的路径.
2.客户端跟服务器建立好链接,得到响应体之后就创建好管道,并打开管道.
3.每次接收一点数据就传输一点数据.
4.文件下载完成,关闭管道.
// 文件保存的路径
NSString *filePath = @"/Users/zhangjie/Desktop/sogou.zip";
// 创建管道
self.stream = [NSOutputStream outputStreamToFileAtPath:filePath append:YES];
// 打开管道
[self.stream open];
// 传输数据 : 内存-->沙盒,下载一点儿就传输一点儿
[self.stream write:data.bytes maxLength:data.length];
// 文件下载完成就关闭管道
[self.stream close];
data.bytes//将二进制数据分成一个一个字节
// 六.———————————-断点续传————————————–
断点续传实现思路
先判断本地文件是否存在,如果文件不存在,从0开始下载.
如果本地文件存在
1.判断本地文件 == 服务器文件,不需要下载.
2.判断本地文件 < 服务器文件,从当前位置开始下载.
3.判断本地文件 > 服务器文件,删除之前的文件,从0开始下载.
断点续传实现步骤 :
1.获取服务器文件大小
2.获取本地文件大小
3.比较两个文件大小
4.得到比较的结果再发送GET请求利用代理下载文件
注意 : 这四步是要有顺序的.后面一步都是以前面那一步的结果为依据的.所以要同步执行.
提示 : 拿到比较的结果,即当前文件的大小(0,下了部分,已经下完).然后可以告诉服务器从哪开始去下载文件.
我们比较的目的就是为了得到本地当前文件的大小,有了本地当前文件的大小,就知道从哪儿继续下载了
获取服务器文件大小
获取服务器文件大小就是发送同步的HEAD请求.获取到响应头,并从中取出服务器文件大小.
如果发送异步请求,有可能文件已经在比较了,但是服务器的文件大小还没有获取到.
提示 : 拿到响应头之后,还可以顺便取出文件名,用于拼接文件保存到沙盒的路径.
HEAD请求简介
不获取响应体data,只获取响应头NSURLResponse.
一般在真正下载文件之前先同步获取文件的大小.
同步HEAD请求使用场景
当我们要下载一个文件,在下载之前需要知道这个文件的一些基本信息时.需要用到HEAD请求
比如先拿到文件总大小,计算下载进度和断点续传.
比如先拿到文件的文件名称,就可以在下载完成之前确定好文件保存的路径.
// 获取本地文件大小
NSFileManager *manager = [NSFileManager defaultManager];
NSDictionary *attrs = [manager attributesOfItemAtPath:self.filePath error:NULL];//文件信息
[manager fileExistsAtPath:self.filePath];//文件是否存在
// 移除文件
[manager removeItemAtPath:self.filePath error:NULL];
在网页文件下载中,请求头里面有个Range字段.
Range字段就是告诉服务器从文件的第几个字节开始下载.
Range: bytes=x-y 表示从x字节下载到y字节
Range: bytes=x- 表示从x字节下载到文件末尾.断点续传
Range: bytes=-y 表示从头下载到y字节
断点续传的核心
发送GET请求
设置请求头中的Range字段,告诉服务器从哪儿开始继续下载.
// 设置请求头,告诉服务器从哪儿开始下载文件
[requestM setValue:[NSString stringWithFormat:@”bytes=%lld-“,self.currentLength] forHTTPHeaderField:@”Range”];
文件下载_暂停下载
暂停下载,其实就是取消NSURLConnection的网络下载操作.调用他的cancel方法.
继续下载,其实就是断点续传.不需要再另外实现.
//暂停其实是将下载链接杀死,链接没了就不会执行下载的代理方法,如果要继续下载任务,就需要重新创建下载链接对象
[self.connection cancel];
目前存在的问题 : 当我们在文件下载的过程中,不断的点击”下载”按钮,即不断的创建新的下载任务时.会出现多下载任务下载这同一个文件的情况.
解决重复下载的思路
创建下载管理类(单例设计模式),管理所有的下载操作.
管理类里面定义一个缓存池,专门缓存已经创建的下载器.
在新建下载器之前判断缓存池里有没有,有就不再新建下载器,没有就新建下载器.
1. 获取服务器文件大小是发送的同步HEAD请求,需要在子线程中执行的.
2. 获取本地文件大小,并与服务器文件比较大小是I/O操作也是可以在子线程执行的.
3. 设置代理也是要在子线程执行的
结论 : 以上三步,要有序的在子线程中执行.
将FileDownloader的继承改成NSOperation,直接就将FileDownloader设计成子线程的环境.这样就不需要再另外的分别开启子线程了.
实现步骤
- 将FileDownloader的继承改成NSOperation.
- 重写NSOperation的main方法.
- 在main方法里面实现获取服务器文件大小–>获取本地文件大小,并与服务器文件比较大小–>设置代理.
当我们把FileDownloader这个类改成NSOperation时.文件的暂停下载不光可以取消connection的网络下载操作,还可以取消Operation.
[connection cancel] : 取消的是真正网络下载操作.
[Operation cancel] : 可以取消跟下载相关的所有代码,即可以减少下载时代码的执行量.