为了账号安全,请及时绑定邮箱和手机立即绑定

用NSURLConnection封装一个断点续传后台下载的轻量级下载工具_part1

标签:
iOS

摘要

  • 本文讲述,用NSURLConnection封装一个断点下载,支持后台下载的完整的下载工具。

    效果图

    效果图

用法示例

[[FGGDownloadManager shredManager] downloadWithUrlString:model.url toPath:model.destinationPath process:^(float progress, NSString *sizeString, NSString *speedString) {
                //更新进度条的进度值
                weakCell.progressView.progress=progress;
                //更新进度值文字
                weakCell.progressLabel.text=[NSString stringWithFormat:@"%.2f%%",progress*100];
                //更新文件已下载的大小
                weakCell.sizeLabel.text=sizeString;
                //显示网速
                weakCell.speedLabel.text=speedString;
                if(speedString)
                    weakCell.speedLabel.hidden=NO;

            } completion:^{
                [sender setTitle:@"完成" forState:UIControlStateNormal];
                sender.enabled=NO;
                weakCell.speedLabel.hidden=YES;
                UIAlertView *alert=[[UIAlertView alloc]initWithTitle:@"提示" message:[NSString stringWithFormat:@"%@下载完成✅",model.name] delegate:nil cancelButtonTitle:@"好" otherButtonTitles:nil, nil];
                [alert show];

            } failure:^(NSError *error) {
                [[FGGDownloadManager shredManager] cancelDownloadTask:model.url];
                [sender setTitle:@"恢复" forState:UIControlStateNormal];
                weakCell.speedLabel.hidden=YES;
                UIAlertView *alert=[[UIAlertView alloc]initWithTitle:@"Error" message:error.localizedDescription delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
                [alert show];
}];

思路

  • 搞一个下载类,负责一个下载任务;
  • 搞一个下载管理类,负责下载队列;
  • 在程序进入后台时,开启后台下载;
  • 在程序被终结时取消所有下载并保存下载进度;
  • 当程序启动时,加载上次下载进度;

下载类 FGGDownloader.h

FGGDownloader遵循NSURLConnection的一些协议:
@interface FGGDownloader : NSObject<NSURLConnectionDataDelegate,NSURLConnectionDelegate>
接下来定义三个代码块:

typedef void (^ProcessHandle)(float progress,NSString *sizeString,NSString *speedString);
typedef void (^CompletionHandle)();
typedef void (^FailureHandle)(NSError *error);

然后声明三个只读属性:

//下载过程中回调的代码块,会多次调用
@property(nonatomic,copy,readonly)ProcessHandle process;
//下载完成回调的代码块
@property(nonatomic,copy,readonly)CompletionHandle completion;
//下载失败的回调代码块
@property(nonatomic,copy,readonly)FailureHandle failure;

写一个快速实例的类方法:

+(instancetype)downloader;

搞一个下载的接口:

/**
 *  断点下载
 *
 *  @param urlString        下载的链接
 *  @param destinationPath  下载的文件的保存路径
 *  @param  process         下载过程中回调的代码块,会多次调用
 *  @param  completion      下载完成回调的代码块
 *  @param  failure         下载失败的回调代码块
 */
-(void)downloadWithUrlString:(NSString *)urlString
                      toPath:(NSString *)destinationPath
                     process:(ProcessHandle)process
                  completion:(CompletionHandle)completion
                     failure:(FailureHandle)failure;

搞一个取消下载的方法:

/**
 *  取消下载
 */
-(void)cancel;

程序启动的时候要加载上一次的下载进度:

/**
 * 获取上一次的下载进度
 */
+(float)lastProgress:(NSString *)url;

需要在界面上显示文件下载了的大小,文件总大小:

/**获取文件已下载的大小和总大小,格式为:已经下载的大小/文件总大小,如:12.00M/100.00M。
 *
 * @param url 下载链接
 */
+(NSString *)filesSize:(NSString *)url;

搞三个通知,因为下载管理类要用:

/**
 *  下载完成的通知名
 */
static NSString *const FGGDownloadTaskDidFinishDownloadingNotification=@"FGGDownloadTaskDidFinishDownloadingNotification";
/**
 *  系统存储空间不足的通知名
 */
static NSString *const FGGInsufficientSystemSpaceNotification=@"FGGInsufficientSystemSpaceNotification";
/**
 *  下载进度改变的通知
 */
static NSString *const FGGProgressDidChangeNotificaiton=@"FGGProgressDidChangeNotificaiton";

下载类的实现 FGGDownloader.m

大文件下载,要遵循低内存占用,因此我们要有一个文件读写句柄:NSFileHandle,以及一些储放下载位置、下载路径、链接等一些东西的成员变量。
<pre>
说明一下,_timer是为了获取网速近似值,没0.5秒计算文件增长的大小,取网速平均值.
这里有个假设:假设文件读写不占用时间,而在这0.5秒内文件的增量直接反应网速。
</pre>

@implementation FGGDownloader{

    NSString        *_url_string;
    NSString        *_destination_path;
    NSFileHandle    *_writeHandle;
    NSURLConnection *_con;
    NSUInteger       _lastSize;
    NSUInteger       _growth;
    NSTimer         *_timer;
}

快速实例化的类方法:

+(instancetype)downloader{

    return [[[self class] alloc]init];
}

初始化方法:

-(instancetype)init{

    if(self=[super init]){

        //每0.5秒计算一次文件大小增加部分的尺寸
        _timer=[NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(getGrowthSize) userInfo:nil repeats:YES];
    }
    return self;
}

计算文件没0.5秒的增量,以计算网速近似值

//计算一次文件大小增加部分的尺寸,以计算网速近似值
-(void)getGrowthSize
{
    NSUInteger size=[[[[NSFileManager defaultManager] attributesOfItemAtPath:_destination_path error:nil] objectForKey:NSFileSize] integerValue];
    _growth=size-_lastSize;
    _lastSize=size;
}

关键方法,下载接口方法

/**
 *  断点下载
 *
 *  @param urlString        下载的链接
 *  @param destinationPath  下载的文件的保存路径
 *  @param  process         下载过程中回调的代码块,会多次调用
 *  @param  completion      下载完成回调的代码块
 *  @param  failure         下载失败的回调代码块
 */
-(void)downloadWithUrlString:(NSString *)urlString toPath:(NSString *)destinationPath process:(ProcessHandle)process completion:(CompletionHandle)completion failure:(FailureHandle)failure{

    if(urlString&&destinationPath){

        _url_string=urlString;
        _destination_path=destinationPath;
        _process=process;
        _completion=completion;
        _failure=failure;

        NSURL *url=[NSURL URLWithString:urlString];
        NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url];
        NSFileManager *fileManager=[NSFileManager defaultManager];
        BOOL fileExist=[fileManager fileExistsAtPath:destinationPath];
        if(fileExist){

            NSUInteger length=[[[fileManager attributesOfItemAtPath:destinationPath error:nil] objectForKey:NSFileSize] integerValue];
            NSString *rangeString=[NSString stringWithFormat:@"bytes=%ld-",length];
            [request setValue:rangeString forHTTPHeaderField:@"Range"];
        }
        _con=[NSURLConnection connectionWithRequest:request delegate:self];
    }
}

