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>