你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 手把手教你封裝下載管理器

手把手教你封裝下載管理器

編輯:IOS開發基礎

img-(18).jpg

轉自:標哥的技術博客

概述

即將要做一個有點技術含量的項目,其中一個小技術點就是視頻上傳、下載,在項目開始前,就需要做一下下技術調研,並寫出相應的demo。

本篇文章是針對所設計的demo而寫的,只有下載的功能。當然,這個demo只是最簡單版的,不考慮耦合性,只考慮是否可實現的問題。

高手也可以看看,最好在閱讀之後可以將自己的想法在評論中寫出來,交流交流各自封裝的思想。如果您不會寫,也可以參考參考,相信也會有所收獲!

目錄

第一節:功能說明

第二節:設計理念

第三節:如何設計整個下載管理器

第四節:子類化NSOperation

第五節:反饋到UI展示進度及狀態提示

第六節:設計管理下載類

第七節:小結

第八節:下載demo

效果圖

沒有效果圖就沒有閱讀完本篇文章的勇氣,給大家打打氣,繼續閱讀吧!

QQ20160526-4@2x320.jpg

第一節:功能說明

首先,本篇文章教大家寫一個最簡單的下載管理器,不包含上傳管理器。不過,上傳管理器與下載管理器是一樣的,後面會拋磚引玉,大家可以各自去嘗試!

本篇文章所講解的下載管理器具備以下功能:

  • 開始下載某個視頻

  • 掛起某個視頻下載(暫停下載)

  • 恢復某個視頻下載(繼續下載)

  • 可設置下載最大並發量

  • 添加到下載隊列

以下便是最基本的功能了,那麼我們就根據這幾個基本功能來實現。至於要做到後台自動下載及退出App,下次進入再自動恢復到上一次退出的狀態的,這些不在本demo范圍之內!

為了demo的簡單,一切從簡!

第二節:設計理念

  • 設計理念通常都希望簡單使用且易擴展易維護

  • 與具體的下載類型無關,比如不管是視頻下載還是音頻下載又或是普通文件下載,都沒有關系,都可通用

  • 單個下載應保持功能的單一性,專心做一件事

第三節:如何設計整個下載管理器

  • 考慮到需要記錄進度及狀態,所以一旦開啟下載,整個app過程中都會存在,可考慮使用單例,也可以考慮非單例,但是非單例模式也得保證只創建一遍並交給appDelegate持有,其實與單例設計相當的。為了簡化,這裡采用的是單例設計。所以,下載管理器以單例形式存在。

  • 考慮到需要處理並發下載問題,因此使用NSOperationQueue

  • 考慮到下載類的功能單一性,采用子類化NSOperation

  • 考慮到使用下載功能與文件類型無關,可定義協議,使model必須遵守,比如豆瓣開源的DOUAudioStreamer就是采用這種方式來實現

但是,為了demo的簡單,這裡沒有定義協議,直接使用model了。大家可以在真正設計時,采用協議的式,以支持任意model。筆者在項目中真正去寫的時候,也會采用協議的方式,支持下載、上傳做任意類型的文件,包括視頻、音頻等。

本demo中,主要設計以下幾個類:

  • HYBVideoOperation:子類化的NSOperation,用於專門做下載

  • HYBVideoModel:視頻下載數據模型,包括視頻下載地址、存儲地址、進度、狀態等,並持有HYBVideoOperation,以方便管理

  • HYBVideoManager:下載管理器,管理所有的HYBVideoModel

然後,我們還需要與UI交互,所以在cell中需要model。HYBVideoCell類為cell,強引用model!

那麼,這整個交互是這樣的:

  • HYBVideoManager —–>管理所有的HYBVideoModel

  • 每個HYBVideoModel—–>持有一個HYBVideoOperation

  • HYBVideoOperation—->弱持有一個HYBVideoModel

  • HYBVideoCell —–>持有一個HYBVideoModel,當進度或狀態變化時,更新UI

所設計的回調全放在HYBVideoModel中,當HYBVideoModel的進度屬性值和狀態值發生變化時反饋到UI變化上!

第四節:子類化NSOperation