取消下载:

/**
 *  取消下载
 */
-(void)cancel{

    [self.con cancel];
    self.con=nil;
    if(_timer){

        [_timer invalidate];
        _timer=nil;
    }
}

程序每次启动都要获取上次的下载进度:

/**
 * 获取上一次的下载进度
 */
+(float)lastProgress:(NSString *)url{

    if(url)
        return [[NSUserDefaults standardUserDefaults]floatForKey:[NSString stringWithFormat:@"%@progress",url]];
    return 0.0;
}

通过下载链接url获取文件的大小信息(当前大小/总大小 组成的字符串)

/**获取文件已下载的大小和总大小,格式为:已经下载的大小/文件总大小,如:12.00M/100.00M
 */
+(NSString *)filesSize:(NSString *)url{

    NSString *totalLebgthKey=[NSString stringWithFormat:@"%@totalLength",url];
    NSUserDefaults *usd=[NSUserDefaults standardUserDefaults];
    NSUInteger totalLength=[usd integerForKey:totalLebgthKey];
    if(totalLength==0){

        return @"0.00K/0.00K";
    }
    NSString *progressKey=[NSString stringWithFormat:@"%@progress",url];
    float progress=[[NSUserDefaults standardUserDefaults] floatForKey:progressKey];
    NSUInteger currentLength=progress*totalLength;

    NSString *currentSize=[self convertSize:currentLength];
    NSString *totalSize=[self convertSize:totalLength];
    return [NSString stringWithFormat:@"%@/%@",currentSize,totalSize];
}

下面是个工具方法,把文件的长度(字节)转成字符串

/**
 * 计算缓存的占用存储大小
 *
 * @prama length  文件大小
 */
+(NSString *)convertSize:(NSUInteger)length
{
    if(length<1024)
        return [NSString stringWithFormat:@"%ldB",(NSUInteger)length];
    else if(length>=1024&&length<1024*1024)
        return [NSString stringWithFormat:@"%.0fK",(float)length/1024];
    else if(length >=1024*1024&&length<1024*1024*1024)
        return [NSString stringWithFormat:@"%.1fM",(float)length/(1024*1024)];
    else
        return [NSString stringWithFormat:@"%.1fG",(float)length/(1024*1024*1024)];
}

