音乐(文件)断点下载

这篇文章介绍音乐等文件的下载,支持断点续传。

我们需要创建两个类
HYDownLoader:音乐下载的主类,可以进行新建下载、暂停下载、取消下载等。
HYFileTool:文件管理类,主要为HYDownLoader服务,可以判断文件是否存在、移动文件等。

一、文件管理工具类HYFileTool

HYFileTool类比较简单,直接上代码,.h文件已经把方法备注得比较清楚了
.h文件

#import <Foundation/Foundation.h>
@interface HYFileTool : NSObject


/**
 判断文件是否存在

 @param filePath 文件路径
 @return 是否存在
 */
+(BOOL)fileExists:(NSString *)filePath;


/**
 获取文件大小

 @param filePath 文件路径
 @return 文件大小
 */
+(long long)fileSize:(NSString *)filePath;


/**
 移动文件到新的路径

 @param fromPath 文件的原路径
 @param toPath 文件的新路径
 */
+(void)moveFile:(NSString *)fromPath toPath:(NSString *)toPath;


/**
 删除文件

 @param filePath 文件路径
 */
+(void)removeFile:(NSString*)filePath;

@end

.m文件

#import "HYFileTool.h"

@implementation HYFileTool

+(BOOL)fileExists:(NSString *)filePath{
    if (filePath.length == 0) {
        
    }
    return [[NSFileManager defaultManager] fileExistsAtPath:filePath];
}

+(long long)fileSize:(NSString *)filePath{
    
    if (![self fileExists:filePath]) {
        return 0;
    }
    
    NSDictionary *fileInfo = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
    return [fileInfo[NSFileSize] longLongValue];
}

+(void)moveFile:(NSString *)fromPath toPath:(NSString *)toPath{
    if (![self fileExists:fromPath]) {
        return;
    }
    [[NSFileManager defaultManager] moveItemAtPath:fromPath toPath:toPath error:nil];
}

