iOS下载模块的实现

这是我在实习的时候对于一个下载模块的实现,在这里记录一下心得体会。这个模块完全独立实现,且不依赖于公司任何的代码,不会涉及任何隐私。

下载模块的需求

在实现一个新模块之前,我们需要理清楚这个模块需要支持最小的功能集,并逐步扩展。并且由于下载模块是存在潜在的可能性被多个模块调用,因此,我们特别要注意模块的封装。

功能需求

支持下载

下载模块最基本的自然就是可以正确的将需要下载的东西下载到指定路径,这里下载的东西包括但是不局限于图片、文本文档、压缩文件等等。因此,这些下载东西的大小是不等的,在设计功能的时候,需要讲这点考虑进去。

支持取消下载

处于控制角度的考虑,甚至是出于当流量和资源节省的角度考虑,我们都已经对下载任务有控制权。可以随时根据我们业务处于的状态,对下载任务进行暂停或者完整的停止取消。

支持并发下载

下载任务的个数肯定不会只有一个,而是可能同时存在多个。因此,我们就需要考虑如何去支持多个下载任务同时并发的下载。其外,我们还要考虑到潜在的任务之间的关联性和依赖性(比如B下载任务需要依赖于A下载任务的完成)

初步的任务我们可以认为就是如上三个就可以(初期设计模块真的不要考虑过多功能,先开始做起来,一点点看着自己造的轮子可以运转起来,成就感会促使你会不停的去完善。当然,你的代码一定要写的整洁易读!

实现下载

谈到实现下载,很多人会联想到向服务器请求数据,比如我们会使用AFNetworkong或者ASIHttpRequest这两个大名鼎鼎的开源库。两者对HTTP应用层协议进行了良好的封装,支持多种HTTP操作。我们平时也经常使用这两个库经常服务器数据请求和传输,看起来好像我们只要用这两个库,这个任务就自然而然的解决了。但是我们要考虑到,我们的下载模块是一个可能经常被别的模块甚至是应用拿来应用的功能模块,我们如果还耦合于第三库,无疑降低了这个模块的使用价值。因此,我们采用苹果原生的函数来实现,并且考虑到兼容性,我们还是采用了NSURLConnection而不是NSURLSession。

下载任务建模

首先我们需要思考,将下载任务对应成一个实际存在的类,这些类应该包括下载文件所在URL,下载任务目前的状态,比如未开始,进行中还是已结束。当然,我们也可以包含一个自定义参数等等进行下载任务的配置,当然,这是可选的。那是不是我们直接建立一个基于NSObject的子类就是最理想的建模呢?构建一个基于NSOperation的子类化operation更适合我们的场景(具体下文会说)。因此,初步的XXXDownloadOperation结果如下:

@interface XXXDownloadOperation : NSOperation

@property (nonatomic, assign, readonly) XXXDownloadStatus status;
@property (nonatomic, strong, readonly) NSURL *fileURL;
@property (nonatomic, strong)           id userInfo;

- (instancetype)initWithURL:(NSURL *)fileURL
- (void)cancelDownload;
@end
下载任务的状态

在XXXDownloadOperation中,我们可以看到包含了一个只读字段的XXXDownloadStatus的字段,这个字段就是为了反应当前下载任务的状态,它包含如下几种:

typedef NS_ENUM(NSUInteger, XXXDownloadStatus) {
    XXXDownloadStatusReady = 0,
    XXXDownloadStatusFailed = 1,
    XXXDownloadStatusDownloading = 2,
    XXXDownloadStatusCancelled = 3,
    XXXDownloadStatusDone = 4,
    XXXDownloadStatusPause = 5
};
  • XXXDownloadStatusReady 代表了当前任务可以开始进行下载
  • XXXDownloadStatusFailed 代表当前任务下载失败,比如网络断了
  • XXXDownloadStatusDownloading 代表当前下载任务正在进行中
  • XXXDownloadStatusCancelled 代表当前任务已经被取消了
  • XXXDonwloadStatusDone 代表当前任务下载完毕
  • XXXDownloadStatusPause 代表当前任务暂停

设置为只读就是为了避免人为的修改状态

实现NSURLConnection

实现NSURLConnection非常简单,它只有几个简单的Delegate需要我们去实现。

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData *)data;
- (void)connectionDidFinishLoading:(NSURLConnection*)connection;

但是由于我们是基于NSOperation,在非主线程进行NSURLConnection进行操作,所以,我们要特别注意一点,就是非主线程是没有RunLoop的,而NSURLConnection的回调都是基于NSRunLoop,因此我们需要创建一个RunLoop, 如下所示:

NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[self.connection scheduleInRunLoop:runLoop
                           forMode:NSDefaultRunLoopMode];   
[self.connection start];
[runLoop run];

上述这段代码非常容易理解,我们创建了一个NSRunLoop,并且运行它,这样我们的NSURLConnection就可以基于它进行delegate回调了。

且慢,

[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

这段话是什么意思?我们为什么要让RunLoop监听一个port。
原因就在于默认情况下,当RunLoop无所事事的时候,它就自动退出了,有可能导致NSURLConnection的事件还没回调的时候NSRunLoop已经不存在了。因此我们要让它监听一个没啥用的端口,嘿嘿,大名鼎鼎的AFNetworking也是这么做的

实现并发下载

嘿嘿,知道我们为什么采用子类化NSOperation了吧,我们就是为了利用NSOperationQueue的并发特性。

self.taskQueue = [[NSOperationQueue alloc] init];
self.taskQueue.maxConcurrentOperationCount = capacity;
self.taskQueue.name = @"com.satanwoo.taskQueue";

我们可以随意设置的NSOperationQueue的最大并发数来构建并发下载。当然这里要提醒一句,多线程环境中,并不是线程数越多并发性越好,因为多线程最大并发数其实和系统硬件资源相关联。而过多的线程数会导致过多的线程上下文切换。

NSOperation与KVO

首先建议大家读一下这篇文章 Concurrent Operations Demystified

- (BOOL)isExecuting
{
    return self.status == XXXDownloadStatusDownloading;
}

- (BOOL)isCancelled
{
    return self.status == XXXDownloadStatusCancelled;
}

- (BOOL)isFinished
{
    return self.status == XXXDownloadStatusCancelled ||
           self.status == XXXDownloadStatusDone ||
           self.status == XXXDownloadStatusFailed;
}

从上面链接文章我们可以得知,NSOperation的执行以来于这些状态。因此,我们根据自己下载任务的状态override了这些变量状态。
然后,我们只要利用KVO,去更改如上这些override的状态就好。

- (void)updateState:(XXXDownloadStatus)status
{
      // 防止我们直接调用了canel
    [self.connection cancel];

    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];
    self.status = status;
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

- (void)cancel
{
    [self willChangeValueForKey:@"isCancelled"];
    [self updateState];
    [self didChangeValueForKey:@"isCancelled"];
}

至此,一个具备并发、取消的下载功能就完成了。我们可以根据业务需要,上层封装一系列简单的函数,进行便捷的管理。

接下来我们要实现的,就是基于HTTP Header的range字段来支持断点续传功能了。