關於子類化NSOperation需要做哪些事件,最好還是先閱讀筆者之前所寫的一篇文章NSOperation/Queue,不過下面我也會列出一些要點:

  • 重寫start方法時,要做好isCannelled的判斷

  • 重寫isExecuting、isFinished、isConcurrent

  • 重寫cancel,並處理好isCancelled KVO處理

我們設計Operation時,采用NSURLSession實現下載,通過控制NSURLSessionDownloadTask,可實現下載、暫停下載和斷點下載功能。

我們整個頭文件的設計為:

@class HYBVideoModel;
@interface NSURLSessionTask (VideoModel)
// 為了更方便去獲取,而不需要遍歷,采用擴展的方式,可直接提取,提高效率
@property (nonatomic, weak) HYBVideoModel *hyb_videoModel;
@end
@interface HYBVideoOperation : NSOperation
- (instancetype)initWithModel:(HYBVideoModel *)model session:(NSURLSession *)session;
@property (nonatomic, weak) HYBVideoModel *model;
// 可以不公開此屬性
@property (nonatomic, strong, readonly) NSURLSessionDownloadTask *downloadTask;
- (void)suspend;
- (void)resume;
- (void)downloadFinished;
@end

這裡還擴展了NSURLSessionTask,將模型與之關聯,注意采用弱引用哦!我不知道這樣設計是否合理,但是我個人認為這麼設計的好處是:接口簡單,與外部沒有直接的聯系,session來源於下載管理類,這樣可統一管理。

當下載完成之後,一定要回調downloadFinished,目的是讓任務退隊。要讓任務退隊,只有保證isFinished為YES才能退隊!

[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_executing = NO;
_finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];

因為任務完成還可以重新下載,通常情況下不會自動退隊。

第五節:反饋到UI展示進度及狀態提示

我們通過模型來反饋到UI上,在進度和狀態變化時,可以回調來更新UI。

首先,下載過程有很多種狀態,我們定義成枚舉:

typedef NS_ENUM(NSInteger, HYBVideoStatus) {
  kHYBVideoStatusNone = 0,       // 初始狀態
  kHYBVideoStatusRunning = 1,    // 下載中
  kHYBVideoStatusSuspended = 2,  // 下載暫停
  kHYBVideoStatusCompleted = 3,  // 下載完成
  kHYBVideoStatusFailed  = 4,    // 下載失敗
  kHYBVideoStatusWaiting = 5    // 等待下載
 };

設計屬性:

typedef void(^HYBVideoStatusChanged)(HYBVideoModel *model);
typedef void(^HYBVideoProgressChanged)(HYBVideoModel *model);
@interface HYBVideoModel : NSObject
@property (nonatomic, copy) NSString *videoId;
@property (nonatomic, copy) NSString *videoUrl;
@property (nonatomic, copy) NSString *imageUrl;
@property (nonatomic, copy) NSString *title;
// 用於斷點下載記錄,其實應該要存儲到文件中,然後記錄路徑,但是為了簡單,demo就不這麼做了
@property (nonatomic, strong) NSData *resumeData;
// 下載後存儲到此處
@property (nonatomic, copy) NSString *localPath;
@property (nonatomic, copy) NSString *progressText;
// 非常關鍵的屬性,進度變化會自動回調onProgressChanged
@property (nonatomic, assign) CGFloat progress;
// 狀態變化會自動回調onStatusChanged
@property (nonatomic, assign) HYBVideoStatus status;
// 這裡為什麼要引用operation且是強引用?因為管理器直接管理的是model,
// 而真正做下載任務的是operation。
// 為什麼沒有將這兩個分別作為屬性呢?為了整體更簡單!
@property (nonatomic, strong) HYBVideoOperation *operation;
@property (nonatomic, copy) HYBVideoStatusChanged onStatusChanged;
@property (nonatomic, copy) HYBVideoProgressChanged onProgressChanged;
@property (nonatomic, readonly, copy) NSString *statusText;
@end

當然,不同的人來設計,可能會有不同的方式。我分析過好幾種設計方式,但是列出來的好處,不如這一種。

當進度或者狀態變化時,自動地回調:

