即将要做一个有点技术含量的项目,其中一个小技术点就是视频上传、下载,在项目开始前,就需要做一下下技术调研,并写出相应的demo。本篇文章是针对所设计的demo而写的,只有下载的功能。当然,这个demo只是最简单版的,不考虑耦合性,只考虑是否可实现的问题。
高手也可以看看,最好在阅读之后可以将自己的想法在评论中写出来,交流交流各自封装的思想。如果您不会写,也可以参考参考,相信也会有所收获!没有效果图就没有阅读完本篇文章的勇气,给大家打打气,继续阅读吧!
第一节:功能说明
首先,本篇文章教大家写一个最简单的下载管理器,不包含上传管理器。不过,上传管理器与下载管理器是一样的,后面会抛砖引玉,大家可以各自去尝试!
本篇文章所讲解的下载管理器具备以下功能:
开始下载某个视频
挂起某个视频下载(暂停下载)
恢复某个视频下载(继续下载)
可设置下载最大并发量
添加到下载队列
以下便是最基本的功能了,那么我们就根据这几个基本功能来实现。至于要做到后台自动下载及退出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变化上!
子类化Operation
关于子类化NSOperation需要做哪些事件,不过下面我也会列出一些要点:
重写start方法时,要做好isCannelled的判断
重写isExecuting、isFinished、isConcurrent
重写cancel,并处理好isCancelled KVO处理
我们设计Operation时,采用NSURLSession实现下载,通过控制NSURLSessionDownloadTask,可实现下载、暂停下载和断点下载功能。
我们整个头文件的设计为:
@classHYBVideoModel; @interfaceNSURLSessionTask(VideoModel) //为了更方便去获取,而不需要遍历,采用扩展的方式,可直接提取,提高效率 @property(nonatomic,weak)HYBVideoModel*hyb_videoModel; @end @interfaceHYBVideoOperation:NSOperation -(instancetype)initWithModel:(HYBVideoModel*)modelsession:(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才能退队!
[selfwillChangeValueForKey:@"isFinished"]; [selfwillChangeValueForKey:@"isExecuting"]; _executing=NO; _finished=YES; [selfdidChangeValueForKey:@"isExecuting"]; [selfdidChangeValueForKey:@"isFinished"];
因为任务完成还可以重新下载,通常情况下不会自动退队。
更新进度和状态
我们通过模型来反馈到UI上,在进度和状态变化时,可以回调来更新UI。
首先,下载过程有很多种状态,我们定义成枚举:
typedefNS_ENUM(NSInteger,HYBVideoStatus){ kHYBVideoStatusNone=0,//初始状态 kHYBVideoStatusRunning=1,//下载中 kHYBVideoStatusSuspended=2,//下载暂停 kHYBVideoStatusCompleted=3,//下载完成 kHYBVideoStatusFailed=4,//下载失败 kHYBVideoStatusWaiting=5//等待下载 };
设计属性:
typedefvoid(^HYBVideoStatusChanged)(HYBVideoModel*model); typedefvoid(^HYBVideoProgressChanged)(HYBVideoModel*model); @interfaceHYBVideoModel: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)CGFloatprogress; //状态变化会自动回调onStatusChanged @property(nonatomic,assign)HYBVideoStatusstatus; //这里为什么要引用operation且是强引用?因为管理器直接管理的是model, //而真正做下载任务的是operation。 //为什么没有将这两个分别作为属性呢?为了整体更简单! @property(nonatomic,strong)HYBVideoOperation*operation; @property(nonatomic,copy)HYBVideoStatusChangedonStatusChanged; @property(nonatomic,copy)HYBVideoProgressChangedonProgressChanged; @property(nonatomic,readonly,copy)NSString*statusText; @end
当然,不同的人来设计,可能会有不同的方式。我分析过好几种设计方式,但是列出来的好处,不如这一种。
当进度或者状态变化时,自动地回调:
-(void)setProgress:(CGFloat)progress{ if(_progress!=progress){ _progress=progress; if(self.onProgressChanged){ self.onProgressChanged(self); }else{ NSLog(@"progresschangedblockisempty"); } } } -(void)setStatus:(HYBVideoStatus)status{ if(_status!=status){ _status=status; if(self.onStatusChanged){ self.onStatusChanged(self); } } }
这样回调与下载管理类及下载类都没有直接的关系了,而model的回调直接反馈到UI层了!
在配置cell时,如下即可实时展示进度及状态提示:
-(UITableViewCell*)tableView:(UITableView*)tableViewcellForRowAtIndexPath:(NSIndexPath*)indexPath{ HYBVideoCell*cell=[tableViewdequeueReusableCellWithIdentifier:kCellIdentifier forIndexPath:indexPath]; HYBVideoModel*model=[HYBVideoManagershared].videoModels[indexPath.row]; cell.model=model; model.onStatusChanged=^(HYBVideoModel*changedModel){ cell.model=changedModel; }; model.onProgressChanged=^(HYBVideoModel*changedModel){ cell.model=changedModel; }; returncell; }
当我们点击某一个cell进入下载或者暂停之类的操作时,如下:
-(void)tableView:(UITableView*)tableViewdidSelectRowAtIndexPath:(NSIndexPath*)indexPath{ HYBVideoModel*model=[HYBVideoManagershared].videoModels[indexPath.row]; switch(model.status){ casekHYBVideoStatusNone:{ [[HYBVideoManagershared]startWithVideoModel:model]; break; } casekHYBVideoStatusRunning:{ [[HYBVideoManagershared]suspendWithVideoModel:model]; break; } casekHYBVideoStatusSuspended:{ [[HYBVideoManagershared]resumeWithVideoModel:model]; break; } casekHYBVideoStatusCompleted:{ NSLog(@"已下载完成,可以播放了,播放路径:%@",model.localPath); break; } casekHYBVideoStatusFailed:{ [[HYBVideoManagershared]resumeWithVideoModel:model]; break; } casekHYBVideoStatusWaiting:{ [[HYBVideoManagershared]startWithVideoModel:model]; break; } } }
在UI层是否是使用简单呢?从整体来看,使用者可非常简单地调用实现功能。
下载管理类
我们所设计的管理下载类采用的是单例设计模式,而所有操作都直接与model关联,对于外部都没有具体地与operation关联。当然,在项目中,最好不要直接使用这样的模型。笔者在前面的设计理念中讲到,我们可以采用协议的方式来实现,然后让model遵守协议,这样就能做到支持任意类型的model。
@classHYBVideoModel; @interfaceHYBVideoManager: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
我们在初始化时,创建队列及session:
self.queue=[[NSOperationQueuealloc]init]; self.queue.maxConcurrentOperationCount=4; NSURLSessionConfiguration*config=[NSURLSessionConfigurationdefaultSessionConfiguration]; //不能传self.queue self.session=[NSURLSessionsessionWithConfiguration: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=[[HYBVideoOperationalloc]initWithModel:videoModel session:self.session]; [self.queueaddOperation:videoModel.operation]; [videoModel.operationstart]; }else{ [videoModel.operationresume]; } } } -(void)suspendWithVideoModel:(HYBVideoModel*)videoModel{ if(videoModel.status!=kHYBVideoStatusCompleted){ [videoModel.operationsuspend]; } } -(void)resumeWithVideoModel:(HYBVideoModel*)videoModel{ if(videoModel.status!=kHYBVideoStatusCompleted){ [videoModel.operationresume]; } }
这里都是通过模型来取到operation,然后调用对应的操作API来实现的!对于下载管理类,是不是也变得很简化了呢?
最后, 我们要处理一下代理:
//下载完成时,会回调 #pragmamark-NSURLSessionDownloadDelegate -(void)URLSession:(NSURLSession*)session downloadTask:(NSURLSessionDownloadTask*)downloadTask didFinishDownloadingToURL:(NSURL*)location{ //本地的文件路径,使用fileURLWithPath:来创建 if(downloadTask.hyb_videoModel.localPath){ NSURL*toURL=[NSURLfileURLWithPath:downloadTask.hyb_videoModel.localPath]; NSFileManager*manager=[NSFileManagerdefaultManager]; [managermoveItemAtURL:locationtoURL:toURLerror:nil]; } [downloadTask.hyb_videoModel.operationdownloadFinished]; NSLog(@"path=%@",downloadTask.hyb_videoModel.localPath); } //下载失败或者成功时,会回调。其中失败有可能是暂停下载导致,所以需要做一些判断 -(void)URLSession:(NSURLSession*)sessiontask:(NSURLSessionTask*)taskdidCompleteWithError:(NSError*)error{ dispatch_async(dispatch_get_main_queue(),^{ if(error==nil){ task.hyb_videoModel.status=kHYBVideoStatusCompleted; [task.hyb_videoModel.operationdownloadFinished]; }elseif(task.hyb_videoModel.status==kHYBVideoStatusSuspended){ task.hyb_videoModel.status=kHYBVideoStatusSuspended; }elseif([errorcode]<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{ doublebyts=totalBytesWritten*1.0/1024/1024; doubletotal=totalBytesExpectedToWrite*1.0/1024/1024; NSString*text=[NSStringstringWithFormat:@"%.1lfMB/%.1fMB",byts,total]; CGFloatprogress=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{ doublebyts=fileOffset*1.0/1024/1024; doubletotal=expectedTotalBytes*1.0/1024/1024; NSString*text=[NSStringstringWithFormat:@"%.1lfMB/%.1fMB",byts,total]; CGFloatprogress=fileOffset/(CGFloat)expectedTotalBytes; dispatch_async(dispatch_get_main_queue(),^{ downloadTask.hyb_videoModel.progressText=text; downloadTask.hyb_videoModel.progress=progress; }); }
大家发现没有,给task扩展了属性之后,到这里可以非常简单就能直接取到model,而给model赋值进度、状态,都会自动触发更新UI。是不是变得很方便了呢?内部管理代码也比较简单,读起来也挺容易懂的吧!
小结
本篇文章教大家的同时,也希望大家多提出意见,尤其是设计过类似功能的开发人员,请多多指教。这篇文章中的代码设计都是最简单版的了,没有考虑过多的扩展性用耦合度问题,不过文章中设计理念提出了的,请大家在项目中开发时,最好采用协议方式来设计,以支持自由扩展!
看完本篇文章,是否有收获?是否与您之前所想有冲击?是否想过如何设计?请大家在评论区留下保贵的意见和建议!
提示:本demo只是一个小例子,实际笔者在项目中设计的时候,并非完全如此设计,而是采用了单例类统一接收下载类的代理回调,然后统一处理分发状态回调更新UI。祝大家好运,能从本例子中找到灵感!
下载Demo
本篇文章是有demo的,但是demo中笔者将下载资源去掉了。如果大家想要测试效果,只能自寻找下载资源链接!DownloadManager