iOS中的下载管理器(支持断点续传)

在空闲时间自己编写了一个简单的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

四.由于时间关系做的不太精致,还有待改善,感兴趣的小伙伴们可以对其进行封装,使其更加的完善!

原文地址:https://www.cnblogs.com/donghaoios/p/5102715.html