- (void)setProgress:(CGFloat)progress {
  if (_progress != progress) {
    _progress = progress;
    if (self.onProgressChanged) {
      self.onProgressChanged(self);
    } else {
      NSLog(@"progress changed block is empty");
    }
  }
}
- (void)setStatus:(HYBVideoStatus)status {
  if (_status != status) {
    _status = status;
    if (self.onStatusChanged) {
      self.onStatusChanged(self);
    }
  }
}

這樣回調與下載管理類及下載類都沒有直接的關系了,而model的回調直接反饋到UI層了!

在配置cell時,如下即可實時展示進度及狀態提示:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  HYBVideoCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier
                                                       forIndexPath:indexPath];
  HYBVideoModel *model = [HYBVideoManager shared].videoModels[indexPath.row];
  cell.model = model;
  model.onStatusChanged = ^(HYBVideoModel *changedModel) {
    cell.model = changedModel;
  };
  model.onProgressChanged = ^(HYBVideoModel *changedModel) {
    cell.model = changedModel;
  };
  return cell;
}

當我們點擊某一個cell進入下載或者暫停之類的操作時,如下:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
   HYBVideoModel *model = [HYBVideoManager shared].videoModels[indexPath.row];
  switch (model.status) {
    case kHYBVideoStatusNone: {
      [[HYBVideoManager shared] startWithVideoModel:model];
      break;
    }
    case kHYBVideoStatusRunning: {
      [[HYBVideoManager shared] suspendWithVideoModel:model];
      break;
    }
    case kHYBVideoStatusSuspended: {
     [[HYBVideoManager shared] resumeWithVideoModel:model];
      break;
    }
    case kHYBVideoStatusCompleted: {
      NSLog(@"已下載完成,可以播放了,播放路徑:%@", model.localPath);
      break;
    }
    case kHYBVideoStatusFailed: {
      [[HYBVideoManager shared] resumeWithVideoModel:model];
      break;
    }
    case kHYBVideoStatusWaiting: {
      [[HYBVideoManager shared] startWithVideoModel:model];
      break;
    }
  }
}

在UI層是否是使用簡單呢?從整體來看,使用者可非常簡單地調用實現功能。

第六節:設計管理下載類

我們所設計的管理下載類采用的是單例設計模式,而所有操作都直接與model關聯,對於外部都沒有具體地與operation關聯。當然,在項目中,最好不要直接使用這樣的模型。筆者在前面的設計理念中講到,我們可以采用協議的方式來實現,然後讓model遵守協議,這樣就能做到支持任意類型的model。

@class HYBVideoModel;
@interface HYBVideoManager : NSObject
@property (nonatomic, readonly, strong) NSArray *videoModels;
+ (instancetype)shared;
// 添加視頻模型,只是添加並不會下載
- (void)addVideoModels:(NSArray *)videoModels;
// 開始下載某個視頻
- (void)startWithVideoModel:(HYBVideoModel *)videoModel;
// 掛起
- (void)suspendWithVideoModel:(HYBVideoModel *)videoModel;
// 恢復下載
- (void)resumeWithVideoModel:(HYBVideoModel *)videoModel;
// 忽略這個,暫時沒有使用到
- (void)stopWiethVideoModel:(HYBVideoModel *)videoModel;
@end

我們在初始化時,創建隊列及session:

self.queue = [[NSOperationQueue alloc] init];
self.queue.maxConcurrentOperationCount = 4;
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
// 不能傳self.queue
self.session = [NSURLSession sessionWithConfiguration:config
          delegate:self
          delegateQueue:nil];

我們要注意的是delegateQueue不能傳self.queue。起初我傳過去了,導致超過設定的並發數量就不能下載了,就一直不動了,原因就是傳了self.queue。

為什麼不能傳呢?因為我們是自定義的operation,而當使用session後,每個任務創建都會自動添加一個NSBlockOperation類型對象到隊列中,而任務完成並不會自動退隊,也就是狀態就沒有進入完成狀態,從而導致其他任務都被限制在並發處,不能繼續下載。

下面我們來看看開始下載、暫停下載、恢復下載API:

