Troubles-of-realizing-download-module, 实现项目下载需求时遇过的那些坑

分享于 

14分钟阅读

GitHub

  繁體
实现项目下载需求时遇到过的那些坑
  • 源代码名称:Troubles-of-realizing-download-module
  • 源代码网址:http://www.github.com/DeveloperLx/Troubles-of-realizing-download-module
  • Troubles-of-realizing-download-module源代码文档
  • Troubles-of-realizing-download-module源代码下载
  • Git URL:
    git://www.github.com/DeveloperLx/Troubles-of-realizing-download-module.git
    Git Clone代码到本地:
    git clone http://www.github.com/DeveloperLx/Troubles-of-realizing-download-module
    Subversion代码到本地:
    $ svn co --depth empty http://www.github.com/DeveloperLx/Troubles-of-realizing-download-module
    Checked out revision 1.
    $ cd repo
    $ svn up trunk
    
    实现项目下载需求时遇过的那些坑
    `来自DeveloperLx的github`
    
    
    

    导语

    • 当前市面上的APP,凡有涉及到视频、期刊、或其它大型文件传输、浏览等用途的,添加下载或缓存至本地的功能以避免网速的限制及依赖,毫无疑问都将给用户带来更好的体验。而谈到下载技术,就又不得不牵扯到了断点续传,队列任务等老生常谈的问题。这不,本人当前的项目,就恰好遇到了这样的需求。然而在经过大量调研之后,本人竟无法找到一篇总结得很好的文档,对此进行全面的介绍;能够寻到的一些活跃度并不高的开源项目,却又不能恰如其分并抱之以信赖满足项目的需求。所以仔细斟酌后,不得不选择自己动手,丰衣足食。钻研的过程中遇到了不少坑、不少困难,有些个别的地方真是不用不知道,一用才知道是如此得蹩脚,难怪很少有人对此进行系统完整的介绍。现将本人在实现下载模块时所用到的技术总结如下,相信本文中所蕴涵的干货一定不会令费心阅读的你感到失望!

    • 话休絮烦。首先,说下载就离不开网络请求。而当今iOS开发技术当中,最广泛使用的网络请求框架无疑要属AFNetworking。经过对其进行简单研究后,你就会寻到最适合用来完成下载这件"小事"的组件,叫做AFHTTPRequestOperation

    现假定我们的需求是最常见,也是最能体现技术问题的一个,叫做:

    • 下载队列在某一时刻,最多仅能有一个下载任务处于正在下载的状态中!
    -- 叙述的节奏似乎稍稍快了些

    那就先来看下实现队列下载、断点续传等需求的关键示例代码吧!

         NSError * error = nil;
    
    
    
            // 创建下载队列
    
    
            NSOperationQueue * downloadOperationQueue = [[NSOperationQueue alloc]init];
    
    
            //      规定operationQueue中,最大可以同时执行的operation数量为1
    
    
        downloadOperationQueue.maxConcurrentOperationCount = 1;
    
    
    
            // 创建单个下载任务(访问已下载部分的文件,实现断点续传)
    
    
            NSMutableURLRequest * downloadRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:DOWNLOAD_URL_STRING]];
    
    
        [[NSURLCache sharedURLCache] removeCachedResponseForRequest:downloadRequest];
    
    
    
        AFHTTPRequestOperation * downloadOperation = [[AFHTTPRequestOperation alloc]initWithRequest:downloadRequest];
    
    
    
        unsigned long long downloadedPartFileSize = 0;
    
    
    
        if ([[NSFileManager defaultManager] fileExistsAtPath:DOWNLOADED_PART_FILE_PATH]) {
    
    
    
            NSDictionary * fileAttributes = [[NSFileManager defaultManager]attributesOfItemAtPath:DOWNLOADED_PART_FILE_PATH error:&error];
    
    
            downloadedPartFileSize = [fileAttributes fileSize];
    
    
            NSString * headerRangeFieldValue = [NSString stringWithFormat:@"bytes=%llu-", downloadedPartFileSize];
    
    
            [downloadRequest setValue:headerRangeFieldValue forHTTPHeaderField:@"Range"];
    
    
        }
    
    
    
        downloadOperation.outputStream = [NSOutputStream outputStreamToFileAtPath:DOWNLOADED_PART_FILE_PATH append:YES];
    
    
    
        [downloadOperation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
    
    
            NSLog(@"%lld/%lld", totalBytesRead + downloadedPartFileSize, totalBytesExpectedToRead + downloadedPartFileSize);
    
    
        }];
    
    
    
        [downloadOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
    
    
            NSLog(@"downloadOperation completion block invoked");
    
    
        } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    
    
            NSLog(@"downloadOperation failure block invoked");
    
    
        }];
    
    
    
        //  将单个下载任务加入到下载队列当中
    
    
        [downloadOperationQueue addOperation:downloadOperation];
    
    
    
        //  暂停某下载任务
    
    
        [downloadOperation pause];
    
    
    
        //  继续某下载任务
    
    
        [downloadOperation resume];
    
    
    
        //  取消某下载任务(同时应将其已下载部分的文件删除)
    
    
        [downloadOperation cancel];
    
    
        [[NSFileManager defaultManager] removeItemAtPath:DOWNLOADED_PART_FILE_PATH error:&error];
    
    
    
        //  取消全部下载任务
    
    
        [downloadOperationQueue cancelAllOperations];
    
    
    
        //  此外还有若干方法用以判断相应一见其名便知其义的状态...
    
    
        downloadOperation.isReady
    
    
        downloadOperation.isExecuting
    
    
        downloadOperation.isPaused
    
    
        downloadOperation.isCancelled
    
    
        downloadOperation.isFinished
    
    
    
        //  判断downloadOperation是否存在在downloadOperationQueue当中
    
    
        [downloadOperationQueue.operations containsObject:downloadOperation]
    
    
    
    • 以上代码创建了一个AFHTTPRequestOperation对象作为单个下载任务,并将其加入到NSOperationQueue中。仿照上例,我们可以创建多个AFHTTPRequestOperation对象并加入到NSOperationQueue中,即形成了下载队列
    • 做到这里,你是不是认为已经没有神马技术问题啦?只要把operation一个个地添加到queue里, 下载任务就可以一个接一个地自动执行了!而如果我们将上一个operation暂停、取消,或是它自然地下载完成了,又或是它下载中途失败了,下一operation就会聪明地自动启动,继续其下载任务了!!?

    错!!!!

    • 接下来笔者将要告诉你的,就是本文最最核心的干货,绝对颠覆你的想象!!
    • 只要你亲手动手试一试,就会发现如下大跌眼球的惊恐现象!!

    • 惊人事实 1: 对queue中前一个下载operation执行pause方法,下一个operation并不能自动启动进入正在执行的状态!!

    • 惊人事实 2: 如果queue中前一个下载operation执行失败了(可用下载中途断网进行模拟),它将从queue中自动地被移除掉!!

    • 惊人事实 3: 注意到代码里那个failure回调的block了没?它不仅仅将在operation执行失败的时候被调用,还会在operation被cancel的时候被调用!!所以对于神马叫做"operation的失败",你要重新建立起你的世界观了!!

    • 惊人事实 4: 如果对一个正处于pause状态的operation执行cancel会怎么样?答案是这个operation还保留在queue中!!并且仍然保持着pause状态!!仅有的一点变化,是它的isCancelled属性,变成了YES!!

    • ......未完待续,本文要令你感到惊诧的,还有很多

    由于这些问题间相互关系的错综复杂,为了清晰条理地予以说明,特将本人实验中所观察到operation的行为总结如下表。其中有悖于我们想象的结果,已用彩色背景字体标出

    Index Description isReady isExecuting isPaused isCancelled isFinished in Queue success block invoked failure block invoked 与预期不相符的事实 可能的解决方案
    1加入queue前YES
    2加入queue后YESYES
    3自然结束后YESYES
    4正在下载时执行pause方法后YESYES
    5正在下载时执行cancel方法后YESYESYES被标记finished;fail block被调用不能仅凭fail block被调用判定某operation执行失败;必须同时判定isCancelled才是真正的失败,否则只是被人为地取消了
    6正在暂停时执行resume方法后YESYES
    7正在暂停时执行cancel方法后YESYESYESPaused状态未被取消;未能从queue中被移除如想将暂停时的operation取消并从operationQueue中清除掉,必须首先执行[downloadOperation resume]后,再执行[downloadOperation cancel]
    8中途意外失败后YESYES被标记为finished;从queue中被移除必须用另外的方式记录执行中途意外失败的downloadOperation以实现中途失败的下载任务断点续传
    9等待上一任务结束时YESYES
    10上一任务中途意外失败后YESYES
    11上一任务正在执行时被暂停后YESYES上一operation被正在执行时被暂停后不能自动使下一operation开始执行前一个operation被暂停后,必须手动启动下一operation
    12上一任务正在执行时被取消后YESYES
    13上一任务正在暂停时被取消后YESYES上一operation被正在暂停时被取消后不能自动使下一operation开始执行前一个operation被暂停后,必须手动启动下一operation
    14队列被执行cancelAllOperationsYESYESYES被标记finished;fail block被调用不能仅凭fail block被调用判定某operation执行失败;必须同时判定 isCancelled才是真正的失败,否则只是被人为地取消了
    • 有木有感到AFHTTPRequestOperation和NSOperationQueue是个多么坑爹的东东?为何就不能像我们想象中一样用得舒爽?

    • **原因就在于AFHTTPRequestOperation的父类NSOperation,在设计之处就不是为了下载的操作而生的!**人家开始就仅仅是用来处理多线程的啊!!所以造成了AFNetworking在扩展这个类的时候,可用的资源、接口等等就非常少。对于什么下载任务暂停/继续,下载中途失败等等情况,很多问题几乎就是没有办法理想地解决的,只好用NSOperation中仅有的几种状态予以并不贴切的表示。于是乎就出现了上表中种种诡异的情况

    补充几点干货。然后告诉你一个本文之前偷偷误导了你的大坑!!

    • 惊人事实 5:如果一个queue中有一个下载operation正在执行,此时对另一处在isReady状态的operation执行start方法会怎么样?你很可能会说:"没用的,因为之前设了queue.maxConcurrentOperationCount = 1嘛!" 可事实恰好相反,这个operation也会立刻被启动执行!!于是乎你不忍心看到的事情就出现了,这时queue将会有两个任务被同时执行!!maxConcurrentOperationCount完全失效了!!

    • 惊人事实 6:承接上一点,如果此时另一条的状态不是isReady,而是isPaused暂停状态,你对其执行resume方法,此时会怎么样呢?哈哈,没错,你吸取了上一条的经验,终于猜对了!这个operation也会立刻启动被执行,不管当前的queue有没有另一个operation正在被执行!!从中我们就可以意识到,maxConcurrentOperationCount这个属性,只能管得自动启动每一operation时,先检查下是否正在执行的operation的数量已经超过那个数字了;可是如果你要手动start某一operation,对不起,这条限制半点都没有用处了......

    • 惊人事实 7:从上表中我们可以看到,无论是一个operation自然地执行完毕,还是中途失败,还是被执行了cancel方法,都会被标记为isFinished,从operation中被移除掉,operation所认为的"完成"可完全不像我们想象中的那么狭义!问题来了,此时如果再对这个operation执行start方法会怎么样?对不起!没有任何用处!


    相关文章