iOS多任務下載的簡要概述

AliciaPrend 7年前發布 | 8K 次閱讀 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?=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?=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?=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];
}

 

 

來自:http://www.jianshu.com/p/7540dd1935b4

 

 本文由用戶 AliciaPrend 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!