iOS多任务下载的简要概述
2017-02-27 本文已影响475人
MiracleGl
多任务下载顾名思义就是多个任务同时下载,各个任务在同一时间一起下载,比如迅雷等下载软件就具备这些功能,而iOS开发中也涉及到了一些多任务下载。本文就是一个多任务下载的简要概述,如有错误请见谅。
- 获取网络数据
- 网络数据转模型
- 自定义cell
- 设置下载按钮
- 单例管理下载
- 错误的进度回调演示
- 进度回调的保存提取和执行
- 进度展示
- 下载完成的回调
- 判断是否正在下载
- 暂停下载
- 继续下载
导图
获取网络数据
- 获取电子书列表数据的主方法
- (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&sc=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];
}
网络数据转模型
JSON数据示例
{
"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
}
模型类.h文件
@interface BookModel : NSObject
/// 书名
@property (nonatomic,copy) NSString *name;
/// 音频下载地址
@property (nonatomic,copy) NSString *path;
+ (instancetype)bookWithDict:(NSDictionary *)dict;
@end
模型类.m文件
+ (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];
}
获取数据成功之后,实现网络数据转模型
- (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&sc=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];
}
自定义cell
自定义cell类.h文件
#import <UIKit/UIKit.h>
#import "BookModel.h"
@interface BookCell : UITableViewCell
/// 接收VC传入的模型
@property (nonatomic,strong) BookModel *book;
@end
自定义cell类.m文件
#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
-
控制器里面拿到数据源数组之后,刷新列表
注意 : NSURLSession的回调默认是在子线程异步执行的
所以 : 拿到数据源数组之后的刷新列表需要手动的回到主线程
- (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&sc=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];
}
UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _bookList.count;
}
- (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;
}
设置下载按钮
把cell系统的accessoryView设置成下载按钮
- (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];
}
-
下载按钮点击事件
改变按钮的状态,显示正确的文字
发送代理消息,让控制器去下载音频文件
-
改变按钮的状态,显示正确的文字
开发技巧提醒 : 在cell上添加按钮时,如何解决按钮状态因为cell的复用而复用的问题
方案一 : 自定义按钮,暴露一个按钮保存选中状态的BOOL属性
方案二 : 在模型里面,定义一个按钮保存选中状态的BOOL属性
此处选择方案二 : 模型类增加记录按钮选中状态的属性
/// 记录按钮的选中状态
@property (nonatomic,assign) BOOL isSelected;
按钮点击事件
- (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];
}
}
模型的setter里面解决按钮复用的问题
- (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;
}
发送代理消息,通知控制器去下载音频文件
引入代理
@interface ViewController () <UITableViewDataSource,BookCellDelegate>
@end
遵守代理
- (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;
}
控制器实现代理方法
- (void)downloadBtnClick:(BookCell *)cell
{
// 获取点击的是第几行cell
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
NSLog(@"你该去下载了 %zd",indexPath.row);
}
单例管理下载
单例直接管理NSURLSession去实现下载功能
单例类.h文件
@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
单例类.m文件
引入NSURLSession代理
@interface DownloadManager ()<NSURLSessionDownloadDelegate>
@end
定义全局的下载session
@implementation DownloadManager {
/// 全局下载的session
NSURLSession *_downloadSession;
}
实现获取单例类方法
+ (instancetype)sharedManager
{
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
重写单例实例化方法:实例化全局的下载session
提示 : delegateQueue 如果传入nil,说明代理方法都在子线程执行
- (instancetype)init
{
if (self = [super init]) {
// 全局下载session的配置信息
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"HM"];
// 实例化全局下载session
_downloadSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
}
return self;
}
单例下载的主方法
- (void)downloadWithURLString:(NSString *)URLString progress:(void (^)(float))progressBlock completion:(void (^)(NSString *))completionBlock
{
// URL
NSURL *URL = [NSURL URLWithString:URLString];
// 自定义session发起和启动任务
[[_downloadSession downloadTaskWithURL:URL] resume];
}
NSURLSessionDownloadDelegate
监听文件下载进度
- (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);
}
监听文件下载完成
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
NSLog(@"DownloadManager 文件下载完成 %@",location.path);
}
控制器调用单例的下载方法
- (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);
}];
}
-
小结
接下来要做的事情 : 把对应的cell上的下载进度回调到控制器
存在的问题 : 如何区别哪个进度的回调是对应哪个cell的?
提示 : 不能用属性记录外界传入的progressBlock,后面的会覆盖前面的
错误的进度回调演示
定义回调下载进度的Block属性
@interface DownloadManager () <NSURLSessionDownloadDelegate>
/// 回调下载进度
@property (nonatomic,copy) void(^progressBlock)(float progress);
@end
调用单例下载文件的主方法时,记录这个外界传入的进度回调
- (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;
}
回调下载进度
- (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);
}];
}
}
控制器里面调用单例下载的方法并传入进度回调
- (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);
}];
}
-
结果分析
后面传入的进度回调把前面的进度回调覆盖了
解决办法 : 使用字典制作进度回调缓存池,记录哪个回调是属于哪个任务的
进度回调的保存提取和执行
使用字典制作进度回调缓存池,记录哪个回调是属于哪个任务的
制作进度回调缓存池
@implementation DownloadManager {
/// 全局下载的session
NSURLSession *_downloadSession;
/// 进度回调缓存池
NSMutableDictionary *_progressBlockDict;
}
/// 单例的实例化方法
- (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;
}
-
在单例下载文件的主方法里面,把控制器传入的进度回调保存到缓存池
提示 : 以downloadTask作为key的目的是,需要在代理里面取这个回调,但是代理方法只能拿到downloadTask
- (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];
}
-
回调各自下载任务的进度
- 使用步骤 :
- 计算进度
- 从缓存池取进度回调 (downloadTask作为key)
- 把下载进度回调到控制器
- 使用步骤 :
/// 监听文件下载进度
- (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);
}];
}
}
控制器里面调用单例下载的方法并传入进度回调
- (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);
}];
}
进度展示
模型类增加记录文件下载进度的属性
/// 记录下载进度
@property (nonatomic,assign) float downloadProgress;
自定义cell类中,模型的setter方法
- (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;
}
-
控制器里面downloadBtnClick方法的实现
解决cell的复用造成的进度复用的问题
- (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);
}];
}
下载完成的回调
使用字典制作下载完成回调的缓存池
@implementation DownloadManager {
/// 全局下载的session
NSURLSession *_downloadSession;
/// 进度回调缓存池
NSMutableDictionary *_progressBlockDict;
/// 完成回调缓存池
NSMutableDictionary *_completionBlockDict;
}
// 单例的实例化方法
- (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;
}
把控制器传入的下载完成的回调保存到缓存池
- (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];
}
-
NSURLSession的下载完成的代理方法
把下载完成的音频文件缓存到沙盒
取出下载完成回调
回调路径到控制器
下载完成之后把相关缓存池清空
- (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];
}
判断是否正在下载
- 思路
- 先准备下载任务缓存池
- 每创建一个下载任务,就把下载任务添加到这个缓存池
- 控制器在建立下载任务之前,先判断要建立的下载任务在缓存池有没有
- 如果要建立的下载任务在缓存池中有,就执行暂停.反之,就下载
- 下载任务完成或者暂停下载之后,需要把任务从缓存池移除掉
单例里面需要实现的
单例增加方法 判断是否正在下载
/**
* 检查是否正在下载
*
* @param URLString 下载地址
*
* @return 返回是否正在下载
*/
- (BOOL)checkIsDownloadingWithURLString:(NSString *)URLString;
判断是否正在下载方法的实现
- (BOOL)checkIsDownloadingWithURLString:(NSString *)URLString
{
if ([_downlaodTaskDict objectForKey:URLString] != nil) {
return YES;
}
return NO;
}
保存下载任务到缓存池
- (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];
}
下载完成之后,移除相关的缓存池
- (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];
}
控制器里面,在建立下载任务之前,先判断是否正在下载
- (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(@"暂停");
}
}
暂停下载
单例准备暂停下载的主方法
/**
* 暂停下载的主方法
*
* @param URLString 暂停下载的地址
*/
- (void)pauseDownloadWithURLString:(NSString *)URLString pauseBlock:(void(^)())pauseBlock;
-
暂停下载的主方法实现
取出正在下载的任务
暂停这个正在下载的任务
把续传数据缓存到沙盒
清空相关缓存池
把暂停的结果传回vc
- (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();
}
}];
}
}
控制器中使用单例的暂停方法
- (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(@"暂停成功");
}];
}
}
继续下载
-
继续下载思路
如果有续传数据就继续下载
如果没有续传数据就新建下载任务从头开始下载
- (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];
}
感谢读到最后的朋友,最后祝大家工作顺利,请点赞支持一下,谢谢!