下载管理器

1、架构

项目中需要管理下载,多个任务同时下载,下载的暂停,恢复,进度显示等等。比如现在的百度云,迅雷等软件的下载功能:


WechatIMG1.jpeg

当然我不要做的那么牛逼,但是在现有的基础上还是可以模仿一个差不多的下载管理器出来的。我主要用的下面三个类:

KTDownloadManager.h
KTDownloadManager.m
KTDownloadModel.h
KTDownloadModel.m
KTDownloadOperation.h
KTDownloadOperation.m

主要有下面的功能:
1、添加,删除下载项;
2、开始、暂停下载;
3、进度显示,错误提醒,保存路径等;
4、断点续传;
5、下载项的本地序列化,反序列化(对于可以断点续传的下载项,杀掉应用后还可以继续下载);

1.1、KTDownloadManager

使用单例方法生成的实例来做下载管理者,他负责管理KTDownloadModel和调度KTDownloadOperation,像SDWebImage也是使用的单例来管理下载的。除了session之外,KTDownloadManager还有两个重要的属性:

// 下载队列,默认并发下载数量为3
@property (nonatomic, strong, readonly) NSOperationQueue *downloadQueue;
// download models
@property (nonatomic, strong) NSMutableArray *downloadModels;
// nsurlsession
@property (nonatomic, strong) NSURLSession *session;

这两个都是私有属性,因为不需要外面知道。downloadModels是所有的KTDownloadModel对象的集合。downloadQueue是下载队列,实际下载操作在KTDownloadOperation里面进行,那么下载过程就是添加KTDownloadOperation实例到downloadQueue中。

1.2、KTDownloadModel

这里的model是用来显示以及保存相关信息的,比如界面上有一个下载项就有一个model,model里面有基本的url,totalReceivedBytes,totalBytes等属性,表征一个下载的基本信息。我们在做界面显示的时候,只需要把model的属性展示出来即可,下载进度等等变化也只需要监听对应的model属性变化,这里使用代理来通知界面UI变化。

@interface KTDownloadModel : NSObject
// 下载的url
@property (nonatomic, strong, readonly) NSURL *url;
// 下载的文件全路径,可以指定,必须处于Documents或者Library/Caches文件夹下面
// 如果为nil,那么下载完成之后使用KTDownloadManager的downloadFolderPath配合suggest name构成文件全路径
@property (nonatomic, copy) NSString *downloadFilePath;
// 已接收的总字节数
@property (nonatomic, assign) int64_t totalReceivedBytes;
// 当前接收的data,由于存在断点续传,只表示这一次下载的data,并不表示下载的总data
@property (nonatomic, strong) NSMutableData *receivedData;
// 总字节数
@property (nonatomic, assign) int64_t totalBytes;
// 下载状态
@property (nonatomic, assign) KTDownloadState state;
// 下载operation,正在下载中或者暂停一段时间之内的model会有一个operation,其他情况为nil
@property (nonatomic, weak) KTDownloadOperation *operation;
// delegate
@property (nonatomic, weak) id<KTDownloadModelDelegate> delegate;
// error
@property (nonatomic, strong) NSError *error;
1.3、KTDownloadOperation

实际下载动作都在KTDownloadOperation里面进行,这里是模仿SDWebImage的下载队列来的,使用NSOperationQueue的好处是你可以设置同时下载的最大个数,同时你暂停一个下载之后,或者一个下载完成之后下一个下载会自动开始,还有可以很方便的暂停,启动,取消一个下载操作,这些都非常符合下载队列的需求,而GCD是做不到的。

@interface KTDownloadOperation : NSOperation

// 每个operation必须有一个model
@property (nonatomic, weak, readonly) KTDownloadModel *downloadModel;
@property (nonatomic, strong, readonly) NSURLSessionDataTask *dataTask;
1.4、三者之间的关系

KTDownloadManager是管理KTDownloadModel和KTDownloadOperation的,model负责记录下载项的进度,url等属性,同时和UI打交道,operation负责下载。下载操作就是KTDownloadManager通过model生成一个operation操作,然后把它丢到队列queue中去下载,下载进度,结果,错误等operation会告诉model,然后model在记录下来的同时会去通知UI。model和operation会互相弱引用,由于一个下载项对应一个model,但是下载项不一定处于下载状态,有可能暂停,没开始,或者已经完成,此时是没有对应一个operation的。所以KTDownloadModel的operation属性可能为空,但是KTDownloadOperation的downloadModel属性一定不为空。

2、注意点

2.1、使用NSURLSessionDataTask

