iOS 多任务下载(支持离线)

代码下载

代码下载地址

效果展示

屏幕快照 2017-03-30 上午9.41.16.png
屏幕快照 2017-03-30 上午9.41.33.png
屏幕快照 2017-03-30 上午9.41.43.png

分析

说到iOS中的下载,有很多方式可以实现,NSURLConnection(已经弃用)就不说了,AFNetworking也不说了。我使用的是NSURLSession,常用的有3个任务类,NSURLSessionDataTask、NSURLSessionDownloadTask、NSURLSessionUploadTask,它们都继承自NSURLSessionTask。很明显他们一个用于获取数据一个用于下载另一个用于上传的。首先我们肯定选择使用NSURLSessionDownloadTask来做下载,那么接下来就聊聊吧。

  1. 创建一个NSURLSession实例来管理网络任务
        //可以上传下载HTTP和HTTPS的后台任务(程序在后台运行)。 在后台时,将网络传输交给系统的单独的一个进程,即使app挂起、推出甚至崩溃照样在后台执行。
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"QSPDownload"];
        _session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];

2.添加下载任务

/**
 添加下载任务
 
 @param netPath 下载地址
 */
 - (void)addDownloadTast:(NSString *)netPath
{
    NSURLSessionDownloadTask *tast = [self.session downloadTaskWithURL:[NSURL URLWithString:netPath]];
    [(NSMutableArray *)self.downloadSources addObject:tast];
    //开始下载任务
    [task resume];
}

3.实现相关协议
NSURLSessionDownloadTaskDelegate协议有如下3个方法:

这个方法在下载过程中反复调用,用于获知下载的状态

  • (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
    这个方法在下载完成之后调用,用于获取下载后的文件
  • (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
    这个方法在暂停后重新开始下载时调用,一般不操作这个代理
  • (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes

我们可以使用- (void)cancelByProducingResumeData:(void (^)(NSData * _Nullable resumeData))completionHandler这个方法来暂停任务,使用- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData这个方法来重启任务。可是没有办法获取下载过程中的数据来实现离线下载,程序退出后就要重新下载了。

使用NSURLSessionDataTask实现离线下载

一、包装一个QSPDownloadSource类来存储每个下载任务的数据资源,并且实现NSCoding协议用于归档存储数据,并通QSPDownloadSourceDelegate协议来监听每个下载任务的过程

#import <Foundation/Foundation.h>

typedef NS_ENUM(NSInteger, QSPDownloadSourceStyle) {
    QSPDownloadSourceStyleDown = 0,//下载
    QSPDownloadSourceStyleSuspend = 1,//暂停
    QSPDownloadSourceStyleStop = 2,//停止
    QSPDownloadSourceStyleFinished = 3,//完成
    QSPDownloadSourceStyleFail = 4//失败
};

@class QSPDownloadSource;
@protocol QSPDownloadSourceDelegate <NSObject>
@optional
- (void)downloadSource:(QSPDownloadSource *)source changedStyle:(QSPDownloadSourceStyle)style;
- (void)downloadSource:(QSPDownloadSource *)source didWriteData:(NSData *)data totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;
@end

@interface QSPDownloadSource : NSObject <NSCoding>
//地址路径
@property (copy, nonatomic, readonly) NSString *netPath;
//本地路径
@property (copy, nonatomic, readonly) NSString *location;
//下载状态
@property (assign, nonatomic, readonly) QSPDownloadSourceStyle style;
//下载任务
@property (strong, nonatomic, readonly) NSURLSessionDataTask *task;
//文件名称
@property (strong, nonatomic, readonly) NSString *fileName;
//已下载的字节数
@property (assign, nonatomic, readonly) int64_t totalBytesWritten;
//文件字节数
@property (assign, nonatomic, readonly) int64_t totalBytesExpectedToWrite;
//是否离线下载
@property (assign, nonatomic, getter=isOffLine) BOOL offLine;
//代理
@property (weak, nonatomic) id<QSPDownloadSourceDelegate> delegate;

@end

二、使用单例QSPDownloadTool来管理所有下载任务
1.单例化工具类,初始化、懒加载相关数据。

+ (instancetype)shareInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _shareInstance = [[self alloc] init];
    });
    
    return _shareInstance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _shareInstance = [super allocWithZone:zone];
        if (![[NSFileManager defaultManager] fileExistsAtPath:QSPDownloadTool_DownloadDataDocument_Path]) {
            [[NSFileManager defaultManager] createDirectoryAtPath:QSPDownloadTool_DownloadDataDocument_Path withIntermediateDirectories:YES attributes:nil error:nil];
        }
    });
    
    return _shareInstance;
}

