关闭

手把手教你封装下载管理器

484人阅读 评论(0) 收藏 举报
分类:

2016-05-30 08:31 编辑: cocopeng 分类: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,可实现下载、暂停下载和断点下载功能。

我们整个头文件的设计为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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才能退队!

1
2
3
4
5
6
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_executing = NO;
_finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];

因为任务完成还可以重新下载,通常情况下不会自动退队。

第五节:反馈到UI展示进度及状态提示

我们通过模型来反馈到UI上,在进度和状态变化时,可以回调来更新UI。

首先,下载过程有很多种状态,我们定义成枚举:

1
2
3
4
5
6
7
8
typedef NS_ENUM(NSInteger, HYBVideoStatus) {
  kHYBVideoStatusNone = 0,       // 初始状态
  kHYBVideoStatusRunning = 1,    // 下载中
  kHYBVideoStatusSuspended = 2,  // 下载暂停
  kHYBVideoStatusCompleted = 3,  // 下载完成
  kHYBVideoStatusFailed  = 4,    // 下载失败
  kHYBVideoStatusWaiting = 5    // 等待下载
 };

设计属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

当然,不同的人来设计,可能会有不同的方式。我分析过好几种设计方式,但是列出来的好处,不如这一种。

当进度或者状态变化时,自动地回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (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时,如下即可实时展示进度及状态提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (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进入下载或者暂停之类的操作时,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@class HYBVideoModel;
@interface HYBVideoManager : NSObject
@property (nonatomic, readonly, strong) NSArray *videoModels;
+ (instancetype)shared;
// 添加视频模型,只是添加并不会下载
- (void)addVideoModels:(NSArray<hybvideomodel *> *)videoModels;
// 开始下载某个视频
- (void)startWithVideoModel:(HYBVideoModel *)videoModel;
// 挂起
- (void)suspendWithVideoModel:(HYBVideoModel *)videoModel;
// 恢复下载
- (void)resumeWithVideoModel:(HYBVideoModel *)videoModel;
// 忽略这个,暂时没有使用到
- (void)stopWiethVideoModel:(HYBVideoModel *)videoModel;
@end</hybvideomodel *>

我们在初始化时,创建队列及session:

1
2
3
4
5
6
7
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (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来实现的!对于下载管理类,是不是也变得很简化了呢?

最后, 我们要处理一下代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 下载完成时,会回调
#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


0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:50948次
    • 积分:1071
    • 等级:
    • 排名:千里之外
    • 原创:40篇
    • 转载:103篇
    • 译文:1篇
    • 评论:4条
    最新评论