我没用NSURLSessionDownloadTask的原因居然是断点续传,实际上NSURLSessionDownloadTask下载的时候会将临时下载的文件保存在tmp文件夹中,下一次下载的时候可以根据这个文件恢复下载,即实现断点续传。但是杀掉应用之后,tmp文件夹很有可能被清掉,那么此时是不能恢复下载的。当然也可以像这两篇文章那样曲线救国实现退出应用后的断点续传:
http://www.cocoachina.com/ios/20160503/16053.html
http://www.tuicool.com/articles/uyQrIzi
我采用的办法是直接使用NSURLSessionDataTask来下载,自己实现临时文件的管理以及恢复下载的逻辑。实现这两个代理方法:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data
{
    self.downloadModel.totalBytes = [dataTask.response expectedContentLength];
    [self.receivedData appendData:data];
    dispatch_async(dispatch_get_main_queue(), ^{
        self.downloadModel.totalReceivedBytes += data.length;
    });
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error
{
......
}

自己管理下载的临时数据,自己保存临时数据。

2.2、断点续传

http断点续传使用http请求头Range字段来实现的,网上有很多文章:
http://www.cnblogs.com/ziyunfei/archive/2012/11/18/2775499.html
KTDownloadOperation有一个私有属性receivedData用来保存当前下载的内容,如果你暂停了下载,或者其他原因断掉了,KTDownloadOperation会做下面的操作:
1、检查返回的response(也就是http响应)头部信息里面有没有Accept-Ranges字段,并且值不是none。比如http响应头里有这样的字段Accept-Ranges: bytes,那么说明服务器支持Range分段请求,否则不支持。
2、如果支持断点续传那么保存当前下载的receivedData到本地。
启动下载的时候,根据receivedData的大小来设置http请求头Range:bytes=1024-,假设receivedData的大小是1024字节。

2.3、文件size的返回

我在测试的时候发现有些链接,比如github上面的这个下载链接:
https://codeload.github.com/hanton/HTY360Player/zip/master
下载时不能正确知道Content-Length的大小,不知道这个就等于你没法显示下载进度,这里有解决办法:
http://stackoverflow.com/questions/12235617/mbprogresshud-with-nsurlconnection/12599242#12599242

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:anURL];
[request addValue:@"" forHTTPHeaderField:@"Accept-Encoding"];

此时服务器就会在http响应头里面写上正确的Content-Length字段值了。

2.4、持久化

我这里就是用的很简单的plist文件保存。KTDownloadManager监听applicationWillTerminate消息,然后将当前下载项保存,下次启动读取plist文件即可。

2.5、NSURLSession代理回调的分发

NSURLSession对象是KTDownloadManager的属性,NSURLSession的代理也是KTDownloadManager,那么上面提到的回调方法只能写在KTDownloadManager里面。但是有多个下载操作,NSURLSession的代理回调内容必须正确分发到KTDownloadOperation中。这里模仿的是AFNetworking的做法,使用分类给dataTask添加属性downloadOperation,从而让dataTask与operation一一对应,KTDownloadOperation复写代理方法即可。

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data
{
    [dataTask.downloadOperation URLSession:session dataTask:dataTask didReceiveData:data];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error
{
    [task.downloadOperation URLSession:session task:task didCompleteWithError:error];
}
2.6、UI更新

给KTDownloadModel设置一个state属性,表明下载状态,通过这两个代理方法告诉UI下载状态和进度的变化:

typedef NS_ENUM(NSUInteger, KTDownloadState)
{
    KTDownloadStateNone = 0,            // 创建新的实例时所处状态
    KTDownloadStateWaiting,             // 等待中(前面还有正在下载的操作)
    KTDownloadStateDownloading,         // 下载中
    KTDownloadStatePaused,              // 暂停
    KTDownloadStateFinished,            // 完成
    KTDownloadStateFailed               // 失败
};

@protocol KTDownloadModelDelegate <NSObject>

// 以下代理方法在operation存在并且在下载的时候才会调用
@optional
- (void)downloadModel:(KTDownloadModel *)model didChangedState:(KTDownloadState)state;
- (void)downloadModel:(KTDownloadModel *)model didReceivedTotalBytes:(int64_t)totalReceivedBytes totalBytes:(int64_t)totalBytes;

@end

如果只是在一个静态页面显示一个KTDownloadModel下载项这样并不会有什么问题,但是如果使用tableView显示多个下载项就会出问题了。比如文章开头的那个图里面,一个tableViewCell显示一个下载项,我们肯定会这样写:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {   
    DownloadTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DownloadTableViewCell" forIndexPath:indexPath];
    KTDownloadModel *model = [self.downloadModels objectAtIndex:indexPath.row];
    model.delegate = cell;
    [cell configWithModel:model];
    
    return cell;
}

这样基本是没什么问题的,但是我们都知道tableView会复用tableViewCell,那么在滑动页面的时候,会出现这样的情况:比如有20个KTDownloadModel需要展示,但是一个界面只能展示10个tableViewCell,那么tableView实际上会有11个tableViewCell实例在内存里。tableViewCell1展示KTDownloadModel1,tableViewCell2展示KTDownloadModel2。。。tableViewCell11展示KTDownloadModel11。滑到第12个的时候,此时是tableViewCell1来展示KTDownloadModel12的。说了这么多就是想说同一个tableViewCell可能会在不同的时候展示不同的KTDownloadModel,上面的代码会导致同一个tableViewCell是多个KTDownloadModel的代理,那么意味着一个tableViewCell可能在同一个时刻收到多个KTDownloadModel的进度或状态更新通知,那么就会:显示紊乱!
要保证tableViewCell在一个时刻只能是一个KTDownloadModel的代理,我在KTDownloadManager里面添加了这个方法:

- (void)setDelegate:(id<KTDownloadModelDelegate>)delegate forModel:(KTDownloadModel *)model
{
    for (KTDownloadOperation *op in self.downloadQueue.operations) {
        if (op.downloadModel.delegate == delegate) {
            op.downloadModel.delegate = nil;
        }
    }
    model.delegate = delegate;
}

将上面的model.delegate = cell替换成这一句:[[KTDownloadManager sharedManager] setDelegate:cell forModel:model];保证一个cell同一时刻只能是一个model的代理,就不会出现显示紊乱的问题了。

3、完善

项目地址:https://github.com/tujinqiu/KTDownloadManager
1、使用文件句柄写缓存,避免内存占用过大
2、后台下载
3、bug修复
欢迎提问题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容