+(void)removeFile:(NSString*)filePath{
    [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
}

@end

二、下载主类HYDownLoader.h
在HYDownLoader.h中先定义下载状态的枚举

typedef NS_ENUM(NSUInteger,HYDownloadState){
    HYDownloadStatePause,
    HYDownloadStateDowning,
    HYDownloadStateSuccess,
    HYDownloadStateFail
};

以及各种状态下的Block回调

typedef void(^DownLoadInfoType)(long long totalSize);
typedef void(^ProgressBlockType)(float progress);
typedef void(^SuccesswBlockType)(NSString *path);
typedef void(^FailBlockType)(void);
typedef void(^StateChangeBlockType)(HYDownloadState state);

然后定义对应的属性

/**
 下载状态
 */
@property(nonatomic,assign,readonly) HYDownloadState state;

/**
 下载进度
 */
@property(nonatomic,assign,readonly) float progress;

/**
 下载信息回调
 */
@property(nonatomic,copy) DownLoadInfoType downLoadInfo;

/**
 下载状态改变回调
 */
@property(nonatomic,copy) StateChangeBlockType stateChangeInfo;

/**
 下载进度改变回调
 */
@property(nonatomic,copy) ProgressBlockType progressChange;

/**
 下载成功回调
 */
@property(nonatomic,copy) SuccesswBlockType successBlock;

/**
 下载失败回调
 */
@property(nonatomic,copy) FailBlockType failBlock;

以及方法

/**
 下载文件

 @param url 下载文件的网络地址
 */
-(void)downLoader:(NSURL *)url;

/**
 下载文件

 @param url 下载文件的网络地址
 @param downLoadInfo 下载信息block
 @param progressBlock 下载进度block
 @param successBlock 下载成功block
 @param failedBlock 下载失败block
 */
-(void)downLoader:(NSURL *)url downLoadInfo:(DownLoadInfoType)downLoadInfo progress:(ProgressBlockType)progressBlock success:(SuccesswBlockType)successBlock failed:(FailBlockType)failedBlock;


/**
 暂停当前任务
 */
-(void)pauseCurrentTask;


/**
 取消当前任务
 */
-(void)cancelCurrentTask;

/**
 取消并清除当前任务
 */
-(void)cancelAndClean;

三、HYDownLoader.m中的变量

HYDownLoader.m中定义内部方法需要的变量

@interface HYDownLoader()<NSURLSessionDataDelegate>{
    long long _tempSize;//已下载文件大小
    long long _totalSize;//文件总大小
}

@property (nonatomic,strong) NSURLSession *session;
@property (nonatomic,weak) NSURLSessionDataTask *dataTask;

/**
 下载文件完成后的路径
 */
@property (nonatomic,copy) NSString *downloadedPath;
/**
 下载文件时的路径
 */
@property (nonatomic,copy) NSString *downloadingPath;
/**
 写入文件的流
 */
@property (nonatomic,strong) NSOutputStream *outputStream;

@end

对应的get和set方法

-(NSURLSession*)session{
    if (_session == nil) {
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        _session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    return _session;
}

-(void)setState:(HYDownloadState)state{
    if (_state == state) {
        return;
    }
    _state = state;
    
    if (self.stateChangeInfo) {
        self.stateChangeInfo(_state);
    }
    if (state == HYDownloadStateSuccess && self.successBlock) {
        self.successBlock(_downloadedPath);
    }
    if (state == HYDownloadStateFail && self.failBlock) {
        self.failBlock();
    }
}

-(void)setProgress:(float)progress{
    _progress = progress;
    
    if (self.progressChange) {
        self.progressChange(progress);
    }
}

其中在setState中,我们根据不同的下载状态回调对应的Block通知外界。

四、HYDownLoader.m的主要下载方法downLoader:和downloadWithURL:offset:

-(void)downLoader:(NSURL *)url{

    //如果任务存在,当前只是暂停,则继续下载
    if ([url isEqual:self.dataTask.originalRequest.URL] && self.state == HYDownloadStatePause) {
        [self resumeCurrenttask];
        return;
    }
    
    //1.文件的存放
    //下载时存放到temp(此目录用于存放临时文件,app退出时会被清理)
    //下载完成后移动到cache(iTunes不会备份此目录,此目录下文件不会在app退出时删除)
    NSString *fileName = url.lastPathComponent;
    self.downloadedPath = [kCachePath stringByAppendingPathComponent:fileName];
    self.downloadingPath = [kTmpPath stringByAppendingPathComponent:fileName];
    
    //1.判断url地址对应的资源是否已下载完成
    //1.1如果已完成,则返回相关信息
    if([HYFileTool fileExists:self.downloadedPath]){
        NSLog(@"已下载完成(文件已存在)");
        self.state = HYDownloadStateSuccess;
        return;
    }
    
    [self downloadWithURL:url offset:0];

    
    //2.否则检查临时文件是否存在
    //2.1若存在,以当前已存在文件大小,作为开始字节请求资源。
    if ([HYFileTool fileExists:self.downloadingPath]) {
        //获取本地文件大小(已下载部分)
        _tempSize = [HYFileTool fileSize:self.downloadingPath];
        [self downloadWithURL:url offset:_tempSize];
        return;
    }
    
    // 本地大小 == 总大小 则移动到cache文件夹
    // 本地大小 > 总大小  则删除本地缓存,重新从0开始下载
    // 本地大小 < 总大小  从本地大小开始下载
    
    //2.2 不存在,则从0字节开始请求资源
    [self downloadWithURL:url offset:0];
    
}

再写带有回调block的方法,此时只要给各个block赋值, 并调用上面下载的主方法:

-(void)downLoader:(NSURL *)url downLoadInfo:(DownLoadInfoType)downLoadInfo progress:(ProgressBlockType)progressBlock success:(SuccesswBlockType)successBlock failed:(FailBlockType)failedBlock{
    self.downLoadInfo = downLoadInfo;
    self.progressChange = progressBlock;
    self.successBlock = successBlock;
    self.failBlock  = failedBlock;
    [self downLoader:url];
}

downLoader:方法只是对当前状态做了判断,下载的核心其实是downloadWithURL:offset:方法。使用系统的NSURLSession进行网络请求。 HTTP1.1 协议(RFC2616)开始支持获取文件的部分内容,这为并行下载以及断点续传提供了技术支持。它通过在 Header 里两个参数实现的,客户端发请求时对应的是 Range ,服务器端响应时对应的是 Content-Range。所以断点续传的请求的关键在于,给request的请求头设置Range,表示只请求offset后面的数据。

/**
 根据开始字节,请求资源

 @param url 下载url
 @param offset 开始字节
 */
- (void)downloadWithURL:(NSURL *)url offset:(long long)offset{
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:0];
    [request setValue:[NSString stringWithFormat:@"bytes=%lld-",offset] forHTTPHeaderField:@"Range"];
    self.dataTask = [self.session dataTaskWithRequest:request];
    [self resumeCurrenttask];
}

NSURLSession的请求回调为代理方式。HYDownLoader遵循NSURLSessionDataDelegate代理,并完成其中的三个代理回调:

#pragma mark - 协议

//接收到响应头
- (void)URLSession:(NSURLSession *)session dataTask:(nonnull NSURLSessionDataTask *)dataTask didReceiveResponse:(nonnull NSHTTPURLResponse *)response completionHandler:(nonnull void (^)(NSURLSessionResponseDisposition))completionHandler{
    NSLog(@"%@",response);
    _totalSize = [response.allHeaderFields[@"Content-Length"] longLongValue];
    NSString *contentRangeStr = response.allHeaderFields[@"Content-Range"];
    if (contentRangeStr.length > 0) {
        _totalSize = [[[contentRangeStr componentsSeparatedByString:@"/"] lastObject] longLongValue];
    }
    self.downLoadInfo(_totalSize);
    
    if (_tempSize == _totalSize) {
        //文件移动到完成文件夹
        NSLog(@"下载完成,移动文件到完成文件夹");
        [HYFileTool moveFile:_downloadingPath toPath:_downloadedPath];
        completionHandler(NSURLSessionResponseCancel);
        self.state = HYDownloadStateSuccess;
        return;
    }
    
    if (_tempSize > _totalSize) {
        //删除临时缓存
        [HYFileTool removeFile:self.downloadingPath];
        //重新下载
        [self downLoader:response.URL];
        //取消请求
        completionHandler(NSURLSessionResponseCancel);
    }
    
    //继续接受数据
    self.state = HYDownloadStateDowning;
    self.outputStream = [NSOutputStream outputStreamToFileAtPath:self.downloadingPath append:YES];
    completionHandler(NSURLSessionResponseAllow);
}

//继续接收数据
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{
    
    _tempSize += data.length;
    self.progress = 1.0 * _tempSize / _totalSize;
    
    [self.outputStream write:data.bytes maxLength:data.length];
    NSLog(@"接受数据");
}

//请求结束
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
    NSLog(@"接收完成");
    if (error == nil) {
        [HYFileTool moveFile:self.downloadingPath toPath:self.downloadedPath];
        self.state = HYDownloadStateSuccess;
    }else{
        if(error.code == -999){
            NSLog(@"取消下载");
            self.state = HYDownloadStatePause;
        }else{
            NSLog(@"下载错误%@",error);
            self.state = HYDownloadStateFail;
        }
    }
    [self.outputStream close];
}

其中的核心点我们详细解释下:

  1. 在didReceiveResponse接受到头信息后,通过completionHandler回调,决定对后续内容的操作,如取消请求或继续接收数据
  2. 通过outputStream流的方式写入文件。
  3. 文件接收中止后会回调didCompleteWithError:方法,这时候有三种情况:正常完成、用户取消、异常中止等,根据error可以分别判断。

完整代码:
https://github.com/dolacmeng/HYDownLoader

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值