- (NSURLSession *)session
{
    if (_session == nil) {
        //可以上传下载HTTP和HTTPS的后台任务(程序在后台运行)。 在后台时,将网络传输交给系统的单独的一个进程,即使app挂起、推出甚至崩溃照样在后台执行。
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"QSPDownload"];
        _session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    }
    
    return _session;
}

2.设计QSPDownloadToolDelegate协议来监控下载工具类任务的完成

@class QSPDownloadTool;
@protocol QSPDownloadToolDelegate <NSObject>

- (void)downloadToolDidFinish:(QSPDownloadTool *)tool downloadSource:(QSPDownloadSource *)source;

@end

3.添加一系列控制下载的方法

/**
 添加下载任务

 @param netPath 下载地址
 @param offLine 是否离线下载该任务
 @return 下载任务数据模型
 */
- (QSPDownloadSource *)addDownloadTast:(NSString *)netPath andOffLine:(BOOL)offLine;

/**
 添加代理
 
 @param delegate 代理对象
 */
- (void)addDownloadToolDelegate:(id<QSPDownloadToolDelegate>)delegate;
/**
 移除代理

 @param delegate 代理对象
 */
- (void)removeDownloadToolDelegate:(id<QSPDownloadToolDelegate>)delegate;

/**
 暂停下载任务

 @param source 下载任务数据模型
 */
- (void)suspendDownload:(QSPDownloadSource *)source;
/**
 暂停所有下载任务
 */
- (void)suspendAllTask;

/**
 继续下载任务

 @param source 下载任务数据模型
 */
- (void)continueDownload:(QSPDownloadSource *)source;
/**
 开启所有下载任务
 */
- (void)startAllTask;
/**
 停止下载任务

 @param source 下载任务数据模型
 */
- (void)stopDownload:(QSPDownloadSource *)source;
/**
 停止所有下载任务
 */
- (void)stopAllTask;

说明:

  • 所有的下载任务数据保存在数组downloadSources中,如果添加的是离线任务,我们需要保存到本地。所以增加一个保存下载任务数据的方法:
- (void)saveDownloadSource
{
    NSMutableArray *mArr = [[NSMutableArray alloc] initWithCapacity:1];
    for (QSPDownloadSource *souce in self.downloadSources) {
        if (souce.isOffLine) {
            NSData *data = [NSKeyedArchiver archivedDataWithRootObject:souce];
            [mArr addObject:data];
        }
    }
    
    [mArr writeToFile:QSPDownloadTool_DownloadSources_Path atomically:YES];
}
  • 关于QSPDownloadTool工具类,我设计的是能够添加多个代理对象,代理对象存储于数组中,数组对内部的数据都是强引用,所以会造成循环引用,为了解决这个问题,我设计一个代理中间类QSPDownloadToolDelegateObject,数组强引用代理中间类对象,代理中间类对象弱引用代理对象。
@interface QSPDownloadToolDelegateObject : NSObject

@property (weak, nonatomic) id<QSPDownloadToolDelegate> delegate;

@end

4.重中之重,实现NSURLSessionDataDelegate协议记录相关数据

