下载管理器

作者: KevinTing | 来源:发表于2016-08-21 22:42 被阅读209次

    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修复
    欢迎提问题

    相关文章

      网友评论

        本文标题:下载管理器

        本文链接:https://www.haomeiwen.com/subject/rlzpsttx.html