在空闲时间自己编写了一个简单的iOS下载管理器。该管理器实现如下功能:
1、能够支持正常的下载,暂停,继续操作。
2、支持断点续传,实现暂停执行继续操作后,依然能正常将文件下载完成。
3、实现实时状态回调,下载进度,速度,一目了然。
准备工作:压缩文件
遇到的主要问题: 拼接到内存中的数据峰值太大,会导致app闪退.
解决办法:
一.(1)用NSFileHandle解决占用内存过大问题(下载一点 写入沙盒一点)
#import "ViewController.h"
@interface ViewController ()<NSURLConnectionDataDelegate>
@property(nonatomic,copy)NSString *filePath;
/**
* 服务器上面的文件的总大小
*/
@property(nonatomic,assign)long long expectedContentLength;
/**
* 已经接收到的文件的大小
*/
@property(nonatomic,assign)long long hasReceivedContentLength;
/**
* 文件的管道
*/
@property(nonatomic,strong)NSFileHandle *writeFileHandle;
@end
@implementation ViewController
#pragma mark - 懒加载
- (NSString *)filePath{
if (_filePath==nil) {
//获取到Cache目录
NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
_filePath = [cache stringByAppendingPathComponent:@"dh.zip"];
}
return _filePath;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSLog(@"%@",NSHomeDirectory());
}
/**
这个地方不太好
1.没有进度
2.进度峰值太大,会导致我们的app闪退
*/
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//1.URL
NSURL *url = [NSURL URLWithString:@"http://localhost/video.zip"];
//2.NSURLRequest
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//3.只要调用了类方法就会自动发起请求
NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];
}
#pragma mark - DownLoadDelegate
/*
这个只会调用一次
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
NSLog(@"下载开始!!!");
self.expectedContentLength = response.expectedContentLength;
//1.创建一个0KB的文件,空文件
/**
NSFileManger 做大事,创建,删除,移动我们的文件,并且还可以获取文件的信息
NSFileHandle 做小事,专门用来,流入数据
*/
NSFileManager *manager = [NSFileManager defaultManager];
BOOL createSuccess = [manager createFileAtPath:self.filePath contents:nil attributes:NULL];
//2.创建管道
if (createSuccess) {
NSLog(@"创建文件成功!!!");
self.writeFileHandle = [NSFileHandle fileHandleForWritingAtPath:self.filePath];
}
}
/**
这个玩意儿会调用多次
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
//NSLog(@"%zd",data.length);
//1.拼接文件的总大小
self.hasReceivedContentLength += data.length;
float progress = (float)self.hasReceivedContentLength / (float)self.expectedContentLength;
NSLog(@"progress==%f",progress);
//2.接收到一点,就写入到沙盒指定的文件里面去一点
//2.1把文件指针,移到已经下载完毕的文件的末尾
[self.writeFileHandle seekToEndOfFile];
//2.2 将新获取到的data 拼接到已经下载完毕的文件的末尾
[self.writeFileHandle writeData:data];
}
/**
只会调用一次
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
NSLog(@"下载完毕!!!");
//需要将管道关闭
[self.writeFileHandle closeFile];
}
@end
(2)用NSOutputStream解决占用内存过大问题
#import "ViewController.h"
@interface ViewController ()<NSURLConnectionDataDelegate>
@property(nonatomic,copy)NSString *filePath;
/**
* 服务器上面的文件的总大小
*/
@property(nonatomic,assign)long long expectedContentLength;
/**
* 已经接收到的文件的大小
*/
@property(nonatomic,assign)long long hasReceivedContentLength;
/**
* 输出,输入是以内存为参照物的
输出,就是将内存中的东西,输出到沙盒
输入,就是将沙盒中的东西输入到内存
*/
@property(nonatomic,strong)NSOutputStream *outputStream;
@end
@implementation ViewController
#pragma mark - 懒加载
- (NSString *)filePath{
if (_filePath==nil) {
//获取到Cache目录
NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
_filePath = [cache stringByAppendingPathComponent:@"dh.zip"];
}
return _filePath;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSLog(@"%@",NSHomeDirectory());
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//1.URL
NSURL *url = [NSURL URLWithString:@"http://localhost/video.zip"];
//2.NSURLRequest
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//3.只要调用了类方法就会自动发起请求
NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];
//开启子线程的NSRunloop,下载比较特殊,只要下载完毕之后,他的NSRunloop会自动停止
[[NSRunLoop currentRunLoop] run];
});
}
#pragma mark - DownLoadDelegate
/*
这个只会调用一次
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
NSLog(@"下载开始!!!");
self.expectedContentLength = response.expectedContentLength;
//1.创建输出流
self.outputStream = [NSOutputStream outputStreamToFileAtPath:self.filePath append:YES];
//2.打开流
#warning 打开流
[self.outputStream open];
}
/**
这个玩意儿会调用多次
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
//NSLog(@"%zd",data.length);
//1.拼接文件的总大小
self.hasReceivedContentLength += data.length;
float progress = (float)self.hasReceivedContentLength / (float)self.expectedContentLength;
NSLog(@"progress==%f %@",progress,[NSThread currentThread]);
//获取一点,就写入沙盒一点
[self.outputStream write:data.bytes maxLength:data.length];
}
/**
只会调用一次
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
NSLog(@"下载完毕!!!");
//关闭流
[self.outputStream close];
}
@end
二.文件下载进度(暂停和恢复)
#import "ViewController.h"
@interface ViewController ()<NSURLConnectionDataDelegate>
@property(nonatomic,copy)NSString *filePath;
/**
* 服务器上面的文件的总大小
*/
@property(nonatomic,assign)long long expectedContentLength;
/**
* 已经接收到的文件的大小
*/
@property(nonatomic,assign)long long hasReceivedContentLength;
/**
* 输出,输入是以内存为参照物的
输出,就是将内存中的东西,输出到沙盒
输入,就是将沙盒中的东西输入到内存
*/
@property(nonatomic,strong)NSOutputStream *outputStream;
/**
* connection
*/
@property(nonatomic,strong)NSURLConnection *connection;
@end
@implementation ViewController
#pragma mark - 懒加载
- (NSString *)filePath{
if (_filePath==nil) {
//获取到Cache目录
NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
_filePath = [cache stringByAppendingPathComponent:@"dh.zip"];
}
return _filePath;
}
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@",NSHomeDirectory());
}
//开始下载
- (IBAction)start:(id)sender {
NSLog(@"%s",__func__);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//获取到我们的文件的总大小
[self getFileLenght];
//1.URL
NSURL *url = [NSURL URLWithString:@"http://localhost/video.zip"];
//2.NSURLRequest
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//3.只要调用了类方法就会自动发起请求
self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
//开启我们子线程的NSRunloop,下载比较特殊,只要下载完毕之后,他的NSRunloop会自动停止
[[NSRunLoop currentRunLoop] run];
});
}
//暂停
- (IBAction)pause:(id)sender {
//取消
//一旦调用了这个方法,connnection就废了
[self.connection cancel];
}
//恢复
- (IBAction)resume:(id)sender {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//先得到文件的大小
[self getFileLenght];
//1.URL
NSURL *url = [NSURL URLWithString:@"http://localhost/video.zip"];
//2.NSURLRequest
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//2.1 设置请求头,告诉服务器一些额外的信息
NSString *rangeValue = [NSString stringWithFormat:@"bytes=%lld-",self.hasReceivedContentLength];
[request setValue:rangeValue forHTTPHeaderField:@"Range"];
//3.只要调用了类方法就会自动发起请求
self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
//开启我们子线程的NSRunloop,下载比较特殊,只要下载完毕之后,他的NSRunloop会自动停止
[[NSRunLoop currentRunLoop] run];
});
}
- (void)getFileLenght{
//1.URL
NSURL *url = [NSURL URLWithString:@"http://localhost/video.zip"];
//2.NSURLRequest
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//如果只是获取文件的信息,而不是把数据download下来
request.HTTPMethod = @"HEAD";
//3.发送请求去服务器上面,获取文件的信息
NSURLResponse *response;
NSData *data =[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
NSLog(@"getFileLenght %zd",data.length);
//4.设置给我们文件的总长度
self.expectedContentLength = response.expectedContentLength;
NSLog(@"文件的总大小 %zd",self.expectedContentLength);
}
#pragma mark - DownLoadDelegate
/*
这个只会调用一次
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
NSLog(@"下载开始!!!");
NSLog(@"文件总大小----------%lld",response.expectedContentLength);
//1.创建输出流
self.outputStream = [NSOutputStream outputStreamToFileAtPath:self.filePath append:YES];
//2.打开流
#warning 打开流
[self.outputStream open];
}
/**
这个玩意儿会调用多次
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
//NSLog(@"%zd",data.length);
//1.拼接文件的总大小
self.hasReceivedContentLength += data.length;
float progress = (float)self.hasReceivedContentLength / (float)self.expectedContentLength;
NSLog(@"progress==%f %@",progress,[NSThread currentThread]);
//获取一点,就写入沙盒一点
[self.outputStream write:data.bytes maxLength:data.length];
}
/**
只会调用一次
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
NSLog(@"下载完毕!!!");
//关闭流
[self.outputStream close];
}
@end
三.封装进度条控件
.h文件
/**
XCode6开始,可以动态的在我们Xib里面调试
*/
#import <UIKit/UIKit.h>
IB_DESIGNABLE
@interface ProgressView : UIView
/**
进度必须在0到1之间
*/
@property(nonatomic,assign) IBInspectable float progress;
/**
* 外环
*/
@property(nonatomic,assign) IBInspectable CGFloat waiHuanLineWidth;
@property(nonatomic,strong) IBInspectable UIColor *huanColor;
@end
.m文件
#import "ProgressView.h"
@implementation ProgressView
- (void)setProgress:(float)progress{
_progress = progress;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
//得到宽度和高度
CGFloat width = rect.size.width;
CGFloat height = rect.size.height;
//1.中点
CGPoint centerPoint = CGPointMake(width *0.5, height*0.5);
//2.外环
//7.线框
if (self.waiHuanLineWidth==0) {
self.waiHuanLineWidth = 5;
}
//3.半径
CGFloat waiHuanRadius = width * 0.5 - self.waiHuanLineWidth*0.5;
//4.画圆
UIBezierPath *waiHuanBezierPath = [UIBezierPath bezierPathWithArcCenter:centerPoint radius:waiHuanRadius startAngle:0 endAngle:M_PI*2 clockwise:YES];
//5.颜色
if (self.huanColor == nil) {
self.huanColor = [UIColor blueColor];
}
//6.设置颜色
[self.huanColor set];
//7.设置外环的宽度
waiHuanBezierPath.lineWidth = self.waiHuanLineWidth;
//8.画出来
[waiHuanBezierPath stroke];
//画矩形
CGFloat cubeWidth = 20;
CGFloat cubeHeight = 20;
CGFloat cubeX = width *0.5 - cubeWidth*0.5;
CGFloat cubeY = height *0.5 - cubeHeight*0.5;
UIBezierPath *cubeBezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(cubeX, cubeY, cubeWidth, cubeHeight)];
[self.huanColor set];
//画出来我们中间的矩形
[cubeBezierPath fill];
//画那个动的圆形
CGFloat neiYuanWidth = self.waiHuanLineWidth;
CGFloat neiYuanRadius = width *0.5 - self.waiHuanLineWidth - neiYuanWidth*0.5;
CGFloat endAngle= M_PI * 2 * self.progress + (- M_PI_2);
UIBezierPath *neiYuanBezierPath = [UIBezierPath bezierPathWithArcCenter:centerPoint radius:neiYuanRadius startAngle:- M_PI_2 endAngle:endAngle clockwise:YES];
[self.huanColor set];
//设置线框
neiYuanBezierPath.lineWidth = neiYuanWidth;
//最后画出来
[neiYuanBezierPath stroke];
}
@end
四.由于时间关系做的不太精致,还有待改善,感兴趣的小伙伴们可以对其进行封装,使其更加的完善!