基本思路:
-
判断本地文件,如果本地文件存在要判断文件的大小
-
如果没有本地文件,下载
-
如果本地文件存在,发送head请求获取服务器文件大小
-
本地文件大小 == 服务器文件大小,不下载
-
本地文件大小 < 服务器文件大小,从之前的位置开始下载
-
本地文件大小 > 服务器文件大小,删除本地文件,重新下载
-
-
检查服务器文件
获取服务器上的文件信息
//获取服务器上的文件大小和文件名
- (void)checkServerInfo:(NSURL *)url {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"HEAD";
NSURLResponse *response = nil;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
self.expectedContentLength = response.expectedContentLength;
self.filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename];
}
检查本地文件
-
如果没有文件 返回0, 从头下载
-
如果文件大小 > 服务器文件大小 返回0 删除本地文件 从头下载
-
如果文件大小 == 服务器文件大小 返回文件大小 如果大小相等,不用下载
-
如果文件大小 < 服务器文件大小 返回文件大小 从之前的位置开始下载
-
- (long long)checkLocalInfo {
NSFileManager *fileManger = [NSFileManager defaultManager];
//检查文件是否已存在
if (![fileManger fileExistsAtPath:self.filePath]) {
return 0;
}
//获取本地文件的大小
NSDictionary *fileAttrs = [fileManger attributesOfItemAtPath:self.filePath error:NULL];
long long fileSize = fileAttrs.fileSize;
if (fileSize > self.expectedContentLength) {
[fileManger removeItemAtPath:self.filePath error:NULL];
return 0;
}
return fileSize;
}
下载
-
如果本地文件和服务器文件大小相等,不下载
-
long long fileSize = [self checkLocalInfo];
if (fileSize == self.expectedContentLength) {
NSLog(@"文件已经下载");
return;
}
-
从指定偏移处开始下载
-
range 取值
-
bytes=x-y 从x字节开始下载,下载到y字节
bytes=x- 从x字节开始下载,下载到最后
bytes=-x 从0字节开始下载,下载到x字节
-
-
//从指定偏移处下载文件
- (void)downloadWithUrl:(NSURL *)url offset:(long long)offset {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
/*
range 取值
bytes=x-y 从x字节开始下载,下载到y字节
bytes=x- 从x字节开始下载,下载到最后
bytes=-x 从0字节开始下载,下载到x字节
*/
[request setValue:[NSString stringWithFormat:@"bytes=%lld",offset] forHTTPHeaderField:@"range"];
NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
}
异步下载
-
默认代理方法都在主线程上执行的,下载会卡死
-
异步下载
-
NSURLConnection 的代理方法,想在子线程上执行的话必须开启消息循环
-
把下载方法中的所有代码都放在异步队列中执行
-
在指定位置处开启消息循环,消息循环的模式必须是default模式
-
- (void)downloadWithUrl:(NSURL *)url offset:(long long)offset {
[[NSOperationQueue new] addOperationWithBlock:^{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
/*
range 取值
bytes=x-y 从x字节开始下载,下载到y字节
bytes=x- 从x字节开始下载,下载到最后
bytes=-x 从0字节开始下载,下载到x字节
*/
[request setValue:[NSString stringWithFormat:@"bytes=%lld-",offset] forHTTPHeaderField:@"range"];
NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
//启动消息循环
[[NSRunLoop currentRunLoop] run];
}];
}
-
下载完成后的回调
-
下载完成后或出错之后要在主界面做提示,现在所有的下载操作都封装在Downloader这个类中,学习SDWebImage中的做法,给下载操作传入需要的block,当下载完成或出错的时候调用
-
修改Download二头文件中的下载方法,增加需要的block,进度,完成,出错的block
-
修改downloader.m中的实现方法,因为具体的进度,完成,出错都是在URLConnection的代理方法实现的,所以传入block后需要定义属性接收
-
定义block的属性
-
@property (nonatomic, copy) void(^successBlock)(NSString *path);
@property (nonatomic, copy) void(^processBlock)(float process);
@property (nonatomic, copy) void(^errorBlock)(NSError *error);
-
下载方法中给block属性赋值
-
- (void)downloadWithUrlString:(NSString *)urlStr success:(void (^)(NSString *))successBlock process:(void (^)(float))processBlock error:(void (^)(NSError *))errorBlock {
self.successBlock = successBlock;
self.processBlock = processBlock;
self.errorBlock = errorBlock;
-
对应的位置调用回调方法
-
URLConnection的下载方法中调用进度的回调,在当前子线程中执行
-
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.stream write:data.bytes maxLength:data.length];
self.currentContentLength += data.length;
float process = self.currentContentLength * 1.0 / self.expectedContentLength;
if (self.processBlock) {
self.processBlock(process);
}
}
-
URLConnection的下载完成方法中调用完成的回调,下载完成会回归主线程调用,在主线程更新界面
-
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.stream close];
if (self.successBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
self.successBlock(self.filePath);
});
}
}
-
URLConnection的下载出错的方法中调用出错的回调,在那个线程执行由调用者决定
-
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self.stream close];
if (self.errorBlock) {
self.errorBlock(error);
}
}
-
如果文件已经存在,也要调用完成的方法
-
if (fileSize == self.expectedContentLength) {
NSLog(@"文件已经下载");
if (self.successBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
self.successBlock(self.filePath);
});
}
return;
}
-
controller中调用
-
下载进度提示
-
界面上放置一个自定义按钮,设置大小
-
创建按钮的自定义类 (按钮必须是custom的 系统默认的模式button在重绘时会按钮会跟被点击一样,会不断闪烁)
-
定义一个progress的属性,把进度传过来
-
每当给progress属性赋值的时候调用setneeddisplay 重绘
-
//重写progress的set方法
- (void)setProgress:(float)progress {
_progress = progress;
[self setTitle:[NSString stringWithFormat:@"0.2f%%%",progress * 100] forState:UIControlStateNormal];
[self setNeedsDisplay];
}
-
重写drawRect方法,根据progress画圆
-
#define kLINEWIDTH 5
- (void)drawRect:(CGRect)rect {
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint center = CGPointMake(self.bounds.size.width * 0.5, self.bounds.size.height * 0.5);
CGFloat radius = MIN(center.x, center.y) - kLINEWIDTH;
CGFloat startA = -M_PI_2;
CGFloat endA = startA + self.progress * 2 * M_PI;
[path addArcWithCenter:center radius:radius startAngle:startA endAngle:endA clockwise:YES];
path.lineCapStyle = kCGLineCapRound;
path.lineWidth = kLINEWIDTH;
[[UIColor orangeColor] setStroke];
[path stroke];
}
-
controller中显示进度
-
controller 中在下载进度的回调中调用
-
process:^(float process) {
dispatch_async(dispatch_get_main_queue(), ^{
self.progressView.progress = process;
});
}
-
在storyboard中设置自定义类的属性
-
-
IB
-
-
-
-
暂停下载
-
暂停下载就是调用connection的cancel方法
-
- (void)pause {
[self.conn cancel];
}
代码重构
-
多点击屏幕几次,这个时候会不停的下载同一个文件
-
创建一个下载的单例的管理类
-
通过管理类缓存下载操作,解决重复下载同一个文件
-
下载管理类和缓存池
-
创建下载的管理类DownloaderManger单例
-
static id instance = nil;
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [super allocWithZone:zone];
});
return instance;
}
+ (instancetype)sharedDownloaderManger {
return [[self alloc] init];
}
-
下载,调用下载器的下载方法
-
定义缓存池,当下载开始,把下载器缓存起来
-
下载之前先判断缓存池中是否有此下载操作
-
下载完成或失败后,从缓存池移除下载操作-----最终解决重复下载的问题
-
- (void)pauseWithUrlString:(NSString *)urlStr {
SMHDownloader *downloader = self.dictCache[urlStr];
[downloader pause];
[self.dictCache removeObjectForKey:urlStr];
}
- (NSMutableDictionary *)dictCache {
if (_dictCache == nil) {
_dictCache = [NSMutableDictionary dictionaryWithCapacity:5];
}
return _dictCache;
}
- (void)downloadWithUrlString:(NSString *)urlStr success:(void (^)(NSString *))successBlock process:(void (^)(float))processBlock error:(void (^)(NSError *))errorBlock {
if (self.dictCache[urlStr]) {
NSLog(@"正在下载");
return;
}
SMHDownloader *downloader = [[SMHDownloader alloc] init];
[self.dictCache setObject:downloader forKey:urlStr];
[downloader downloadWithUrlString:urlStr success:^(NSString *path) {
[self.dictCache removeObjectForKey:urlStr];
if (successBlock) {
successBlock(path);
}
} process:processBlock error:^(NSError *error) {
[self.dictCache removeObjectForKey:urlStr];
if (errorBlock) {
errorBlock(error);
}
}];
}
-
Downloader 改成NSOpration
-
改成NSOpration的好处:
-
可以设置最大并发数,限制下载文件的个数
-
可以设置依赖,让一个下载在另一个下载后面执行
-
可以暂停正在下载的任务
-
- (void)main {
NSURL *url = [NSURL URLWithString:self.urlStr];
[self checkServerInfo:url];
long long fileSize = [self checkLocalInfo];
if (fileSize == self.expectedContentLength) {
NSLog(@"文件已经下载");
if (self.successBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
self.successBlock(self.filePath);
});
}
return;
}
self.currentContentLength = fileSize;
[self downloadWithUrl:url offset:fileSize];
}
+ (instancetype)downloaderWithUrlString:(NSString *)urlStr success:(void (^)(NSString *))successBlock process:(void (^)(float))processBlock error:(void (^)(NSError *))errorBlock {
SMHDownloader *downloader = [[self alloc] init];
downloader.successBlock = successBlock;
downloader.processBlock = processBlock;
downloader.errorBlock = errorBlock;
downloader.urlStr = urlStr;
return downloader;
}
-
取消下载操作
-
- (void)pauseWithUrlString:(NSString *)urlStr {
SMHDownloader *downloader = self.dictCache[urlStr];
if (downloader == nil) {
NSLog(@"没有此操作");
return;
}
[downloader pause];
[self.dictCache removeObjectForKey:urlStr];
}
-
下载操作还要取消正在执行的操作
-
在main方法的比较耗时的操作后面加上下面代码
-
//取消正在下载的操作
if (self.isCancelled) {
return;
}