iOS之断点下载,使用NSURLSession简单封装

最近公司需要做个文件管理的功能模块,刚交到博主手上时,头都大了。因为没做过这方面的东西,只好咬牙加班,并请教某位大神,指点了一下,清楚研究方向,找了网上大量资料,最后实现简单的封装。

上代码:.h文件

#import <Foundation/Foundation.h>

@interface DocDownloader : NSObject

/**
 *  创建断点续传管理对象,启动下载请求
 *
 *  @param url          文件资源地址
 *  @param targetPath   文件存放路径
 *  @param success      文件下载成功的回调块
 *  @param failure      文件下载失败的回调块
 *  @param progress     文件下载进度的回调块
 *
 *  @return 断点续传管理对象
 *
 */
+(DocDownloader*)resumeManagerWithURL:(NSURL*)url 
                              targetPath:(NSString*)targetPath
                                 success:(void (^)())success
                                 failure:(void (^)(NSError *error))failure
                                progress:(void (^)(long long totalReceivedContentLength, long long totalContentLength))progress;

/**
 *  启动断点续传下载请求(普通的静态下载链接或GET请求)
 */
-(void)start;

/**
 *  启动断点续传下载请求(POST请求)
 *  
 *  @param params  POST的内容
 */
-(void)startWithParams:(NSString *)params;

/**
 *  取消断点续传下载请求
 */
-(void)cancel;

.m文件

#import "DocDownloader.h"

typedef void (^completionBlock)();
typedef void (^progressBlock)();

@interface DocDownloader ()<NSURLSessionDelegate, NSURLSessionTaskDelegate>

@property (nonatomic, strong) NSURLSession *session;    //注意一个session只能有一个请求任务
@property(nonatomic, readwrite, retain) NSError *error; //请求出错
@property(nonatomic, readwrite, copy) completionBlock completionBlock;
@property(nonatomic, readwrite, copy) progressBlock progressBlock;

@property (nonatomic, strong) NSURL *url;           //文件资源地址
@property (nonatomic, strong) NSString *targetPath; //文件存放路径
@property long long totalContentLength;             //文件总大小
@property long long totalReceivedContentLength;     //已下载大小

/**
 *  设置成功、失败回调block
 *
 *  @param success 成功回调block
 *  @param failure 失败回调block
 */
- (void)setCompletionBlockWithSuccess:(void (^)())success
                              failure:(void (^)(NSError *error))failure;

/**
 *  设置进度回调block
 *
 *  @param progress
 */
-(void)setProgressBlockWithProgress:(void (^)(long long totalReceivedContentLength, long long totalContentLength))progress;

/**
 *  获取文件大小
 *  @param path 文件路径
 *  @return 文件大小
 *
 */
- (long long)fileSizeForPath:(NSString *)path;

@end

@implementation DocDownloader

/**
 *  设置成功、失败回调block
 *
 *  @param success 成功回调block
 *  @param failure 失败回调block
 */
- (void)setCompletionBlockWithSuccess:(void (^)())success
                              failure:(void (^)(NSError *error))failure{
    
    __weak typeof(self) weakSelf = self;
    self.completionBlock = ^ {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            
            if (weakSelf.error) {
                if (failure) {
                    failure(weakSelf.error);
                }
            } else {
                if (success) {
                    success();
                }
            }
            
        });
    };
}

/**
 *  设置进度回调block
 *
 *  @param progress
 */
-(void)setProgressBlockWithProgress:(void (^)(long long totalReceivedContentLength, long long totalContentLength))progress{
    
    __weak typeof(self) weakSelf = self;
    self.progressBlock = ^{
        
        dispatch_async(dispatch_get_main_queue(), ^{
            
            progress(weakSelf.totalReceivedContentLength, weakSelf.totalContentLength);
        });
    };
}

/**
 *  获取文件大小
 *  @param path 文件路径
 *  @return 文件大小
 *
 */
- (long long)fileSizeForPath:(NSString *)path {
    
    long long fileSize = 0;
    NSFileManager *fileManager = [NSFileManager new]; // not thread safe
    if ([fileManager fileExistsAtPath:path]) {
        NSError *error = nil;
        NSDictionary *fileDict = [fileManager attributesOfItemAtPath:path error:&error];
        if (!error && fileDict) {
            fileSize = [fileDict fileSize];
        }
    }
    return fileSize;
}

