preface

SGDownload is a file download, very suitable for video download, support background, lock screen download. Simultaneously supports iOS, macOS, tvOS three platforms — source address

Use as follows

// Add the following code to the AppDelegate.
- (void)application:(UIApplication *)application 
    handleEventsForBackgroundURLSession:(NSString *)identifier 
    completionHandler: (void (^)())completionHandler
{
    [SGDownload handleEventsForBackgroundURLSession:identifier 
        completionHandler:completionHandler];
}

// Start the download
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]
Copy the code

May read the above code, may be confused, how to run up? In addition, how to download the task directly addDownloadTask to go, do not actively call download?

The actual author uses NSCondition to implement this function in a producer-consumer pattern

It can be seen that the author has a very deep understanding of locks, which is worth our learning

Let’s explore this framework with questions

Introduction to the

SGDownLoad has seven main feature classes, , respectively, SGDownloadImp, SGDownloadTask, SGDownloadTaskQueue, SGDownloadTuple, SGDownloadTupleQueue, SGDownloadTaskCorrect, SGDownloadTo ols

SGDownloadImp: this mainly contains the SGDownload class, which coordinates several other classes and starts a resident thread (blocked when idle) to coordinate task execution, cancellation, etc

SGDownloadTask, SGDownloadTaskQueue: SGDownloadTask is the description of the request file class, including the request URL, title, file address and other information, support archiving, unarchiving, used for resumable breakpoint function, and SGDownloadTaskQueue is the management class control SGDownloadTask. NSCondition controls the timing of task execution (producer-consumer mode). In addition to controlling the addition and deletion of SGDownloadTask, SGDownloadTask can also be used to read and delete files cached by SGDownloadTask

SGDownloadTuple, SGDownloadTupleQueue: SGDownloadTuple is the combination of SGDownloadTask and NSURLSessionDownloadTask, mainly used to coordinate file description and request task information, to ensure that the description file can be directly linked to the network request. SGDownloadTupleQueue is a management class that manages SGDownloadTuple. It can be added, deleted, or cancelled

SGDownloadTaskCorrect: mainly used to correct the problem that some versions generate tasks

SGDownloadTools: File manipulation class

The general structure is shown below

The source code to explore

SGDownloadTask

This contains the necessary information for the download, as well as the resumeInfoData information in the event of a download failure to resume the download

This class implements NSObject archiving protocol, mainly used for task reply, has failed files

You can use this information to cancel downloaded tasks and delete downloaded files (both completed and unfinished).

@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;
Copy the code

Note also the state parameter of this class, which indicates the state of the download and can be used according to its state

typedef NS_ENUM(NSUInteger, SGDownloadTaskState)
{
    SGDownloadTaskStateNone, // No operation is performed by default
    SGDownloadTaskStateWaiting, // Tasks are queued for execution
    SGDownloadTaskStateRunning, // He is blind
    SGDownloadTaskStateSuspend, // The task was started, but then suspended
    SGDownloadTaskStateFinished, // The task is complete
    SGDownloadTaskStateCanceled, // The task was cancelled
    SGDownloadTaskStateFailured, // Task failed
};
Copy the code

SGDownloadTaskQueue

This class is used to manage SGDownloadTask objects, including adding and deleting, archiving and unarchiving

During initialization, unarchiving is performed to recover unfinished tasks. You can delete unfinished tasks if you do not want to

+ (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];
        // Get the task set
        self->_tasks = [NSKeyedUnarchiver unarchiveObjectWithFile:self.archiverPath];
        if(! self->_tasks) { self->_tasks = [NSMutableArray array]; } self.condition = [[NSCondition alloc] init]; [self resetQueue]; }return self;
}
Copy the code

Here are the core functions of taskQueue, the add and remove functions, which add and execute tasks

Add any and execute tasks set to producer-consumer mode

