iOS多任务下载的简要概述

多任务下载顾名思义就是多个任务同时下载,各个任务在同一时间一起下载,比如迅雷等下载软件就具备这些功能,而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];
}

感谢读到最后的朋友,最后祝大家工作顺利,请点赞支持一下,谢谢!

推荐阅读更多精彩内容