#pragma mark - NSURLSessionDataDelegate代理方法
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    NSLog(@"%s", __FUNCTION__);
    dispatch_async(dispatch_get_main_queue(), ^{
        for (QSPDownloadSource *source in self.downloadSources) {
            if (source.task == dataTask) {
                source.totalBytesExpectedToWrite = source.totalBytesWritten + response.expectedContentLength;
            }
        }
    });
    
    // 允许处理服务器的响应,才会继续接收服务器返回的数据
    completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    dispatch_async(dispatch_get_main_queue(), ^{
        for (QSPDownloadSource *source in self.downloadSources) {
            if (source.task == dataTask) {
                [source.fileHandle seekToEndOfFile];
                [source.fileHandle writeData:data];
                source.totalBytesWritten += data.length;
                if ([source.delegate respondsToSelector:@selector(downloadSource:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)]) {
                    [source.delegate downloadSource:source didWriteData:data totalBytesWritten:source.totalBytesWritten totalBytesExpectedToWrite:source.totalBytesExpectedToWrite];
                }
            }
        }
    });
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    if (error) {
        NSLog(@"%@", error);
        NSLog(@"%@", error.userInfo);
    }
    
    dispatch_async(dispatch_get_main_queue(), ^{
        QSPDownloadSource *currentSource = nil;
        for (QSPDownloadSource *source in self.downloadSources) {
            if (source.fileHandle) {
                [source.fileHandle closeFile];
                source.fileHandle = nil;
            }
            
            if (error) {
                if (source.task == task && source.style == QSPDownloadSourceStyleDown) {
                    source.style = QSPDownloadSourceStyleFail;
                    if (error.code == -997) {
                        [self continueDownload:source];
                    }
                }
            }
            else
            {
                if (source.task == task) {
                    currentSource = source;
                    break;
                }
            }
        }
        
        if (currentSource) {
            currentSource.style = QSPDownloadSourceStyleFinished;
            [(NSMutableArray *)self.downloadSources removeObject:currentSource];
            [self saveDownloadSource];
            for (QSPDownloadToolDelegateObject *delegateObj in self.delegateArr) {
                if ([delegateObj.delegate respondsToSelector:@selector(downloadToolDidFinish:downloadSource:)]) {
                    [delegateObj.delegate downloadToolDidFinish:self downloadSource:currentSource];
                }
            }
        }
    });
}

说明:

  • NSURLSessionDataDelegate代理方法为异步调用,为了避免多线程对资源的争夺和能够刷新UI,把代理方法中的操作都切入主线程。
  • 在(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data这个代理方法中,把数据写入磁盘,防止内存过高,并记录相关数据存储于QSPDownloadSource对象中,还有就是在这里需要回调QSPDownloadSourceDelegate协议的代理方法。
  • 在(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error这个代理方法中,我们需要对错误进行处理,移除掉完成的任务,并回调QSPDownloadToolDelegate的代理方法。

三、问题与补充

  • 计算下载文件的大小,代码中的QSPDownloadTool_Limit值为1024
/**
 按字节计算文件大小

 @param tytes 字节数
 @return 文件大小字符串
 */
+ (NSString *)calculationDataWithBytes:(int64_t)tytes
{
    NSString *result;
    double length;
    if (tytes > QSPDownloadTool_Limit) {
        length = tytes/QSPDownloadTool_Limit;
        if (length > QSPDownloadTool_Limit) {
            length /= QSPDownloadTool_Limit;
            if (length > QSPDownloadTool_Limit) {
                length /= QSPDownloadTool_Limit;
                if (length > QSPDownloadTool_Limit) {
                    length /= QSPDownloadTool_Limit;
                    result = [NSString stringWithFormat:@"%.2fTB", length];
                }
                else
                {
                    result = [NSString stringWithFormat:@"%.2fGB", length];
                }
            }
            else
            {
                result = [NSString stringWithFormat:@"%.2fMB", length];
            }
        }
        else
        {
            result = [NSString stringWithFormat:@"%.2fKB", length];
        }
    }
    else
    {
        result = [NSString stringWithFormat:@"%lliB", tytes];
    }
    
    return result;
}
  • 计算下载速率:思路是这样的,在获得下载数据的代理方法中记录多次回调的时间差和多次回调获得的数据大小总和,就这样数据大小除以时间就为下载速率了,具体实现请看工程中的代码。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容