When executing a task: Iterate over the task set to find the default state or waiting task, and continue to execute the task. Otherwise, NSCondition’s wait function blocks the current thread

When adding a task: If the task is in the pending, cancelled, default, or failed state, the state is changed to wait for execution. Then, NSCondition’s wait function is used to wake up the current thread. In an SGDownload custom thread)

Producer-consumer mode, can refer to the ios common several locks, which are described in the inside

In short, the consumer goes through a loop, gets the execution of the task, blocks when there is no task, and when the producer adds the task, wakes up the thread of the consumer to continue the previous loop and continue the execution of the task

The code looks like this:

/ / consumer
- (SGDownloadTask *)downloadTaskSync
{
    if (self.closed) returnnil; Condition lock [self.condition lock]; SGDownloadTask * task;// Start consumer mode, a running loop that blocks the queue until the producer adds a new task and wakes up the current thread
    // Stop running if task is found
    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 it is empty, the queue has no task, and the current thread is blocked, waiting for the task to wake up, and then continue to fetch
        if(! task) {// It is blocked[self.condition wait]; }}while(! task); [self.condition unlock];return task;
}
Copy the code
/ / producer
- (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; }}// If the task queue already has a task, use signal to wake up the consumer's thread and continue executing
    if (needSignal) {
        // Send a signal to wake up the current
        [self.condition signal];
    }
    [self.condition unlock];
    [self tryArchive];
}
Copy the code

The above two steps will be followed in SGDownload to learn more about the delivery process

The following setTaskState method is called when the task state changes, handles the state, and re-archives the task to ensure that the download progress is saved properly and not lost

- (void)setTaskState:(SGDownloadTask *)task state:(SGDownloadTaskState)state
{
    if(! task)return;
    if (task.state == state) return;
    Lock / / conditions
    [self.condition lock];
    task.state = state;
    [self.condition unlock];
    // Archive once
    [self tryArchive];
}
Copy the code

SGDownloadTuple

A class that associates SGDownloadTask with NSURLSessionDownloadTask. This class has both task description information and download task information, reducing code coupling in the SGDownload class and enabling simultaneous operation of two related functions

@property (nonatomic, strong) SGDownloadTask * downloadTask;
@property (nonatomic, strong) NSURLSessionDownloadTask * sessionTask;
Copy the code

SGDownloadTupleQueue

It is mainly used to add SGDownloadTuple, delete, etc., including the cancellation of tasks, etc., because it is relatively simple, I will not introduce more

- (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;
Copy the code

SGDownload

The core class of this functionality coordinates the processing of SGDownloadTupleQueue, SGDownloadTaskQueue, and download callback results

Note that this class has all the required functions, and it basically has all the functions in the coordinated class, as far as possible to avoid the tedious call process, as shown below

- (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;
Copy the code

When SGDownload is initialized, it is threaded in the background to ensure that users can continue downloading while the screen is locked and in the background

- (instancetype)initWithIdentifier:(NSString *)identifier
{
    if (self = [super init]) {
        / / logo
        self->_identifier = identifier;
        // Start background threads
        self->_sessionConfiguration = [NSURLSessionConfiguration 
            backgroundSessionConfigurationWithIdentifier:identifier];
        self.maxConcurrentOperationCount = 1;
        // Task queue
        self.taskQueue = [SGDownloadTaskQueue queueWithDownload:self];
        // Tuple queue
        self.taskTupleQueue = [[SGDownloadTupleQueue alloc] init];
    }
    return self;
}
Copy the code

When the code is called, it is found that the run function is called first, and then the automatic execution of the task is added, with questions, to refer to the following code

- (void)run
{
    if (!self.running) {
        self.running = YES;
        [self setupOperation];
    }
}
- (void)setupOperation
{
    if (self.maxConcurrentOperationCount <= 0) {
        self.maxConcurrentOperationCount = 1;
    }
    // Initialize the concurrent conditional lock
    self.concurrentCondition = [[NSCondition alloc] init];
    self.lastResumeLock = [[NSLock alloc] init];
    
    // Callback queue, asynchronous serial queue, proxy callback
    self.sessionDelegateQueue = [[NSOperationQueue alloc] init];
    self.sessionDelegateQueue.maxConcurrentOperationCount = 1;
    // Highest priority, mainly used to provide interactive UI operations, such as handling click events and drawing images to the screen
    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 Download task
                NSURLSessionDownloadTask * obj = [lastTasks objectForKey:key];
                // Create downloadTask for caching
                SGDownloadTask * downloadTask = [self.taskQueue 
                    taskForContentURL:[self getURLFromSessionTask:obj]];
                
                if (downloadTask) {
                    // Change the state
                    [self.taskQueue setTaskState:downloadTask state:SGDownloadTaskStateRunning];
                    SGDownloadTuple * tuple = [SGDownloadTuple 
                        tupleWithDownloadTask:downloadTask sessionTask:obj];
                    [self.taskTupleQueue addTuple:tuple];
                }
            }
        }
    }
    [self.lastResumeLock unlock];
    self.sessionDelegateQueue.suspended = NO;
    // Create a download task NSInvocationOperation
    self.downloadOperation = [[NSInvocationOperation alloc] 
        initWithTarget:self selector:@selector(downloadOperationHandler) object:nil];
    // Download the queue
    self.downloadOperationQueue = [[NSOperationQueue alloc] init];
    // Serial queue
    self.downloadOperationQueue.maxConcurrentOperationCount = 1;
    self.downloadOperationQueue.qualityOfService = NSQualityOfServiceUserInteractive;
   [self.downloadOperationQueue addOperation:self.downloadOperation];
}
Copy the code