- (void)startWithVideoModel:(HYBVideoModel *)videoModel {
  if (videoModel.status != kHYBVideoStatusCompleted) {
    videoModel.status = kHYBVideoStatusRunning;
    if (videoModel.operation == nil) {
      videoModel.operation = [[HYBVideoOperation alloc] initWithModel:videoModel
                     session:self.session];
      [self.queue addOperation:videoModel.operation];
      [videoModel.operation start];
    } else {
      [videoModel.operation resume];
    }
  }
}
- (void)suspendWithVideoModel:(HYBVideoModel *)videoModel {
  if (videoModel.status != kHYBVideoStatusCompleted) {
    [videoModel.operation suspend];
  }
}
- (void)resumeWithVideoModel:(HYBVideoModel *)videoModel {
  if (videoModel.status != kHYBVideoStatusCompleted) {
    [videoModel.operation resume];
  }
}

這裡都是通過模型來取到operation,然後調用對應的操作API來實現的!對於下載管理類,是不是也變得很簡化了呢?

最後, 我們要處理一下代理:

// 下載完成時,會回調
#pragma mark - NSURLSessionDownloadDelegate
- (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location {
  //本地的文件路徑,使用fileURLWithPath:來創建
  if (downloadTask.hyb_videoModel.localPath) {
    NSURL *toURL = [NSURL fileURLWithPath:downloadTask.hyb_videoModel.localPath];
    NSFileManager *manager = [NSFileManager defaultManager];
    [manager moveItemAtURL:location toURL:toURL error:nil];
  }
  [downloadTask.hyb_videoModel.operation downloadFinished];
  NSLog(@"path = %@", downloadTask.hyb_videoModel.localPath);
}
// 下載失敗或者成功時,會回調。其中失敗有可能是暫停下載導致,所以需要做一些判斷
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
  dispatch_async(dispatch_get_main_queue(), ^{
    if (error == nil) {
      task.hyb_videoModel.status = kHYBVideoStatusCompleted;
      [task.hyb_videoModel.operation downloadFinished];
    } else if (task.hyb_videoModel.status == kHYBVideoStatusSuspended) {
      task.hyb_videoModel.status = kHYBVideoStatusSuspended;
    } else if ([error code] < 0) {
      // 網絡異常
      task.hyb_videoModel.status = kHYBVideoStatusFailed;
    }
  });
}
// 這個是處理進度的
- (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
  double byts =  totalBytesWritten * 1.0 / 1024 / 1024;
  double total = totalBytesExpectedToWrite * 1.0 / 1024 / 1024;
  NSString *text = [NSString stringWithFormat:@"%.1lfMB/%.1fMB",byts,total];
  CGFloat progress = totalBytesWritten / (CGFloat)totalBytesExpectedToWrite;
  dispatch_async(dispatch_get_main_queue(), ^{
    downloadTask.hyb_videoModel.progressText = text;
    downloadTask.hyb_videoModel.progress = progress;
  });
}
// 當通過resume恢復下載時,會回調一次這裡,更新進度
- (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
 didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes {
  double byts =  fileOffset * 1.0 / 1024 / 1024;
  double total = expectedTotalBytes * 1.0 / 1024 / 1024;
  NSString *text = [NSString stringWithFormat:@"%.1lfMB/%.1fMB",byts,total];
  CGFloat progress = fileOffset / (CGFloat)expectedTotalBytes;
  dispatch_async(dispatch_get_main_queue(), ^{
    downloadTask.hyb_videoModel.progressText = text;
    downloadTask.hyb_videoModel.progress = progress;
  });
}

大家發現沒有,給task擴展了屬性之後,到這裡可以非常簡單就能直接取到model,而給model賦值進度、狀態,都會自動觸發更新UI。是不是變得很方便了呢?內部管理代碼也比較簡單,讀起來也挺容易懂的吧!

第七節:小結

本篇文章教大家的同時,也希望大家多提出意見,尤其是設計過類似功能的開發人員,請多多指教。這篇文章中的代碼設計都是最簡單版的了,沒有考慮過多的擴展性用耦合度問題,不過文章中設計理念提出了的,請大家在項目中開發時,最好采用協議方式來設計,以支持自由擴展!

看完本篇文章,是否有收獲?是否與您之前所想有沖擊?是否想過如何設計?請大家在評論區留下保貴的意見和建議!

第八節:下載Demo

本篇文章是有demo的,但是demo中筆者將下載資源去掉了。如果大家想要測試效果,只能自尋找下載資源鏈接!

DEMO下載:DownloadManager

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved