MCDownloader(iOS下载器)说明书

作者: 老马的春天 | 来源:发表于2017-04-10 14:02 被阅读2676次

    示例

    前言

    很多iOS应用中都需要下载数据,并对这些下载的过程和结果进行管理,因此我才有了写这个MCDownloader的想法。在IOS 文件下载器-MCDownloadManager这篇文章中,我使用GCD和集合来实现了这个功能,基本上也能满足需求,这一部分的实现原理主要参考AFNetworking的源码,有兴趣的同学可以看看我写的AFNetworking 3.0 源码解读系列

    但是本篇文章中讲的MCDownloader的实现原理和上边提到的不一样,是基于NSOperation来实现的,可以说是我对SDWebImage源码解读的一些额外的扩展,同样,有兴趣的同学可以看看我写的SDWebImage源码解读系列

    MCDownloader目前的版本是1.0.0,可以在这里下载https://github.com/agelessman/MCDownloader

    功能

    MCDownloader1.0.0版本提供了以下几个功能:

    1. 多线程异步下载,支持自定义并发线程数。在上图的示例图中,采用的并发数为3
    2. 边下载变保存,这一条是该下载器最重要的思想,数据被实时的保存在本地,同时支持断点下载
    3. 十分方便的数据获取能力,通过MCDownloadReceipt来对下载的数据进行抽象,几乎所有的信息都能在MCDownloadReceipt中获取
    4. 提供了下载进度,可以通过接口函数的block监听下载进度和完成回调,也可以通过给MCDownloadReceipt绑定block来监听block回调
    5. 支持显示当前的下载速度
    6. 支持批量下载,批量取消功能
    7. 支持任务的暂停,取消,删除功能
    8. 支持下载顺序定制,先入先出或者后入先出
    9. 支持后台和锁屏下载

    如何使用?

    开启下载

    每一个下载任务的唯一标识是url,因此我们使用下边的代码开始一个下载任务:

     [[MCDownloader sharedDownloader] downloadDataWithURL:[NSURL URLWithString:url] progress:^(NSInteger receivedSize, NSInteger expectedSize, NSInteger speed, NSURL * _Nullable targetURL) {
                    
      } completed:^(MCDownloadReceipt * _Nullable receipt, NSError * _Nullable error, BOOL finished) {
          NSLog(@"==%@", error.description);
      }];
    

    可以在上边的progress和completed中自定义处理方法。进度和完成的block回调都在主线程触发。

    暂停或取消

    MCDownloader的暂停和取消功能是一样的,由于内部下载是基于NSOperation实现的,因此每一个任务就是一个NSOperation,然后再把他们添加到队列之中。当取消或者暂停一个任务后,在重新恢复下载,实际上会重新把该任务添加到队列中,这一点一定要注意。

    使用下边的代码来暂停或取消一个下载任务:

    [[MCDownloader sharedDownloader] cancel:receipt completed:^{
                [self.button setTitle:@"Start" forState:UIControlStateNormal];
            }];
    

    由于取消不是发生在主线程,所以需要一个completed来捕获取消成功事件,然后在主线程调用。

    移除数据

    通过下边的方法来移除保存在本地的数据:

     [[MCDownloader sharedDownloader] remove:receipt completed:^{
                [self.tableView reloadData];
            }];
    

    获取数据信息

    可以通过下边的代码来获取数据的一些信息,这些信息既可以在下载过程中获取,也可以在下载完成后获取。

    MCDownloadReceipt *receipt = [[MCDownloader sharedDownloader] downloadReceiptForURLString:self.url];
    

    通过上边的代码可以看出来,url被当做数据的唯一标识。在上图的例子中,我们是在cell中更新下载进度的,为了防止cell的复用问题,我为每个receipt绑定了progress和complete回调block:

     __weak typeof(receipt) weakReceipt = receipt;
        receipt.downloaderProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSInteger speed, NSURL * _Nullable targetURL) {
            __strong typeof(weakReceipt) strongReceipt = weakReceipt;
            if ([targetURL.absoluteString isEqualToString:self.url]) {
                [self.button setTitle:@"Stop" forState:UIControlStateNormal];
                self.bytesLable.text = [NSString stringWithFormat:@"%0.1fm/%0.1fm", receivedSize/1024.0/1024,expectedSize/1024.0/1024];
                self.progressView.progress = (receivedSize/1024.0/1024) / (expectedSize/1024.0/1024);
                self.speedLable.text = [NSString stringWithFormat:@"%@/s", strongReceipt.speed ?: @"0"];
            }
            
        };
        
        receipt.downloaderCompletedBlock = ^(MCDownloadReceipt *receipt, NSError * _Nullable error, BOOL finished) {
            if (error) {
                [self.button setTitle:@"Start" forState:UIControlStateNormal];
                self.nameLabel.text = @"Download Failure";
            }else {
                [self.button setTitle:@"Play" forState:UIControlStateNormal];
                self.nameLabel.text = @"Download Finished";
            }
            
        };
    

    取消全部下载和删除全部数据

    在某种场景下需要取消全部的下载,比如说监听到网络状态变成4G时,需要询问用户是否继续下载。又或者在需要清空缓存的时候:

    [[MCDownloader sharedDownloader] cancelAllDownloads];
    
    [[MCDownloader sharedDownloader] removeAndClearAll];
    

    上边说的这些功能,在demo中都有演示。

    核心思想

    由于下载功能不算是特别复杂的功能,所以我就简单的说说内部的实现原理。

    在代码设计之初,我最先写的类就是MCDownloadReceipt。通过它来对数据进行抽象封装,我们先不管它是如何获取的,只关心它需要暴露多少信息。这个类很简单,我就不把代码弄上来了,但是需要注意下边几点:

    • 这些属性被设计成只读属性,表明只在该类中获取数据,不要修改其中的数据
    • receipt需要保存在本地,我采用的是归档的方法进行持久化
    • 文件名要做MD5处理
    • 每一个receipt都绑定了一个状态属性

    完成了模型的搭建后,就要处理最基本的下载任务了,MCDownloadOperation继承自NSOperation,因此在MCDownloadOperation中我们就不需要关心线程的问题。我们在这个类中只做了下边这几件事:

    • 开启下载任务,在开启任务的start方法中,我做了一些保证任务开始的必要措施,有兴趣的可以去看看源码
    • 接受数据和写入数据
    • 处理下载过程中和完成后的回调函数
    • 处理下载状态
    • 关心如何取消任务

    接下来就到了最核心的地方,如何把MCDownloadReceipt和MCDownloadOperation组合在一起,也就是MCDownloader的内容。MCDownloader是暴露出来最核心的模块,在设计上主要考虑下边几件事情:

    • 需要一个单利对象来管理全局的情况
    • 支持设置一些跟下载相关的额外信息,比如超时时间,请求头和并发数等等
    • 提供一些控制下载的常用方法,如何开始,取消,移除等等

    综上所述,这基本上是写任何一个框架的基本流程,在编码之前先进行设计。 另外,在使用的过程中,如果有任何问题,可以给我留言,如果有新的需求,也可以给我留言。

    由于水平有限,难免会出现错误,如果发现后,还望能够告知一声。

    相关文章

      网友评论

      • d76d0c9d2b04:下载几个后,发现cpu占用率达到 100%以上,手机有点发热了,好像有点耗性能啊?
      • liuchang49:下载断网会出现下载失败? 这个怎么处理的。群主
        老马的春天:@liuchang49 这个是我自己的逻辑, 我认为下在失败,你可以监听后,自己弄成暂停或者其他情况,这个我只希望能够提供下载能力,业务逻辑自己实现
        liuchang49:请教一下,群主,你们这个cpu在下载的时候占用率很高,有的时候达到了 100-200%,这个有解决么?
        老马的春天:@liuchang49 还是那句话,如果你有自己的逻辑,这些各种各样的要求需要自己实现,你可以把这个下载器当作只提供下载能力
      • 4305824b6977:请教一下,断点下载有时候文件会变大,重复下载这个该怎么处理?
        老马的春天:@剁椒鸡蛋 断点下载按理说应该是从当前下载文件的后边继续下载,不然就不叫断点下载了 ,当然这个还需要服务器的支持才行,断点下载原理是给服务器传了一个header参数,如果服务器没做处理,就默认从新下载了,这样文件就会越来越大
      • 90后的晨仔:你好,如果要是使用AFNetworking使用post的方式进行下载,怎么记录和保存下载进度然后在cell上更新下载进度呢?
        老马的春天:@屌丝爷霉儿 http://code.cocoachina.com/ 你自己找找相似的看看吧
        90后的晨仔:有点不懂
        老马的春天:@屌丝爷霉儿 这个弄个通知也行,代理也行,我之前想过的是自己写一个监听处理类,, 当这个类收到通知后,它通过一个遍历通知它的监听对象,那种方式都行吧
      • 暴走的西瓜:感谢作者.刚好用到.好就没弄过下载文件的功能.学习了.模拟器下载一半.然后杀死重新进来会发现进度还是0.换真机试一下
        老马的春天:@暴走的西瓜 真机应该没问题,有些缓存的数据不是实时缓存的,退到后台或者被杀死的时候会保存,这个只是提供了简单的下载功能,很多不同的需求,需要自己实现业务处理
      • Vincent20481:下载一直是个难点,学习了
      • 6492983b777a:大神,我想问一下,请求头里面可以加id,name,password这些参数么,怎么加?
        6492983b777a:@老马的春天 好的,谢谢,我试试
        老马的春天:意思就是把那个当成平时用的post请求就行
        老马的春天:@yihanyun 这个肯定不能明文放到url中,你需要自己修改里边的代码,发送请求的时候 自己给request增加点东西: 比如:
        NSData *jsonData = [NSJSONSerialization dataWithJSONObject:parameters options:self.writingOptions error:error];

        if (!jsonData) {
        return nil;
        }

        [mutableRequest setHTTPBody:jsonData];
      • 随风风流:你好,我在使用MCDwonloader过程中设置了清除缓存功能(该功能是用户可以删除本地下载的文件)想要重新下载的时候发现下载状态是已完成的是(receipt.state == MCDownloadStateCompleted) ,然后这种要怎么破呢?
        老马的春天:@随风风流 就把那个配置文件也删了,调用里边的remove方法
      • 不谷_AndiOS:if (sself.downloadPrioritizaton == MCDownloadPrioritizationLIFO) {
        // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
        [sself.lastAddedOperation addDependency:operation];
        sself.lastAddedOperation = operation;
        }
        你这样的操作 添加依赖 会不会把并发变成了串行
      • 云画的跃光:你好,我想知道怎么下载pdf的?因为试了几次都没有,而且我根据receipt.filePath查询下载的文件,里面没有东西
        老马的春天:@云画的跃光 这样,你找个pdf下载的url试试demo, 如果url是你们自己后台的,那你得确认下 请求头 是否设置正确,
        云画的跃光:@老马的春天 我的url只能在内部网使用
        老马的春天:@云画的跃光 等我上班了我看看,你给我个url
      • overla5:你好。如果我A界面点下载按钮,B界面看进度,A界面和B界面应该做什么操作
        老马的春天:@失格人间 那个就随你了,你想做成本地的就是本地的,想做成网络的就网络的,这个只是方便的提供下载功能, 如果你自己进行其他方面的逻辑管理,那是最好的
        overla5:@老马的春天 好的,一般像优酷那种,在播放界面点击下载应该就自动下载了,缓存界面直接拿单例就可以了吗?缓存界面的url列表是本地还是服务器获取的呢? 请指点一下,谢谢
        老马的春天:@失格人间 下载是一个单利,根据url获取到receipt 然后使用receipt的属性就行
      • Misscxuan:楼主问个问题, 最近在用您的框架做浏览器的下载功能, 在多次下载同一个文件时下载器会自动判断completed状态, 但是我想实现多次下载同一个文件时, 文件还会被下载,被存储为 文件(1), 文件(2) 这种形式, 请问该如何处理.
        老马的春天:@Misscxuan 反正原理上,你看看根据url获取filepath是不是正确的,要正确处理这之间的映射才行
        Misscxuan:@老马的春天 你好, 前面那个问题我已经解决了, 现在还有一个问题, 每次通过链接获取到的文件大小都是-1,这个该怎么处理.
        老马的春天:@Misscxuan 我觉得你修改下你的下载url就行了,你搜搜url相关的知识
      • edison0428:楼主,请教下,iOS 中,真正能做到后台下载的应该只有NSURLSessionDownloadTask吧,我看你好像是利用后台任务延长app存活时间,一般这个时间是三分钟或者几分钟不准,那如果有种情况,就是下载文件比较大,而网速比较渣,在你申请的后台时间内下载不完,那怎么破,希望楼主能出一个NSURLSessionDownloadTask写的demo
        老马的春天:@edison0428 恩,是的,那个要等到下载完成后才能获取数据,这个再研究研究
        edison0428:@老马的春天 其实用DataTask对于断点下载是最方便的,就是不知道比如优酷这种下载怎么做,好几百兆,后台,断点都有
        老马的春天:@edison0428 NSURLSessionDownloadTask 或者在进入后台时切换background config ,这个目前确实缺失,无法保证后台任务一定能完成,就算这么设置了,该被杀死还是得被杀死吧?
      • wykings:请问有没有办法指定下载的文件的唯一标识?因为很多的视频文件为了防盗链会随机生成下载的链接
        wykings:@老马的春天 是的,需要在服务器那边定下规则,我也已经有思路了,谢谢:smile:
        老马的春天:@wykings 这个不好弄吧,你有自己的规则,除非给一个规则转换的block,
      • 囧囧的时候:请问下为什么MCDownloadStateSuspened这个状态没进去过 ,怎么才能知道在等待下载呢
        老马的春天:@囧囧的时候 别用这个,我没设计暂停的状态,要么正在下载,要么就是等待下载,要么就是没下载
        囧囧的时候:@老马的春天 整个里面就只有定义的时候有MCDownloadStateSuspened这个 其他地方搜不到
        老马的春天:@囧囧的时候 你看我的demo呢
      • 小小看护:请问下作者,我现在是用pod下载下来的版本呢,还是直接download下来用源文件呢:smile:
        老马的春天:@kuangyanboy 直接下源码,方便修改,再有必要的时候
      • 指尖上的代码:方面留个QQ吗 有个问题想问你 老🐴的春天
        老马的春天:@指尖上的代码 714080794
      • be44526d6f6b:我想问一下下载文件存在哪里啦
        老马的春天:@光荣_7c6c 你根据url获取到receipt后,.filePath 就是文件的路径
      • 船长One:在 8.1 系统下发现播放下载的音频 有残损,由于我没有8.1的真机,只能在模拟器上试的,不知道真机有这个问题么?
        老马的春天:@船长One 这个是因为数据没有下载完全吗?
      • 孙金亮:非常受益,谢谢作者
        老马的春天:@孙金亮 :smile:
      • 再见远洋:有点小bug 你第一次进去后随便点几个下载,某一个等待下载完成,另外几个不要下载完成,然后退出,再次登录会发现进度条全是满的
        老马的春天:@再见远洋 嗯,你把手机跟电脑的线拔掉再试,应该就行了
        再见远洋:@老马的春天 我是重新运行了,所以你没有监听到,但是从后台杀死的情况是没问题的,我是真机。我不知道我重新运行走的哪个代理方法。
        老马的春天:@再见远洋 用真机测试,不能用模拟器,有些信息是在特定的条件下持久化的,比如退到后台,或者被系统杀死时会保存一次,用模拟器点击暂停,无法监听该事件
      • MrJ的杂货铺:removeAndClearAll 的时候 再次下载同一文件时 会给出MCDownloadStateCompleted的状态
        MrJ的杂货铺:@老马的春天 setAllStateToNone方法里,MCDownloadStateCompleted的排除掉是什么目的
        老马的春天:@MrJ的杂货铺 按理说,我把保存receipt的都删了,应该都是none了
        老马的春天:确实有这个问题, 之前是暂停,然后把除了下载完成的其他都设置了None 状态,这个我改改。。
      • fc1df54ed914:楼主好棒,给了个大大的start。我想请教一下,如果我想把不同的视频下载到指定的不同文件夹,并保持原来的名字不变,该怎么做呢,谢谢!
        老马的春天:@shxlxa 嗯那,哪里需要改哪里:smile:
        fc1df54ed914:@老马的春天 修改了一下你的代码,可以满足需求了,多谢!
        老马的春天:@shxlxa 这个目前没有提供更换文件夹的功能,如果你想那么做,只能下载完成后,把视频数据移到你指定的文件夹中,然后读取的路径需要你自己进行管理,这个时候,MCDonalder只提供下载的功能和下载进度。
      • MrJ的杂货铺:锁屏后台下载 需要做特殊处理吗
        老马的春天:@MrJ的杂货铺 这个不能保证什么时候会被杀死
        MrJ的杂货铺:@老马的春天 有时间限制吗?
        老马的春天:@MrJ的杂货铺 不需要,支持锁屏和后台下载
      • d2d00a0a3626:一个download完成后退出再进来,receipt.state是none,是没保存么?
        d2d00a0a3626:好,我去试试
        老马的春天:@原来这有好多好文章 在模拟器上,当app到后台,或者被杀死的时候才会报错数据,真机上肯定会触发,模拟器不一定
      • kingkong1221:如何删除本地存储指定批量音频,不是全部删除. 比如每本书籍里面包含了很多音频,如何删除指定书籍里面的音频?
        老马的春天:@kingkong1221 目前来说 把所有的数据都放到了一个文件夹,要删除一本书籍中的视频,就得拿到书籍中的视频的url,然后一个一个删,可以用循环
      • YViVi:有断点下载?
        老马的春天:@JaysonGD 嗯,支持
      • bluajack:老马,请教你一个问题!你的下载器一次性我开了8个任务,但是真正在下载的任务是3个,这个时候程序进入后台,前面的三个任务假如有一个下载完了,唤醒程序,继续开启下一个任务。按照逻辑是这个样子的吧,那会不会存在这样一个问题,后台任务的开启会被延迟开启,请看连接:https://forums.developer.apple.com/thread/14854
        老马的春天:@bluajack 下载能下载,不能保证什么时候被系统干掉,有时间得改成这种后台模式
        bluajack:@老马的春天 使用default是不支持你文章第9条:支持后台和锁屏下载的。:no_mouth:
        老马的春天:使用background的模式,存在delay的情况,这个确实没考虑,这个得仔细研究研究,目前用的是defalut的,我看afn的源码又改了,找时间看下:smile:
      • 白水灬煮一切:太棒了楼主!感谢分享!世界因你而更加精彩!
        白水灬煮一切:@老马的春天 赞美的语言不分彼此
        老马的春天:@小僵儿 怎么感觉是code4app呢
      • 乔兰伊雪:正好用到,非常感谢
        MrJ的杂货铺:可以在下载前获取文件的大小吗?
        乔兰伊雪:@老马的春天 那必须的呀,有个问题需要问问你,如果我设置最大并发数为一,要怎么实时获取到当前下载队列里哪个视频在下载呢?除了遍历每个任务的下载状态
        老马的春天:@乔兰伊雪 哈哈,别忘了给个star啊:smile:

      本文标题:MCDownloader(iOS下载器)说明书

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