下载过程可能会存储空间不足,若不做处理,会导致crash,因此每次句柄写文件时,都要判断存储空间是否足够:

/**
 *  获取系统可用存储空间
 *
 *  @return 系统空用存储空间,单位:字节
 */
-(NSUInteger)systemFreeSpace{

    NSString *docPath=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSDictionary *dict=[[NSFileManager defaultManager] attributesOfFileSystemForPath:docPath error:nil];
    return [[dict objectForKey:NSFileSystemFreeSize] integerValue];
}

下面是NSURLConnect的代理和数据源

下载失败,回调error block:

#pragma mark - NSURLConnection
/**
 * 下载失败
 */
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{

    if(_failure)
        _failure(error);
}

在接受到响应请求的代理方法中存储文件中大小,以及初始化文件读写句柄_writeHandle:

/**
 * 接收到响应请求
 */
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{

    NSString *key=[NSString stringWithFormat:@"%@totalLength",_url_string];
    NSUserDefaults *usd=[NSUserDefaults standardUserDefaults];
    NSUInteger totalLength=[usd integerForKey:key];
    if(totalLength==0){

        [usd setInteger:response.expectedContentLength forKey:key];
        [usd synchronize];
    }
    NSFileManager *fileManager=[NSFileManager defaultManager];
    BOOL fileExist=[fileManager fileExistsAtPath:_destination_path];
    if(!fileExist)
        [fileManager createFileAtPath:_destination_path contents:nil attributes:nil];
    _writeHandle=[NSFileHandle fileHandleForWritingAtPath:_destination_path];
}

下载过程的接收数据的回调,处理计算网速、下载进度、文件大小信息,存档当前大小等操作,最后回调<b><i>网速</b></i>,文件大小/总大小字符串</b></i>,<b><i>下载进度</b></i>信息:

/**
 * 下载过程,会多次调用
 */
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{

    [_writeHandle seekToEndOfFile];

    NSUInteger freeSpace=[self systemFreeSpace];
    if(freeSpace<1024*1024*20){

        UIAlertController *alertController=[UIAlertController alertControllerWithTitle:@"提示" message:@"系统可用存储空间不足20M" preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *confirm=[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil];
        [alertController addAction:confirm];
        [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alertController animated:YES completion:nil];
        //发送系统存储空间不足的通知,用户可自行注册该通知,收到通知时,暂停下载,并更新界面
        [[NSNotificationCenter defaultCenter] postNotificationName:FGGInsufficientSystemSpaceNotification object:nil userInfo:@{@"urlString":_url_string}];
        return;
    }
    [_writeHandle writeData:data];
    NSUInteger length=[[[[NSFileManager defaultManager] attributesOfItemAtPath:_destination_path error:nil] objectForKey:NSFileSize] integerValue];
    NSString *key=[NSString stringWithFormat:@"%@totalLength",_url_string];
    NSUInteger totalLength=[[NSUserDefaults standardUserDefaults] integerForKey:key];

    //计算下载进度
    float progress=(float)length/totalLength;

    [[NSUserDefaults standardUserDefaults]setFloat:progress forKey:[NSString stringWithFormat:@"%@progress",_url_string]];
    [[NSUserDefaults standardUserDefaults] synchronize];

    //获取文件大小,格式为:格式为:已经下载的大小/文件总大小,如:12.00M/100.00M
    NSString *sizeString=[FGGDownloader filesSize:_url_string];

    //发送进度改变的通知(一般情况下不需要用到,只有在触发下载与显示下载进度在不同界面的时候才会用到)
    NSDictionary *userInfo=@{@"url":_url_string,@"progress":@(progress),@"sizeString":sizeString};
    [[NSNotificationCenter defaultCenter] postNotificationName:FGGProgressDidChangeNotificaiton object:nil userInfo:userInfo];

    //计算网速
    NSString *speedString=@"0.00Kb/s";
    NSString *growString=[FGGDownloader convertSize:_growth*(1.0/0.1)];
    speedString=[NSString stringWithFormat:@"%@/s",growString];

    //回调下载过程中的代码块
    if(_process)
        _process(progress,sizeString,speedString);
}

下载完成,发送通知,回调下载完成的代码块:

/**
 * 下载完成
 */
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{

    [[NSNotificationCenter defaultCenter] postNotificationName:FGGDownloadTaskDidFinishDownloadingNotification object:nil userInfo:@{@"urlString":_url_string}];
    if(_completion)
        _completion();
}
点击查看更多内容
3人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
1
获赞与收藏
92

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消