iOS多任务下载的简要概述

AliciaPrend 8年前
   <p>多任务下载顾名思义就是多个任务同时下载,各个任务在同一时间一起下载,比如迅雷等下载软件就具备这些功能,而iOS开发中也涉及到了一些多任务下载。本文就是一个多任务下载的简要概述,如有错误请见谅。</p>    <ul>     <li>获取网络数据</li>     <li>网络数据转模型</li>     <li>自定义cell</li>     <li>设置下载按钮</li>     <li>单例管理下载</li>     <li>错误的进度回调演示</li>     <li>进度回调的保存提取和执行</li>     <li>进度展示</li>     <li>下载完成的回调</li>     <li>判断是否正在下载</li>     <li>暂停下载</li>     <li>继续下载</li>    </ul>    <h2>导图</h2>    <p style="text-align:center"><img src="https://simg.open-open.com/show/5fbb31cb5d2b979bceac6e56d456dc33.jpg"></p>    <h3>获取网络数据</h3>    <ul>     <li>获取电子书列表数据的主方法</li>    </ul>    <pre>  <code class="language-objectivec">- (void)loadBookList  {      // URL      NSURL *URL = [NSURL URLWithString:@"http://42.62.15.101/yyting/bookclient/ClientGetBookResource.action?bookId=30776&imei=OEVGRDQ1ODktRUREMi00OTU4LUE3MTctOUFGMjE4Q0JDMTUy&nwt=1&pageNum=1&pageSize=50&q=114≻=acca7b0f8bcc9603c25a52f572f3d863&sortType=0&token=KMSKLNNITOFYtR4iQHIE2cE95w48yMvrQb63ToXOFc8%2A"];        // session发起和启动任务      [[[NSURLSession sharedSession] dataTaskWithURL:URL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {            // 处理响应          if (error == nil && data != nil) {                // 反序列化              id result = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];              NSLog(@"%@ %@",[result class],result);            } else {              NSLog(@"%@",error);          }        }] resume];  }</code></pre>    <h3>网络数据转模型</h3>    <ul>     <li>JSON数据示例</li>    </ul>    <pre>  <code class="language-objectivec">{      "buy": 0,      "downPrice": 0.0,      "feeType": 0,      "hasLyric": 0,      "id": 301500958,      "length": 0,      "listenPrice": 0.0,      "name": "第001集_回到古代当兽医",      "path": "http:\/\/kting.info:81\/asdb\/fiction\/chuanyue\/hdgddsy\/r4jigc2a.mp3",      "payType": 0,      "section": 1,      "size": 9913859  }</code></pre>    <ul>     <li>模型类.h文件</li>    </ul>    <pre>  <code class="language-objectivec">@interface BookModel : NSObject    /// 书名  @property (nonatomic,copy) NSString *name;  /// 音频下载地址  @property (nonatomic,copy) NSString *path;    + (instancetype)bookWithDict:(NSDictionary *)dict;    @end</code></pre>    <ul>     <li>模型类.m文件</li>    </ul>    <pre>  <code class="language-objectivec">+ (instancetype)bookWithDict:(NSDictionary *)dict  {      BookModel *book = [[BookModel alloc] init];        [book setValuesForKeysWithDictionary:dict];        return book;  }    - (void)setValue:(id)value forUndefinedKey:(NSString *)key  {}    - (NSString *)description  {      return [NSString stringWithFormat:@"%@ -- %@",self.name,self.path];  }</code></pre>    <ul>     <li>获取数据成功之后,实现网络数据转模型</li>    </ul>    <pre>  <code class="language-objectivec">- (void)loadBookList  {      // URL      NSURL *URL = [NSURL URLWithString:@"http://42.62.15.101/yyting/bookclient/ClientGetBookResource.action?bookId=30776&imei=OEVGRDQ1ODktRUREMi00OTU4LUE3MTctOUFGMjE4Q0JDMTUy&nwt=1&pageNum=1&pageSize=50&q=114≻=acca7b0f8bcc9603c25a52f572f3d863&sortType=0&token=KMSKLNNITOFYtR4iQHIE2cE95w48yMvrQb63ToXOFc8%2A"];        // session发起和启动任务      [[[NSURLSession sharedSession] dataTaskWithURL:URL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {            // 处理响应          if (error == nil && data != nil) {                // 反序列化              NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];              // 取出list字段对应的字典数组              NSArray *list = result[@"list"];                // 定义临时的可变数组              NSMutableArray *tmpM = [NSMutableArray arrayWithCapacity:list.count];                // 遍历字典数组,取字典,转模型              [list enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {                    // 字典转模型                  BookModel *book = [BookModel bookWithDict:obj];                  // 模型添加到临时数组                  [tmpM addObject:book];              }];                // 查看结果              NSLog(@"%@",tmpM);            } else {              NSLog(@"%@",error);          }        }] resume];  }</code></pre>    <h3>自定义cell</h3>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ee2bb374f6194685f129b4e16cd0e545.jpg"></p>    <ul>     <li>自定义cell类.h文件</li>    </ul>    <pre>  <code class="language-objectivec">#import <UIKit/UIKit.h>  #import "BookModel.h"    @interface BookCell : UITableViewCell    /// 接收VC传入的模型  @property (nonatomic,strong) BookModel *book;    @end</code></pre>    <ul>     <li>自定义cell类.m文件</li>    </ul>    <pre>  <code class="language-objectivec">#import "BookCell.h"    @interface BookCell ()    @property (weak, nonatomic) IBOutlet UILabel *nameLabel;  @property (weak, nonatomic) IBOutlet UIProgressView *progressView;    @end    @implementation BookCell    - (void)awakeFromNib {      // Initialization code  }    - (void)setBook:(BookModel *)book  {      _book = book;        self.nameLabel.text = book.name;  }    @end</code></pre>    <ul>     <li>控制器里面拿到数据源数组之后,刷新列表      <ul>       <li>注意 : NSURLSession的回调默认是在子线程异步执行的</li>       <li>所以 : 拿到数据源数组之后的刷新列表需要手动的回到主线程</li>      </ul> </li>    </ul>    <pre>  <code class="language-objectivec">- (void)loadBookList  {      // URL      NSURL *URL = [NSURL URLWithString:@"http://42.62.15.101/yyting/bookclient/ClientGetBookResource.action?bookId=30776&imei=OEVGRDQ1ODktRUREMi00OTU4LUE3MTctOUFGMjE4Q0JDMTUy&nwt=1&pageNum=1&pageSize=50&q=114≻=acca7b0f8bcc9603c25a52f572f3d863&sortType=0&token=KMSKLNNITOFYtR4iQHIE2cE95w48yMvrQb63ToXOFc8%2A"];        // session发起和启动任务      [[[NSURLSession sharedSession] dataTaskWithURL:URL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {            // 处理响应          if (error == nil && data != nil) {                // 反序列化              NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];              // 取出list字段对应的字典数组              NSArray *list = result[@"list"];                // 定义临时的可变数组              NSMutableArray *tmpM = [NSMutableArray arrayWithCapacity:list.count];                // 遍历字典数组,取字典,转模型              [list enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {                    // 字典转模型                  BookModel *book = [BookModel bookWithDict:obj];                  // 模型添加到临时数组                  [tmpM addObject:book];              }];                // 给数据源数组赋值              _bookList = tmpM.copy;              // 刷新列表              [[NSOperationQueue mainQueue] addOperationWithBlock:^{                  [self.tableView reloadData];              }];            } else {              NSLog(@"%@",error);          }        }] resume];  }</code></pre>    <ul>     <li>UITableViewDataSource</li>    </ul>    <pre>  <code class="language-objectivec">- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section  {      return _bookList.count;  }</code></pre>    <pre>  <code class="language-objectivec">- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath  {      BookCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BookCell" forIndexPath:indexPath];        // 获取cell对应的模型      BookModel *book = _bookList[indexPath.row];      // 给cell传递模型数据      cell.book = book;        return cell;  }</code></pre>    <h3>设置下载按钮</h3>    <ul>     <li>把cell系统的accessoryView设置成下载按钮</li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/5f19f437e30cd17f96be8cc0965abe28.jpg"></p>    <pre>  <code class="language-objectivec">- (void)awakeFromNib {        // 创建右侧下载按钮      UIButton *downloadBtn = [[UIButton alloc] init];      [downloadBtn setTitle:@"下载" forState:UIControlStateNormal];      [downloadBtn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];      [downloadBtn setTitle:@"暂停" forState:UIControlStateSelected];      [downloadBtn sizeToFit];        // 把cell系统的accessoryView设置成按钮      self.accessoryView = downloadBtn;        // 添加下载按钮监听事件      [downloadBtn addTarget:self action:@selector(downloadBtnClick:) forControlEvents:UIControlEventTouchUpInside];  }</code></pre>    <ul>     <li> <p>下载按钮点击事件</p>      <ul>       <li>改变按钮的状态,显示正确的文字</li>       <li>发送代理消息,让控制器去下载音频文件</li>      </ul> </li>     <li> <p>改变按钮的状态,显示正确的文字</p> <p>开发技巧提醒 : 在cell上添加按钮时,如何解决按钮状态因为cell的复用而复用的问题</p> <pre>  <code class="language-objectivec">方案一 : 自定义按钮,暴露一个按钮保存选中状态的BOOL属性`   `方案二 : 在模型里面,定义一个按钮保存选中状态的BOOL属性`</code></pre> </li>    </ul>    <p>此处选择方案二 : 模型类增加记录按钮选中状态的属性</p>    <pre>  <code class="language-objectivec">/// 记录按钮的选中状态  @property (nonatomic,assign) BOOL isSelected;</code></pre>    <p>按钮点击事件</p>    <pre>  <code class="language-objectivec">- (void)downloadBtnClick:(UIButton *)btn  {      // 1.改变按钮选中时的文字信息        // 这种改变按钮状态的方式,在cell复用时,状态也会复用      // btn.selected = !btn.isSelected;        // 1.1 使用模型记录按钮选中状态,避免cell复用时出问题      self.book.isSelected = !self.book.isSelected;        // 1.2 根据记录的状态设置title      NSString *title = (self.book.isSelected == YES) ? @"暂停" : @"下载";      [btn setTitle:title forState:UIControlStateNormal];        // 2.发送代理消息,让控制器去下载音频文件      if ([self.delegate respondsToSelector:@selector(downloadBtnClick:)]) {          [self.delegate downloadBtnClick:self];      }  }</code></pre>    <p>模型的setter里面解决按钮复用的问题</p>    <pre>  <code class="language-objectivec">- (void)setBook:(BookModel *)book  {      _book = book;        // 提示 : 解决cell滚动时,按钮状态复用的问题      // 取出按钮,根据之前点击时记录的状态,设置按钮的文字      UIButton *btn = (UIButton *)self.accessoryView;      NSString *title = (self.book.isSelected == YES) ? @"暂停" : @"下载";      [btn setTitle:title forState:UIControlStateNormal];        self.nameLabel.text = book.name;  }</code></pre>    <ul>     <li>发送代理消息,通知控制器去下载音频文件</li>    </ul>    <p>引入代理</p>    <pre>  <code class="language-objectivec">@interface ViewController () <UITableViewDataSource,BookCellDelegate>    @end</code></pre>    <p>遵守代理</p>    <pre>  <code class="language-objectivec">- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath  {      BookCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BookCell" forIndexPath:indexPath];        // 遵守代理      cell.delegate = self;        // 获取cell对应的模型      BookModel *book = _bookList[indexPath.row];      // 给cell传递模型数据      cell.book = book;        return cell;  }</code></pre>    <p>控制器实现代理方法</p>    <pre>  <code class="language-objectivec">- (void)downloadBtnClick:(BookCell *)cell  {      // 获取点击的是第几行cell      NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];        NSLog(@"你该去下载了 %zd",indexPath.row);  }</code></pre>    <h3>单例管理下载</h3>    <p>单例直接管理NSURLSession去实现下载功能</p>    <ul>     <li>单例类.h文件</li>    </ul>    <pre>  <code class="language-objectivec">@interface DownloadManager : NSObject    + (instancetype)sharedManager;    /**   *  单例下载的主方法   *   *  @param URLString       下载地址   *  @param progressBlock   下载进度回调   *  @param completionBlock 下载完成回调   */  - (void)downloadWithURLString:(NSString *)URLString progress:(void(^)(float progress))progressBlock completion:(void(^)(NSString *filePath))completionBlock;    @end</code></pre>    <ul>     <li>单例类.m文件</li>    </ul>    <p>引入NSURLSession代理</p>    <pre>  <code class="language-objectivec">@interface DownloadManager ()<NSURLSessionDownloadDelegate>    @end</code></pre>    <p>定义全局的下载session</p>    <pre>  <code class="language-objectivec">@implementation DownloadManager {        /// 全局下载的session      NSURLSession *_downloadSession;  }</code></pre>    <p>实现获取单例类方法</p>    <pre>  <code class="language-objectivec">+ (instancetype)sharedManager  {      static id instance;        static dispatch_once_t onceToken;      dispatch_once(&onceToken, ^{          instance = [[self alloc] init];      });        return instance;  }</code></pre>    <p>重写单例实例化方法:实例化全局的下载session</p>    <p>提示 : delegateQueue 如果传入nil,说明代理方法都在子线程执行</p>    <pre>  <code class="language-objectivec">- (instancetype)init  {      if (self = [super init]) {            // 全局下载session的配置信息          NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"HM"];          // 实例化全局下载session          _downloadSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];      }        return self;  }</code></pre>    <p>单例下载的主方法</p>    <pre>  <code class="language-objectivec">- (void)downloadWithURLString:(NSString *)URLString progress:(void (^)(float))progressBlock completion:(void (^)(NSString *))completionBlock  {      // URL      NSURL *URL = [NSURL URLWithString:URLString];      // 自定义session发起和启动任务      [[_downloadSession downloadTaskWithURL:URL] resume];  }</code></pre>    <ul>     <li>NSURLSessionDownloadDelegate</li>    </ul>    <p>监听文件下载进度</p>    <pre>  <code class="language-objectivec">- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask        didWriteData:(int64_t)bytesWritten   totalBytesWritten:(int64_t)totalBytesWritten  totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite  {      // 计算进度      float progress = (float)totalBytesWritten / totalBytesExpectedToWrite;      NSLog(@"DownloadManager 进度 %f",progress);  }</code></pre>    <p>监听文件下载完成</p>    <pre>  <code class="language-objectivec">- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask  didFinishDownloadingToURL:(NSURL *)location  {      NSLog(@"DownloadManager 文件下载完成 %@",location.path);  }</code></pre>    <ul>     <li>控制器调用单例的下载方法</li>    </ul>    <pre>  <code class="language-objectivec">- (void)downloadBtnClick:(BookCell *)cell  {      // 1.获取点击的是第几行cell      NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];      // 2.取出改行cell对应的模型数据      BookModel *book = _bookList[indexPath.row];        // 3.调用单例实现下载      [[DownloadManager sharedManager] downloadWithURLString:book.path progress:^(float progress) {    //        NSLog(@"VC 进度 %zd -- %f",indexPath.row,progress);        } completion:^(NSString *filePath) {    //        NSLog(@"VC 下载完成 %@",filePath);      }];  }</code></pre>    <ul>     <li>小结      <ul>       <li>接下来要做的事情 : 把对应的cell上的下载进度回调到控制器</li>       <li>存在的问题 : 如何区别哪个进度的回调是对应哪个cell的?</li>       <li>提示 : 不能用属性记录外界传入的progressBlock,后面的会覆盖前面的</li>      </ul> </li>    </ul>    <h3>错误的进度回调演示</h3>    <ul>     <li>定义回调下载进度的Block属性</li>    </ul>    <pre>  <code class="language-objectivec">@interface DownloadManager () <NSURLSessionDownloadDelegate>    /// 回调下载进度  @property (nonatomic,copy) void(^progressBlock)(float progress);    @end</code></pre>    <ul>     <li>调用单例下载文件的主方法时,记录这个外界传入的进度回调</li>    </ul>    <pre>  <code class="language-objectivec">- (void)downloadWithURLString:(NSString *)URLString progress:(void (^)(float))progressBlock completion:(void (^)(NSString *))completionBlock  {      // URL      NSURL *URL = [NSURL URLWithString:URLString];      // 自定义session发起和启动任务      [[_downloadSession downloadTaskWithURL:URL] resume];        // 保存VC传入的进度回调      self.progressBlock = progressBlock;  }</code></pre>    <ul>     <li>回调下载进度</li>    </ul>    <pre>  <code class="language-objectivec">- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask        didWriteData:(int64_t)bytesWritten   totalBytesWritten:(int64_t)totalBytesWritten  totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite  {      // 计算进度      float progress = (float)totalBytesWritten / totalBytesExpectedToWrite;  //    NSLog(@"DownloadManager 进度 %f",progress);        // 把下载进度回调到控制器      if (self.progressBlock) {          [[NSOperationQueue mainQueue] addOperationWithBlock:^{              self.progressBlock(progress);          }];      }  }</code></pre>    <ul>     <li>控制器里面调用单例下载的方法并传入进度回调</li>    </ul>    <pre>  <code class="language-objectivec">- (void)downloadBtnClick:(BookCell *)cell  {      // 1.获取点击的是第几行cell      NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];      // 2.取出改行cell对应的模型数据      BookModel *book = _bookList[indexPath.row];        // 3.调用单例实现下载      [[DownloadManager sharedManager] downloadWithURLString:book.path progress:^(float progress) {            NSLog(@"VC 进度 %zd -- %f",indexPath.row,progress);        } completion:^(NSString *filePath) {    //        NSLog(@"VC 下载完成 %@",filePath);      }];  }</code></pre>    <ul>     <li>结果分析      <ul>       <li>后面传入的进度回调把前面的进度回调覆盖了</li>       <li>解决办法 : 使用字典制作进度回调缓存池,记录哪个回调是属于哪个任务的</li>      </ul> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/41ebaef9b63bba834a09269daa600f9e.jpg"></p>    <h3>进度回调的保存提取和执行</h3>    <p>使用字典制作进度回调缓存池,记录哪个回调是属于哪个任务的</p>    <ul>     <li>制作进度回调缓存池</li>    </ul>    <pre>  <code class="language-objectivec">@implementation DownloadManager {        /// 全局下载的session      NSURLSession *_downloadSession;      /// 进度回调缓存池      NSMutableDictionary *_progressBlockDict;  }</code></pre>    <pre>  <code class="language-objectivec">/// 单例的实例化方法  - (instancetype)init  {      if (self = [super init]) {            // 实例化全局下载session          NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"HM"];          _downloadSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];            // 实例化进度回调缓存池          _progressBlockDict = [[NSMutableDictionary alloc] init];      }        return self;  }</code></pre>    <ul>     <li>在单例下载文件的主方法里面,把控制器传入的进度回调保存到缓存池      <ul>       <li>提示 : 以downloadTask作为key的目的是,需要在代理里面取这个回调,但是代理方法只能拿到downloadTask</li>      </ul> </li>    </ul>    <pre>  <code class="language-objectivec">- (void)downloadWithURLString:(NSString *)URLString progress:(void (^)(float))progressBlock completion:(void (^)(NSString *))completionBlock  {      // 1. URL      NSURL *URL = [NSURL URLWithString:URLString];        // 2. 自定义session发起和启动任务      NSURLSessionDownloadTask *downloadTask = [_downloadSession downloadTaskWithURL:URL];        // 3. 把VC传入的进度回调保存到缓存池      [_progressBlockDict setObject:progressBlock forKey:downloadTask];        // 4. 启动任务      [downloadTask resume];  }</code></pre>    <ul>     <li>回调各自下载任务的进度      <ul>       <li>使用步骤 :        <ul>         <li>计算进度</li>         <li>从缓存池取进度回调 (downloadTask作为key)</li>         <li>把下载进度回调到控制器</li>        </ul> </li>      </ul> </li>    </ul>    <pre>  <code class="language-objectivec">/// 监听文件下载进度  - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask        didWriteData:(int64_t)bytesWritten   totalBytesWritten:(int64_t)totalBytesWritten  totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite  {      // 1. 计算进度      float progress = (float)totalBytesWritten / totalBytesExpectedToWrite;  //    NSLog(@"DownloadManager 进度 %f",progress);        // 2. 从缓存池取进度回调      void (^progressBlock)(float) = [_progressBlockDict objectForKey:downloadTask];        // 3. 把下载进度回调到控制器      if (progressBlock) {          [[NSOperationQueue mainQueue] addOperationWithBlock:^{              progressBlock(progress);          }];      }  }</code></pre>    <ul>     <li>控制器里面调用单例下载的方法并传入进度回调</li>    </ul>    <pre>  <code class="language-objectivec">- (void)downloadBtnClick:(BookCell *)cell  {      // 1.获取点击的是第几行cell      NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];      // 2.取出改行cell对应的模型数据      BookModel *book = _bookList[indexPath.row];        // 3.调用单例实现下载      [[DownloadManager sharedManager] downloadWithURLString:book.path progress:^(float progress) {            NSLog(@"VC 进度 %zd -- %f",indexPath.row,progress);        } completion:^(NSString *filePath) {    //        NSLog(@"VC 下载完成 %@",filePath);      }];  }</code></pre>    <h3>进度展示</h3>    <ul>     <li>模型类增加记录文件下载进度的属性</li>    </ul>    <pre>  <code class="language-objectivec">/// 记录下载进度  @property (nonatomic,assign) float downloadProgress;</code></pre>    <ul>     <li>自定义cell类中,模型的setter方法</li>    </ul>    <pre>  <code class="language-objectivec">- (void)setBook:(BookModel *)book  {      _book = book;        // 提示 : 解决cell滚动时,按钮状态复用的问题      // 取出按钮,根据之前点击时记录的状态,设置按钮的文字      UIButton *btn = (UIButton *)self.accessoryView;      NSString *title = (self.book.isSelected == YES) ? @"暂停" : @"下载";      [btn setTitle:title forState:UIControlStateNormal];        self.nameLabel.text = book.name;      self.progressView.progress = book.downloadProgress;  }</code></pre>    <ul>     <li>控制器里面downloadBtnClick方法的实现      <ul>       <li>解决cell的复用造成的进度复用的问题</li>      </ul> </li>    </ul>    <pre>  <code class="language-objectivec">- (void)downloadBtnClick:(BookCell *)cell  {      // 1.获取点击的是第几行cell      NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];      // 2.取出该行cell对应的模型数据      BookModel *book = _bookList[indexPath.row];        // 3.调用单例实现下载      [[DownloadManager sharedManager] downloadWithURLString:book.path progress:^(float progress) {            NSLog(@"VC 进度 %zd -- %f",indexPath.row,progress);            // 如果正在下载,当滚动cell时,cell会复用;          // 提示 : indexPath不会复用,可以通过选中cell时产生的indexPath,获取正确的cell          BookCell *updateCell = [self.tableView cellForRowAtIndexPath:indexPath];            // cell的模型变化了cell也会变化 : MVC的特点          book.downloadProgress = progress;          // 给cell赋值新的模型          updateCell.book = book;        } completion:^(NSString *filePath) {    //        NSLog(@"VC 下载完成 %@",filePath);      }];  }</code></pre>    <h3>下载完成的回调</h3>    <ul>     <li>使用字典制作下载完成回调的缓存池</li>    </ul>    <pre>  <code class="language-objectivec">@implementation DownloadManager {        /// 全局下载的session      NSURLSession *_downloadSession;      /// 进度回调缓存池      NSMutableDictionary *_progressBlockDict;      /// 完成回调缓存池      NSMutableDictionary *_completionBlockDict;  }</code></pre>    <pre>  <code class="language-objectivec">// 单例的实例化方法  - (instancetype)init  {      if (self = [super init]) {            // 实例化全局下载session          NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"HM"];          _downloadSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];            // 实例化进度回调缓存池 / 完成回调缓存池          _progressBlockDict = [[NSMutableDictionary alloc] init];          _completionBlockDict = [[NSMutableDictionary alloc] init];      }        return self;  }</code></pre>    <ul>     <li>把控制器传入的下载完成的回调保存到缓存池</li>    </ul>    <pre>  <code class="language-objectivec">- (void)downloadWithURLString:(NSString *)URLString progress:(void (^)(float))progressBlock completion:(void (^)(NSString *))completionBlock  {      // 1. URL      NSURL *URL = [NSURL URLWithString:URLString];        // 2. 自定义session发起和启动任务      NSURLSessionDownloadTask *downloadTask = [_downloadSession downloadTaskWithURL:URL];        // 3. 把VC传入的进度回调 / 完成回调 保存到缓存池      // 提示 : 以downloadTask作为key的目的是,需要在代理里面取这个回调,但是代理方法只有downloadTask      [_progressBlockDict setObject:progressBlock forKey:downloadTask];      [_completionBlockDict setObject:completionBlock forKey:downloadTask];        // 4. 启动任务      [downloadTask resume];  }</code></pre>    <ul>     <li>NSURLSession的下载完成的代理方法      <ul>       <li>把下载完成的音频文件缓存到沙盒</li>       <li>取出下载完成回调</li>       <li>回调路径到控制器</li>       <li>下载完成之后把相关缓存池清空</li>      </ul> </li>    </ul>    <pre>  <code class="language-objectivec">- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask  didFinishDownloadingToURL:(NSURL *)location  {  //    NSLog(@"DownloadManager 文件下载完成 %@",location.path);        // 1.把下载完成的音频文件缓存到沙盒      NSString *URLString = downloadTask.currentRequest.URL.absoluteString;      NSString *fileName = [URLString lastPathComponent];      NSString *savePath = [NSString stringWithFormat:@"/Users/zhangjie/Desktop/%@",fileName];      [[NSFileManager defaultManager] copyItemAtPath:location.path toPath:savePath error:NULL];        // 2.取出下载完成回调      void(^completionBlock)(NSString *) = [_completionBlockDict objectForKey:downloadTask];        // 3.回调路径到控制器      if (completionBlock) {          [[NSOperationQueue mainQueue] addOperationWithBlock:^{              completionBlock(savePath);          }];      }        // 4.下载完成之后把相关缓存池清空      [_progressBlockDict removeObjectForKey:downloadTask];      [_completionBlockDict removeObjectForKey:downloadTask];  }</code></pre>    <h3>判断是否正在下载</h3>    <ul>     <li>思路      <ul>       <li>先准备下载任务缓存池</li>       <li>每创建一个下载任务,就把下载任务添加到这个缓存池</li>       <li>控制器在建立下载任务之前,先判断要建立的下载任务在缓存池有没有</li>       <li>如果要建立的下载任务在缓存池中有,就执行暂停.反之,就下载</li>       <li>下载任务完成或者暂停下载之后,需要把任务从缓存池移除掉</li>      </ul> </li>    </ul>    <ul>     <li>单例里面需要实现的</li>    </ul>    <p>单例增加方法 判断是否正在下载</p>    <pre>  <code class="language-objectivec">/**   *  检查是否正在下载   *   *  @param URLString 下载地址   *   *  @return 返回是否正在下载   */  - (BOOL)checkIsDownloadingWithURLString:(NSString *)URLString;</code></pre>    <p>判断是否正在下载方法的实现</p>    <pre>  <code class="language-objectivec">- (BOOL)checkIsDownloadingWithURLString:(NSString *)URLString  {      if ([_downlaodTaskDict objectForKey:URLString] != nil) {          return YES;      }        return NO;  }</code></pre>    <p>保存下载任务到缓存池</p>    <pre>  <code class="language-objectivec">- (void)downloadWithURLString:(NSString *)URLString progress:(void (^)(float))progressBlock completion:(void (^)(NSString *))completionBlock  {      // 1. URL      NSURL *URL = [NSURL URLWithString:URLString];        // 2. 自定义session发起和启动任务      NSURLSessionDownloadTask *downloadTask = [_downloadSession downloadTaskWithURL:URL];        // 3. 把VC传入的进度回调 / 完成回调 保存到缓存池      // 提示 : 以downloadTask作为key的目的是,需要在代理里面取这个回调,但是代理方法只有downloadTask      [_progressBlockDict setObject:progressBlock forKey:downloadTask];      [_completionBlockDict setObject:completionBlock forKey:downloadTask];        // 4.把下载任务添加到缓存池      [_downlaodTaskDict setObject:downloadTask forKey:URLString];        // 5. 启动任务      [downloadTask resume];  }</code></pre>    <p>下载完成之后,移除相关的缓存池</p>    <pre>  <code class="language-objectivec">- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask  didFinishDownloadingToURL:(NSURL *)location  {  //    NSLog(@"DownloadManager 文件下载完成 %@",location.path);        // 1.把下载完成的音频文件缓存到沙盒      NSString *URLString = downloadTask.currentRequest.URL.absoluteString;      NSString *fileName = [URLString lastPathComponent];      NSString *savePath = [NSString stringWithFormat:@"/Users/zhangjie/Desktop/%@",fileName];      [[NSFileManager defaultManager] copyItemAtPath:location.path toPath:savePath error:NULL];        // 2.取出下载完成回调      void(^completionBlock)(NSString *) = [_completionBlockDict objectForKey:downloadTask];        // 3.回调路径到控制器      if (completionBlock) {          [[NSOperationQueue mainQueue] addOperationWithBlock:^{              completionBlock(savePath);          }];      }        // 4.下载完成之后把相关缓存池清空      [_progressBlockDict removeObjectForKey:downloadTask];      [_completionBlockDict removeObjectForKey:downloadTask];      [_downlaodTaskDict removeObjectForKey:URLString];  }</code></pre>    <ul>     <li>控制器里面,在建立下载任务之前,先判断是否正在下载</li>    </ul>    <pre>  <code class="language-objectivec">- (void)downloadBtnClick:(BookCell *)cell  {      // 1.获取点击的是第几行cell      NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];      // 2.取出该行cell对应的模型数据      BookModel *book = _bookList[indexPath.row];        // 3.判断是否正在下载      BOOL isDownloading = [[DownloadManager sharedManager] checkIsDownloadingWithURLString:book.path];        if (!isDownloading) {          // 3.调用单例实现下载          [[DownloadManager sharedManager] downloadWithURLString:book.path progress:^(float progress) {                NSLog(@"VC 进度 %zd -- %f",indexPath.row,progress);                // 如果正在下载,当滚动cell时,cell会复用;              // 提示 : indexPath不会复用,可以通过选中cell时产生的indexPath,获取正确的cell              BookCell *updateCell = [self.tableView cellForRowAtIndexPath:indexPath];                // cell的模型变化了cell也会变化 : MVC的特点              book.downloadProgress = progress;              // 给cell赋值新的模型              updateCell.book = book;            } completion:^(NSString *filePath) {                NSLog(@"VC 下载完成 %@",filePath);          }];      } else {          NSLog(@"暂停");      }  }</code></pre>    <h3>暂停下载</h3>    <ul>     <li>单例准备暂停下载的主方法</li>    </ul>    <pre>  <code class="language-objectivec">/**   *  暂停下载的主方法   *   *  @param URLString 暂停下载的地址   */  - (void)pauseDownloadWithURLString:(NSString *)URLString pauseBlock:(void(^)())pauseBlock;</code></pre>    <ul>     <li>暂停下载的主方法实现      <ul>       <li>取出正在下载的任务</li>       <li>暂停这个正在下载的任务</li>       <li>把续传数据缓存到沙盒</li>       <li>清空相关缓存池</li>       <li>把暂停的结果传回vc</li>      </ul> </li>    </ul>    <pre>  <code class="language-objectivec">- (void)pauseDownloadWithURLString:(NSString *)URLString pauseBlock:(void (^)())pauseBlock  {      // 1.取出正在下载的任务      NSURLSessionDownloadTask *downloadTask = [_downlaodTaskDict objectForKey:URLString];        // 2.暂停这个正在下载的任务      if (downloadTask) {            [downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {              // 3.把续传数据缓存到沙盒              [resumeData writeToFile:[URLString appendTempPath] atomically:YES];                // 4.清空相关缓存池              [_progressBlockDict removeObjectForKey:downloadTask];              [_completionBlockDict removeObjectForKey:downloadTask];              [_downlaodTaskDict removeObjectForKey:URLString];                // 5.把暂停的结果回到到VC              if (pauseBlock) {                  pauseBlock();              }          }];      }  }</code></pre>    <ul>     <li>控制器中使用单例的暂停方法</li>    </ul>    <pre>  <code class="language-objectivec">- (void)downloadBtnClick:(BookCell *)cell  {      // 1.获取点击的是第几行cell      NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];      // 2.取出该行cell对应的模型数据      BookModel *book = _bookList[indexPath.row];        // 3.判断是否正在下载      BOOL isDownloading = [[DownloadManager sharedManager] checkIsDownloadingWithURLString:book.path];        if (!isDownloading) {          // 3.调用单例实现下载          [[DownloadManager sharedManager] downloadWithURLString:book.path progress:^(float progress) {                NSLog(@"VC 进度 %zd -- %f",indexPath.row,progress);                // 如果正在下载,当滚动cell时,cell会复用;              // 提示 : indexPath不会复用,可以通过选中cell时产生的indexPath,获取正确的cell              BookCell *updateCell = [self.tableView cellForRowAtIndexPath:indexPath];                // cell的模型变化了cell也会变化 : MVC的特点              book.downloadProgress = progress;              // 给cell赋值新的模型              updateCell.book = book;            } completion:^(NSString *filePath) {                NSLog(@"VC 下载完成 %@",filePath);          }];      } else {          [[DownloadManager sharedManager] pauseDownloadWithURLString:book.path pauseBlock:^{              NSLog(@"暂停成功");          }];      }  }</code></pre>    <h3>继续下载</h3>    <ul>     <li>继续下载思路      <ul>       <li>如果有续传数据就继续下载</li>       <li>如果没有续传数据就新建下载任务从头开始下载</li>      </ul> </li>    </ul>    <pre>  <code class="language-objectivec">- (void)downloadWithURLString:(NSString *)URLString progress:(void (^)(float))progressBlock completion:(void (^)(NSString *))completionBlock  {      // 1. URL      NSURL *URL = [NSURL URLWithString:URLString];        // 获取续传数据      NSData *resumeData = [NSData dataWithContentsOfFile:[URLString appendTempPath]];        // 3. 自定义session发起和启动任务      NSURLSessionDownloadTask *downloadTask;        // 3.1 如果有续传数据就继续下载      if (resumeData != nil) {            downloadTask = [_downloadSession downloadTaskWithResumeData:resumeData];            // 注意 : 续传数据使用完一定要及时的移除,再次暂停时会重新的生成续传数据!!!          [[NSFileManager defaultManager] removeItemAtPath:[URLString appendTempPath] error:NULL];      } else {          // 3.2 如果没有续传数据就重新下载          downloadTask = [_downloadSession downloadTaskWithURL:URL];      }        // 4. 把VC传入的进度回调 / 完成回调 保存到缓存池      // 提示 : 以downloadTask作为key的目的是,需要在代理里面取这个回调,但是代理方法只有downloadTask      [_progressBlockDict setObject:progressBlock forKey:downloadTask];      [_completionBlockDict setObject:completionBlock forKey:downloadTask];        // 5.把下载任务添加到缓存池      [_downlaodTaskDict setObject:downloadTask forKey:URLString];        // 6. 启动任务      [downloadTask resume];  }</code></pre>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/7540dd1935b4</p>    <p> </p>