/**
 *  创建断点续传管理对象,启动下载请求
 *
 *  @param url          文件资源地址
 *  @param targetPath   文件存放路径
 *  @param success      文件下载成功的回调块
 *  @param failure      文件下载失败的回调块
 *  @param progress     文件下载进度的回调块
 *
 *  @return 断点续传管理对象
 *
 */
+(DocDownloader*)resumeManagerWithURL:(NSURL*)url
                              targetPath:(NSString*)targetPath
                                 success:(void (^)())success
                                 failure:(void (^)(NSError *error))failure
                                progress:(void (^)(long long totalReceivedContentLength, long long totalContentLength))progress{
    
    DocDownloader *manager = [[DocDownloader alloc]init];
    
    manager.url = url;
    manager.targetPath = targetPath;
    [manager setCompletionBlockWithSuccess:success failure:failure];
    [manager setProgressBlockWithProgress:progress];
    
    manager.totalContentLength = 0;
    manager.totalReceivedContentLength = 0;
    
    return manager;
}

/**
 *  启动断点续传下载请求(普通的静态下载链接或GET请求)
 */
-(void)start{
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:self.url];
    long long downloadedBytes = self.totalReceivedContentLength = [self fileSizeForPath:self.targetPath];
    if (downloadedBytes > 0) {
        NSString *requestRange = [NSString stringWithFormat:@"bytes=%llu-", downloadedBytes];
        [request setValue:requestRange forHTTPHeaderField:@"Range"];
    }else{
        int fileDescriptor = open([self.targetPath UTF8String], O_CREAT | O_EXCL | O_RDWR, 0666);
        if (fileDescriptor > 0) {
            close(fileDescriptor);
        }
    }
    NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:queue];
    NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request];
    [dataTask resume];
}

/**
 *  启动断点续传下载请求(POST请求)
 *
 *  @param params  POST的内容
 */
-(void)startWithParams:(NSString *)params{
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:self.url];
    [request setHTTPMethod:@"POST"];
    [request setHTTPBody:[params dataUsingEncoding:NSUTF8StringEncoding]];
    long long downloadedBytes = self.totalReceivedContentLength = [self fileSizeForPath:self.targetPath];
    if (downloadedBytes > 0) {
        
        NSString *requestRange = [NSString stringWithFormat:@"bytes=%llu-", downloadedBytes];
        [request setValue:requestRange forHTTPHeaderField:@"Range"];
    }else{
        
        int fileDescriptor = open([self.targetPath UTF8String], O_CREAT | O_EXCL | O_RDWR, 0666);
        if (fileDescriptor > 0) {
            close(fileDescriptor);
        }
    }
    NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:queue];
    NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request];
    [dataTask resume];
}

/**
 *  取消断点续传下载请求
 */
-(void)cancel{
    if (self.session) {
        [self.session invalidateAndCancel];
        self.session = nil;
    }
}

#pragma mark -- NSURLSessionDelegate
/* The last message a session delegate receives.  A session will only become
 * invalid because of a systemic error or when it has been
 * explicitly invalidated, in which case the error parameter will be nil.
 */
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error{
    NSLog(@"didBecomeInvalidWithError");
}

