SGDownload探索

1,727 阅读9分钟

前言

SGDownload 是一个文件下载器,非常适合用于视频下载,支持后台,锁屏下载。同时支持 iOSmacOStvOS 三个平台 --- 源码地址

使用如下所示

// 在 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 个功能类,分别是 SGDownloadImpSGDownloadTaskSGDownloadTaskQueueSGDownloadTupleSGDownloadTupleQueueSGDownloadTaskCorrectSGDownloadTools

SGDownloadImp: 里面主要包含了 SGDownload 类,其主要协调其他几个类,并开启一个常驻线程(空闲时处于阻塞状态),来协调任务的执行、取消等

SGDownloadTask、SGDownloadTaskQueue:其中 SGDownloadTask 是 请求的描述文件类,包含了请求的url、标题、文件地址等信息,支持归档、解归档,用于断点续传功能,而 SGDownloadTaskQueue 则是控制 SGDownloadTask 的管理类,里面通过 NSCondition控制任务执行的时机(生产者-消费者模式),除了控制 SGDownloadTask 的增删,还支持 通过 SGDownloadTask 来执行读取、删除 SGDownloadTask 缓存的文件功能

SGDownloadTuple、SGDownloadTupleQueue: SGDownloadTuple 是 SGDownloadTask 与 NSURLSessionDownloadTask 的结合体,主要用来协调 文件描述信息请求task信息, 保证描述文件能直接与网络请求挂钩, SGDownloadTupleQueue 为管理 SGDownloadTuple 的管理类,支持添加、删除、取消任等

SGDownloadTaskCorrect: 主要用于矫正部分版本生成 task 的问题

SGDownloadTools: 文件操作类

大体结构如下所示

image.png

源码探索

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

该功能的核心类,协调了 SGDownloadTupleQueueSGDownloadTaskQueue 以及下载回调结果的处理

注意,该类需要的功能都有,协调的类中有的功能,他基本都有,尽量避免调用过程中的繁琐,如下所示

- (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就介绍到这里了,想仔细研究可以看源码继续深入,地址开头已给出,欢迎探讨