前言
SGDownload 是一个文件下载器,非常适合用于视频下载,支持后台,锁屏下载。同时支持 iOS、macOS、tvOS 三个平台 --- 源码地址
使用如下所示
// 在 AppDelegate 中添加如下代码。
- (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)())completionHandler
{
[SGDownload handleEventsForBackgroundURLSession:identifier
completionHandler:completionHandler];
}
// 启动下载
self.download = [SGDownload download];
[self.download run];
SGDownloadTask * task = [self.download taskWithContentURL:contentURL];
if (!task)
{
task = [SGDownloadTask taskWithTitle:@“title”
contentURL:contentURL
fileURL:fileURL];
}
[self.download addDownloadTask:task]
可能看了上面的代码,可能会有所疑惑,怎么上就 run 起来了?另外,怎么下载任务直接 addDownloadTask 去就行了,不主动调用下载么?
实际作者使用了 NSCondition, 以生产者-消费者模式,实现的这个功能
可见作者对锁的理解非常深入,这是值得我们学习的地方
下面就带着问题,探索该框架吧
简介
SGDownLoad主要有 7 个功能类,分别是 SGDownloadImp、SGDownloadTask、SGDownloadTaskQueue、SGDownloadTuple、SGDownloadTupleQueue、SGDownloadTaskCorrect、SGDownloadTools
SGDownloadImp: 里面主要包含了 SGDownload 类,其主要协调其他几个类,并开启一个常驻线程(空闲时处于阻塞状态),来协调任务的执行、取消等
SGDownloadTask、SGDownloadTaskQueue:其中 SGDownloadTask 是 请求的描述文件类,包含了请求的url、标题、文件地址等信息,支持归档、解归档,用于断点续传功能,而 SGDownloadTaskQueue 则是控制 SGDownloadTask 的管理类,里面通过 NSCondition控制任务执行的时机(生产者-消费者模式),除了控制 SGDownloadTask 的增删,还支持 通过 SGDownloadTask 来执行读取、删除 SGDownloadTask 缓存的文件功能
SGDownloadTuple、SGDownloadTupleQueue: SGDownloadTuple 是 SGDownloadTask 与 NSURLSessionDownloadTask 的结合体,主要用来协调 文件描述信息 和 请求task信息, 保证描述文件能直接与网络请求挂钩, SGDownloadTupleQueue 为管理 SGDownloadTuple 的管理类,支持添加、删除、取消任等
SGDownloadTaskCorrect: 主要用于矫正部分版本生成 task 的问题
SGDownloadTools: 文件操作类
大体结构如下所示
源码探索
SGDownloadTask
里面包含了下载的必要信息等,同时也包含了下载失败时的 resumeInfoData 信息,以便与恢复下载
该类 实现了 NSObject 的归档接归档协议,主要用于任务的回复,已经失败文件的
可以通过这里的信息,对下载的任务进行取消,以及删除下载的文件等(包括下载完成的和未完成的)
@property (nonatomic, assign, readonly) SGDownloadTaskState state;
@property (nonatomic, copy, readonly) NSURL * contentURL;
@property (nonatomic, copy, readonly) NSString * title;
@property (nonatomic, copy, readonly) NSURL * fileURL;
@property (nonatomic, assign, readonly) BOOL fileDidRemoved;
@property (nonatomic, assign, readonly) BOOL fileIsValid;
@property (nonatomic, assign) BOOL replaceHomeDirectoryIfNeed; // default is YES;
@property (nonatomic, assign, readonly) float progress;
@property (nonatomic, assign, readonly) int64_t bytesWritten;
@property (nonatomic, assign, readonly) int64_t totalBytesWritten;
@property (nonatomic, assign, readonly) int64_t totalBytesExpectedToWrite;
// about resume
@property (nonatomic, strong, readonly) NSData * resumeInfoData;
@property (nonatomic, assign, readonly) int64_t resumeFileOffset;
@property (nonatomic, assign, readonly) int64_t resumeExpectedTotalBytes;
同时注意 此类的 state 参数,其表示下载的状态,可以根据其状态来进行相关操作
typedef NS_ENUM(NSUInteger, SGDownloadTaskState)
{
SGDownloadTaskStateNone, //默认状态,无操作
SGDownloadTaskStateWaiting, //任务排在队列等待执行
SGDownloadTaskStateRunning, //正在瞎子啊
SGDownloadTaskStateSuspend, //任务已经开始,但是后来挂起了
SGDownloadTaskStateFinished, //任务已经完成
SGDownloadTaskStateCanceled, //任务被取消
SGDownloadTaskStateFailured, //任务失败
};
SGDownloadTaskQueue
该类主要来管理 SGDownloadTask 对象,包括增删、归档解归档等功能
初始化的时候,会进行解归档操作,用于恢复未完成的任务,如果不想也可以主动删除
+ (instancetype)queueWithDownload:(SGDownload *)download
{
return [[self alloc] initWithDownload:download];
}
- (instancetype)initWithDownload:(SGDownload *)download
{
if (self = [super init]) {
self->_download = download;
self->_archiverPath = [SGDownloadTools
archiverFilePathWithIdentifier:download.identifier];
//接归档,获取task任务集合
self->_tasks = [NSKeyedUnarchiver unarchiveObjectWithFile:self.archiverPath];
if (!self->_tasks) {
self->_tasks = [NSMutableArray array];
}
self.condition = [[NSCondition alloc] init];
[self resetQueue];
}
return self;
}
下面介绍 taskQueue 中最核心的功能,添加和删除功能, 即增加任务和执行任务
增加任何 和 执行 任务设置成了 生产者--消费者模式
执行任务时:循环遍历 task集合,找默认状态或者等待中的任务,则继续执行,否则,通过 NSCondition 的 wait 功能,阻塞当前线程
添加任务时:将任务添加到,task集合中,如果任务处于挂起、取消、默认、失败等状态,则更改为等待执行状态,然后 使用 NSCondition 的 wait 功能,唤醒当前线程(添加消费不在一个线程中,一个在用户添加下载任务的线程,一个SGDownload的自定义线程中)
生产者 -- 消费者模式 ,可以参考 ios常用的几种锁,里面有介绍
简而言之,消费者通过一个循环,获取任务执行,当没有任务时阻塞,而生产者添加任务后,唤醒消费者所在线程,让其继续之前的循环,继续执行任务
代码如下所示:
//消费者
- (SGDownloadTask *)downloadTaskSync
{
if (self.closed) return nil;
使用互斥锁的功能,保证不被多个线程同时访问
[self.condition lock];
SGDownloadTask * task;
//开启消费者模式,一个运行循环,获取不到task就阻塞队列,等待生产者添加新的任务后,唤醒当前线程
//若获取到task就结束运行
do {
for (SGDownloadTask * obj in self.tasks) {
if (self.closed) {
[self.condition unlock];
return nil;
}
switch (obj.state) {
case SGDownloadTaskStateNone:
case SGDownloadTaskStateWaiting:
task = obj;
break;
default:
break;
}
if (task) break;
}
//为空,说明队列没有任务,阻塞当前线程,等待有任务后唤醒,然后继续获取
if (!task) {
//阻塞掉了
[self.condition wait];
}
} while (!task);
[self.condition unlock];
return task;
}
//生产者
- (void)addDownloadTasks:(NSArray <SGDownloadTask *> *)tasks
{
if (self.closed) return;
if (tasks.count <= 0) return;
[self.condition lock];
BOOL needSignal = NO;
for (SGDownloadTask * obj in tasks) {
if (![self.tasks containsObject:obj]) {
obj.download = self.download;
[self.tasks addObject:obj];
}
switch (obj.state) {
case SGDownloadTaskStateNone:
case SGDownloadTaskStateSuspend:
case SGDownloadTaskStateCanceled:
case SGDownloadTaskStateFailured:
obj.state = SGDownloadTaskStateWaiting;
needSignal = YES;
break;
default:
break;
}
}
//添加任务后,task队列已经有任务,则使用signal唤醒消费者所在的线程,继续执行
if (needSignal) {
//发送了信号-- 唤醒当前
[self.condition signal];
}
[self.condition unlock];
[self tryArchive];
}
上面两个步骤,在后续的SGDownload会更加了解其交货过程
下面的setTaskState,是当task状态改变的时候调用的方法, 该方法处理状态,还会对task重新进行一次归档,以保证下载进度能被正常保存,避免丢失
- (void)setTaskState:(SGDownloadTask *)task state:(SGDownloadTaskState)state
{
if (!task) return;
if (task.state == state) return;
//条件锁
[self.condition lock];
task.state = state;
[self.condition unlock];
//归档一次
[self tryArchive];
}
SGDownloadTuple
将 SGDownloadTask 与 NSURLSessionDownloadTask 关联起来的类,该类同时拥有了 任务描述信息 和 下载task信息,减少了 SGDownload类中的代码耦合,通过该类能同时操作两个相关的功能
@property (nonatomic, strong) SGDownloadTask * downloadTask;
@property (nonatomic, strong) NSURLSessionDownloadTask * sessionTask;
SGDownloadTupleQueue
主要用来添加 SGDownloadTuple,以及 删除等,其中包括了任务的取消等,由于比较简单,就不多介绍了
- (void)addTuple:(SGDownloadTuple *)tuple;
- (void)removeTupleWithSesstionTask:(NSURLSessionTask *)sessionTask;
- (void)removeTuple:(SGDownloadTuple *)tuple;
- (void)removeTuples:(NSArray <SGDownloadTuple *> *)tuples;
- (void)cancelDownloadTask:(SGDownloadTask *)downloadTask resume:(BOOL)resume completionHandler:(void(^)(SGDownloadTuple * tuple))completionHandler;
- (void)cancelDownloadTasks:(NSArray <SGDownloadTask *> *)downloadTasks resume:(BOOL)resume completionHandler:(void(^)(NSArray <SGDownloadTuple *> * tuples))completionHandler;
- (void)cancelAllTupleResume:(BOOL)resume completionHandler:(void(^)(NSArray <SGDownloadTuple *> * tuples))completionHandler;
- (void)cancelTuple:(SGDownloadTuple *)tuple resume:(BOOL)resume completionHandler:(void(^)(SGDownloadTuple * tuple))completionHandler;
- (void)cancelTuples:(NSArray <SGDownloadTuple *> *)tuples resume:(BOOL)resume completionHandler:(void(^)(NSArray <SGDownloadTuple *> * tuples))completionHandler;
SGDownload
该功能的核心类,协调了 SGDownloadTupleQueue、 SGDownloadTaskQueue 以及下载回调结果的处理
注意,该类需要的功能都有,协调的类中有的功能,他基本都有,尽量避免调用过程中的繁琐,如下所示
- (nullable SGDownloadTask *)taskForContentURL:(NSURL *)contentURL;
- (nullable NSArray <SGDownloadTask *> *)tasksForAll;
- (nullable NSArray <SGDownloadTask *> *)tasksForRunningOrWatting;
- (nullable NSArray <SGDownloadTask *> *)tasksForState:(SGDownloadTaskState)state;
- (void)addDownloadTask:(SGDownloadTask *)task;
- (void)addDownloadTasks:(NSArray <SGDownloadTask *> *)tasks;
- (void)addSuppendTask:(SGDownloadTask *)task;
- (void)addSuppendTasks:(NSArray <SGDownloadTask *> *)tasks;
- (void)resumeAllTasks;
- (void)resumeTask:(SGDownloadTask *)task;
- (void)resumeTasks:(NSArray <SGDownloadTask *> *)tasks;
- (void)suspendAllTasks;
- (void)suspendTask:(SGDownloadTask *)task;
- (void)suspendTasks:(NSArray <SGDownloadTask *> *)tasks;
- (void)cancelAllTasks;
- (void)cancelTask:(SGDownloadTask *)task;
- (void)cancelTasks:(NSArray <SGDownloadTask *> *)tasks;
- (void)cancelAllTasksAndDeleteFiles;
- (void)cancelTaskAndDeleteFile:(SGDownloadTask *)task;
- (void)cancelTasksAndDeleteFiles:(NSArray <SGDownloadTask *> *)tasks;
SGDownload初始化的时候,在后台线程,可以保证用户能够在锁屏和后台时继续下载
- (instancetype)initWithIdentifier:(NSString *)identifier
{
if (self = [super init]) {
//标识
self->_identifier = identifier;
//开启后台线程
self->_sessionConfiguration = [NSURLSessionConfiguration
backgroundSessionConfigurationWithIdentifier:identifier];
self.maxConcurrentOperationCount = 1;
//任务队列
self.taskQueue = [SGDownloadTaskQueue queueWithDownload:self];
//元组队列
self.taskTupleQueue = [[SGDownloadTupleQueue alloc] init];
}
return self;
}
在代码调用的时候,发现是先调用的 run功能,然后才添加自动执行的任务,下面带着疑问,来参考看下面的代码吧
- (void)run
{
if (!self.running) {
self.running = YES;
[self setupOperation];
}
}
- (void)setupOperation
{
if (self.maxConcurrentOperationCount <= 0) {
self.maxConcurrentOperationCount = 1;
}
//初始化并发条件锁
self.concurrentCondition = [[NSCondition alloc] init];
self.lastResumeLock = [[NSLock alloc] init];
//回调队列,异步串行队列,代理回调
self.sessionDelegateQueue = [[NSOperationQueue alloc] init];
self.sessionDelegateQueue.maxConcurrentOperationCount = 1;
//最高优先级,主要用于提供交互UI的操作,比如处理点击事件,绘制图像到屏幕上
self.sessionDelegateQueue.qualityOfService = NSQualityOfServiceUserInteractive;
self.sessionDelegateQueue.suspended = YES;
[self.lastResumeLock lock];
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration
delegate:self
delegateQueue:self.sessionDelegateQueue];
Ivar ivar = class_getInstanceVariable(NSClassFromString(@"__NSURLBackgroundSession"), "_tasks");
if (ivar) {
NSDictionary <NSNumber *, NSURLSessionDownloadTask *> * lastTasks =
object_getIvar(self.session, ivar);
if (lastTasks && lastTasks.count > 0) {
for (NSNumber * key in lastTasks) {
//NSURLSession下载任务
NSURLSessionDownloadTask * obj = [lastTasks objectForKey:key];
//创建 downloadTask 做了缓存
SGDownloadTask * downloadTask = [self.taskQueue
taskForContentURL:[self getURLFromSessionTask:obj]];
if (downloadTask) {
//改变状态
[self.taskQueue setTaskState:downloadTask state:SGDownloadTaskStateRunning];
SGDownloadTuple * tuple = [SGDownloadTuple
tupleWithDownloadTask:downloadTask sessionTask:obj];
[self.taskTupleQueue addTuple:tuple];
}
}
}
}
[self.lastResumeLock unlock];
self.sessionDelegateQueue.suspended = NO;
//创建了一下下载任务 NSInvocationOperation
self.downloadOperation = [[NSInvocationOperation alloc]
initWithTarget:self selector:@selector(downloadOperationHandler) object:nil];
//下载队列
self.downloadOperationQueue = [[NSOperationQueue alloc] init];
//串行队列
self.downloadOperationQueue.maxConcurrentOperationCount = 1;
self.downloadOperationQueue.qualityOfService = NSQualityOfServiceUserInteractive;
[self.downloadOperationQueue addOperation:self.downloadOperation];
}
当运行起来任务后,会执行下面的线程,下面的仔细看,会有很多细节,首先通过循环设置常驻线程(平时大家都认为常驻线程需要通过runloop,此话是片面的,当子线程开启后,会主动执行一个任务,如果任务执行完毕,没主动开启子线程runloop时,任务执行完线程会自动销毁,开启runloop循环后,则由于该线程任务无法执行完毕,则不会被销毁,而我们执行的任务如果本身就是一个循环,那么该任务就不会结束,相当于该循环就是一个runloop)
下面就是一个自建的运行循环,通过消费者模式,来获取任务指定,如下所示
- (void)downloadOperationHandler
{
//常驻线程,阻塞、执行、等待(self.concurrentCondition,条件锁的检测)
//消费者的角色 -- downLoadTask
while (YES) {
@autoreleasepool
{
if (self.closed) {
break;
}
NSLog(@"current Thread -- %@",[NSThread currentThread]);
//条件加锁
[self.concurrentCondition lock];
//最大并发数
while (self.taskTupleQueue.tuples.count >= self.maxConcurrentOperationCount) {
NSLog(@"current wait Thread -- %@",[NSThread currentThread]);
[self.concurrentCondition wait];
}
[self.concurrentCondition unlock];
//从taskQueue中获取任务(downloadTaskSync就是taskQueue中的消费者)
//在taskQueue拿到task之前,该线程会一直阻塞到downloadTaskSync方法中
//当通过 SGDownload 简介调用 taskQueue中的生产者功能时,则解除阻塞,继续向下执行
SGDownloadTask * downloadTask = [self.taskQueue downloadTaskSync];
//避免用户强制结束该循环的处理
if (!downloadTask) {
break;
}
//改变下载状态为下载状态
[self.taskQueue setTaskState:downloadTask state:SGDownloadTaskStateRunning];
//创建 NSURLSessionDownloadTask
NSURLSessionDownloadTask * sessionTask = nil;
if (downloadTask.resumeInfoData.length > 0) {
//根据reusmeInfoData恢复下载进度(当状态改变时,task任务会被归档到磁盘中)
sessionTask = [SGDownloadTaskCorrect
downloadTaskWithSession:self.session resumeData:downloadTask.resumeInfoData];
} else {
sessionTask = [self.session downloadTaskWithURL:downloadTask.contentURL];
}
//生成新的 SGDownloadTuple 放到 tupleQueue中去
SGDownloadTuple * tuple = [SGDownloadTuple
tupleWithDownloadTask:downloadTask sessionTask:sessionTask];
[self.taskTupleQueue addTuple:tuple];
[sessionTask resume];
}
}
}
单个文件下载完成后的回调
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)sessionTask
didCompleteWithError:(NSError *)error
{
//这个锁保证几个下载模块,回调同时指定导致的问题,或者主动设置多个线程同时下载的数据安全
[self.lastResumeLock lock];
//条件锁加锁,保证获取处理taskTupleQueue等任务时出现数据安全问题
[self.concurrentCondition lock];
//获取任务,然后更新tupleQueue、taskQueue等状态
SGDownloadTask * downloadTask = [self.taskQueue taskForContentURL:
[self getURLFromSessionTask:sessionTask]];
SGDownloadTuple * tuple = [self.taskTupleQueue tupleWithDownloadTask:downloadTask
sessionTask:(NSURLSessionDownloadTask *)sessionTask];
if (!tuple) {
[self.taskTupleQueue removeTupleWithSesstionTask:sessionTask];
//signal -- 唤醒当前线程,当前正在等待的任务继续执行
[self.concurrentCondition signal];
[self.concurrentCondition unlock];
[self.lastResumeLock unlock];
return;
}
//更新 downloadTask 的状态,成功后下载任务则没有存在的价值,失败后,需要更新 resumeInfoData
//以便于恢复下载
SGDownloadTaskState state;
if (error) {
NSData * resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
if (resumeData) {
tuple.downloadTask.resumeInfoData = resumeData;
}
if (error.code == NSURLErrorCancelled) {
state = SGDownloadTaskStateSuspend;
} else {
tuple.downloadTask.error = error;
state = SGDownloadTaskStateFailured;
}
} else {
if (![[NSFileManager defaultManager] fileExistsAtPath:tuple.downloadTask.fileURL.path]) {
tuple.downloadTask.error = [NSError
errorWithDomain:@"download file is deleted" code:-1 userInfo:nil];
state = SGDownloadTaskStateFailured;
} else {
state = SGDownloadTaskStateFinished;
}
}
//更新 tuple.downloadTask 的状态, 通知归档数据
[self.taskQueue setTaskState:tuple.downloadTask state:state];
[self.taskTupleQueue removeTuple:tuple];
if ([self.taskQueue tasksForRunningOrWatting].count <= 0 &&
self.taskTupleQueue.tuples.count <= 0) {
dispatch_async(dispatch_get_main_queue(), ^{
if ([self.delegate
respondsToSelector:@selector(downloadDidCompleteAllRunningTasks:)]) {
[self.delegate downloadDidCompleteAllRunningTasks:self];
}
});
}
[self.concurrentCondition signal];
[self.concurrentCondition unlock];
[self.lastResumeLock unlock];
}
下载过程中的回调,用于及时更新下载信息
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)sessionTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
//对象锁确保downloadTask 访问安全
[self.lastResumeLock lock];
//获取下载任务,用户更新下载任务信息
SGDownloadTask * downloadTask = [self.taskQueue
taskForContentURL:[self getURLFromSessionTask:sessionTask]];
SGDownloadTuple * tuple = [self.taskTupleQueue
tupleWithDownloadTask:downloadTask sessionTask:(NSURLSessionDownloadTask *)sessionTask];
if (!tuple) {
[self.lastResumeLock unlock];
return;
}
更新下载任务信息
[tuple.downloadTask setBytesWritten:bytesWritten
totalBytesWritten:totalBytesWritten
totalBytesExpectedToWrite:totalBytesExpectedToWrite];
//此步骤更新task的状态,同时进行了一次归档,以保证数据失败也能顺利恢复数据
if (tuple.downloadTask.state != SGDownloadTaskStateSuspend) {
[self.taskQueue setTaskState:tuple.downloadTask state:SGDownloadTaskStateRunning];
}
[self.lastResumeLock unlock];
}
最后
SGDownLoad 充分利用 锁和信号量(信号量也被列入常用的锁)功能,来利用生产者--消费者模式,实现添加任务即可执行的功能,作者对于锁的理解非常值得我们学习
这一模式在数据库的的一些事务等操作那一章也会提及到,因此该模式不只是前端在使用
关于 SGDownload就介绍到这里了,想仔细研究可以看源码继续深入,地址开头已给出,欢迎探讨