#pragma mark -- NSURLSessionTaskDelegate
/* Sent as the last message related to a specific task.  Error may be
 * nil, which implies that no error occurred and this task is complete.
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error{
    NSLog(@"didCompleteWithError");
    if (error == nil && self.error == nil) {
        self.completionBlock();
    }else if (error != nil){
        if (error.code != -999) {
            self.error = error;
            self.completionBlock();
        }
    }else if (self.error != nil){
        self.completionBlock();
    }
}

#pragma mark -- NSURLSessionDataDelegate
/* Sent when data is available for the delegate to consume.  It is
 * assumed that the delegate will retain and not copy the data.  As
 * the data may be discontiguous, you should use
 * [NSData enumerateByteRangesUsingBlock:] to access it.
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data{
    //NSLog(@"dataLength = %lu",(unsigned long)data.length);
    //根据status code的不同,做相应的处理
    NSHTTPURLResponse *response = (NSHTTPURLResponse*)dataTask.response;
    NSLog(@"response = %@",response);
    if (response.statusCode == 200) {
        NSString *contentRange = [response.allHeaderFields valueForKey:@"Content-Length"];
        self.totalContentLength = [contentRange longLongValue];
    }else if (response.statusCode == 206){
        NSString *contentRange = [response.allHeaderFields valueForKey:@"Content-Range"];
        if ([contentRange hasPrefix:@"bytes"]) {
            NSArray *bytes = [contentRange componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" -/"]];
            if ([bytes count] == 4) {
                self.totalContentLength = [[bytes objectAtIndex:3] longLongValue];
            }
        }
    }else if (response.statusCode == 416){
        NSString *contentRange = [response.allHeaderFields valueForKey:@"Content-Range"];
        if ([contentRange hasPrefix:@"bytes"]) {
            NSArray *bytes = [contentRange componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" -/"]];
            if ([bytes count] == 3) {
                self.totalContentLength = [[bytes objectAtIndex:2] longLongValue];
                if (self.totalReceivedContentLength == self.totalContentLength) {
                    //说明已下完,更新进度
                    self.progressBlock();
                }else{
                    //416 Requested Range Not Satisfiable
                    self.error = [[NSError alloc]initWithDomain:[self.url absoluteString] code:416 userInfo:response.allHeaderFields];
                }
            }
        }
        return;
    }else{
        //其他情况还没发现
        return;
    }
    
    NSFileManager *fileManager = [NSFileManager defaultManager];
    //向文件追加数据
    NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:self.targetPath];
    [fileHandle seekToEndOfFile]; //将节点跳到文件的末尾
    [fileHandle writeData:data];//追加写入数据
    if ([fileManager fileExistsAtPath:self.targetPath]) {
        self.totalReceivedContentLength = [[fileManager attributesOfItemAtPath:self.targetPath error:nil] fileSize];
        if (self.totalContentLength == self.totalReceivedContentLength) {
            NSLog(@"下载完了");
            //下载完了,停止请求
            [self cancel];
            self.completionBlock();
        }
    }
    [fileHandle closeFile];
    self.progressBlock();
}

使用步骤:

1.

 [DocDownloader resumeManagerWithURL:[NSURL URLWithString:urlStr] targetPath:self.targetPath success:^{
        NSLog(@"WebRequestTypeDocDownload_success");
        //下载完成,可以写入一些完成之后的操作
    } failure:^(NSError *error) {
        NSLog(@"WebRequestTypeDocDownload_failure");
       //下载失败,可以做相应的提示
    } progress:^(long long totalReceivedContentLength, long long totalContentLength) {
       //回调totalReceivedContentLength和totalContentLength
       // 下载了多少:totalReceivedContentLength 
       // 文件总大小:  totalContentLength
       // 进度条可以这样表示:
       //progress = totalReceivedContentLength / totalContentLength
    }];

2.启动下载(POST请求)

 [self.manager startWithParams:paramStr];

3.暂停下载

- (void)suspendWithCancel {

    [self.manager cancel];

    self.manager = nil;

}

那么问题来了,如果下载了一部分就暂停了,退出app,重新进来,文件数据呢???

这个其实我们已经写入文件了,最好写入Documents目录下。首先判断第一次进入时,检查文件路径是否存在,若存在就需要计算出文件大小,并与我们知道的文件的总大小做比较。

这里比较,可以分为两种情况:(这里以fileSize为文件计算出的大小,totalFileSize为文件的总大小)

第一种文件没有加密,这个很好处理,1.如果0<fileSize<totalFileSize,说明已经下载了一部分,下载进度为fileSize。2.如果fileSize==totalFileSize,说明已经下载完了,标记状态。

第二种文件下载完成后加密,加密之后的文件大小会比原来的大小大一些(由于博主的加密方式是sm4加密,不知道其他的加密方法会不会比原来的大一些),

1.如果0<fileSize<totalFileSize,说明已经下载了一部分,下载进度为fileSize。2.如果fileSize>=totalFileSize,说明已经下载完了,标记状态。

原文地址:https://www.cnblogs.com/DWdan/p/4962578.html