When running the task, will perform the thread below, the following look closely, there will be a lot of details, first by resident loop setting thread (resident everybody thought thread need through the runloop at ordinary times, this is one-sided, child threads after open, will take the initiative to perform a task, if the task is done, did not take the initiative to open the child thread runloop, The thread will be automatically destroyed after the task is executed. After the runloop is started, the task will not be destroyed because the task cannot be completed. If the task is a loop itself, the task will not be destroyed, which means the loop is a runloop.

The following is a custom run loop that uses the consumer mode to get the task specification, as shown below

- (void)downloadOperationHandler
{
    // Permanent thread, block, execute, wait (self.concurrentCondition, conditional lock check)
    // Consumer role -- downLoadTask
    while (YES) {
        @autoreleasepool
        {
            if (self.closed) {
                break;
            }
            NSLog(@"current Thread -- %@",[NSThread currentThread]);
            // Condition lock
            [self.concurrentCondition lock];
            // Maximum number of concurrent requests
            while (self.taskTupleQueue.tuples.count >= self.maxConcurrentOperationCount) {
                NSLog(@"current wait Thread -- %@",[NSThread currentThread]);
                [self.concurrentCondition wait];
            }
            [self.concurrentCondition unlock];
            // Get tasks from taskQueue (downloadTaskSync is the consumer of taskQueue)
            The taskQueue will block in the downloadTaskSync method until the taskQueue gets the task
            // When the producer function in taskQueue is called through the SGDownload profile, it is unblocked and continues
            SGDownloadTask * downloadTask = [self.taskQueue downloadTaskSync];
            // Avoid the user forcing the loop to end
            if(! downloadTask) {break;
            }
            // Change the download state to download state
            [self.taskQueue setTaskState:downloadTask state:SGDownloadTaskStateRunning];
            
            / / create NSURLSessionDownloadTask
            NSURLSessionDownloadTask * sessionTask = nil;
            if (downloadTask.resumeInfoData.length > 0) {
                // Restore download progress according to reusmeInfoData (when the status changes, the task is archived to disk)
                sessionTask = [SGDownloadTaskCorrect 
                    downloadTaskWithSession:self.session resumeData:downloadTask.resumeInfoData];
            } else {
                sessionTask = [self.session downloadTaskWithURL:downloadTask.contentURL];
            }
            // Generate a new SGDownloadTuple and place it in the tupleQueue
            SGDownloadTuple * tuple = [SGDownloadTuple 
                tupleWithDownloadTask:downloadTask sessionTask:sessionTask]; [self.taskTupleQueue addTuple:tuple]; [sessionTask resume]; }}}Copy the code

A callback after a single file has been downloaded

- (void)URLSession:(NSURLSession *)session 
    task:(NSURLSessionTask *)sessionTask 
    didCompleteWithError:(NSError *)error
{
    // This lock ensures that several download modules, callbacks specifying the cause of the problem at the same time, or actively secure data downloaded by multiple threads at the same time
    [self.lastResumeLock lock];
    // Conditional lock to ensure data security when processing tasks such as taskTupleQueue
    [self.concurrentCondition lock];
    // Get the task, then update the status of tupleQueue, taskQueue, etc
    SGDownloadTask * downloadTask = [self.taskQueue taskForContentURL:
        [self getURLFromSessionTask:sessionTask]];
    SGDownloadTuple * tuple = [self.taskTupleQueue tupleWithDownloadTask:downloadTask 
        sessionTask:(NSURLSessionDownloadTask *)sessionTask];
    if(! tuple) { [self.taskTupleQueue removeTupleWithSesstionTask:sessionTask];//signal -- wakes up the current thread, and the currently waiting task continues
        [self.concurrentCondition signal];
        [self.concurrentCondition unlock];
        [self.lastResumeLock unlock];
        return;
    }
    
    // updates the status of the downloadTask. If the downloadTask succeeds, it has no value. If the downloadTask fails, resumeInfoData needs to be updated
    // To resume the download
    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 = [NSErrorerrorWithDomain: @"download file is deleted" code:-1 userInfo:nil];
            state = SGDownloadTaskStateFailured;
        } else{ state = SGDownloadTaskStateFinished; }}// Updates the status of tupl. downloadTask to notify archived data
    [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]; }Copy the code

A callback during download to update download information in a timely manner

- (void)URLSession:(NSURLSession *)session 
    downloadTask:(NSURLSessionDownloadTask *)sessionTask 
    didWriteData:(int64_t)bytesWritten 
    totalBytesWritten:(int64_t)totalBytesWritten 
    totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    // Object lock ensures secure access to the downloadTask
    [self.lastResumeLock lock];
    // The user updates the download task information
    SGDownloadTask * downloadTask = [self.taskQueue 
        taskForContentURL:[self getURLFromSessionTask:sessionTask]];
    SGDownloadTuple * tuple = [self.taskTupleQueue 
        tupleWithDownloadTask:downloadTask sessionTask:(NSURLSessionDownloadTask *)sessionTask];
    if(! tuple) { [self.lastResumeLock unlock];return; } update download task information [tuple downloadTask setBytesWritten: bytesWrittentotalBytesWritten:totalBytesWritten
              totalBytesExpectedToWrite:totalBytesExpectedToWrite];
     // This step updates the status of the task and performs an archive to ensure that the data can be successfully recovered even if the data fails
    if(tuple.downloadTask.state ! = SGDownloadTaskStateSuspend) { [self.taskQueue setTaskState:tuple.downloadTask state:SGDownloadTaskStateRunning]; } [self.lastResumeLock unlock]; }Copy the code

The last

SGDownLoad makes full use of locks and semaphores (semaphores are also included in common locks) to take advantage of the producer-consumer model and implement the function of adding tasks. The author’s understanding of locks is well worth learning

This pattern is also covered in the chapter on database transactions and other operations, so it is not just used at the front end

SGDownload is introduced here, want to study carefully can see the source code to continue, address has been given, welcome to discuss