Use NSURLSession To Implement Download Task Break Point Continue Example

In recent project, i encounter some problems of downloading large file’s break point continuation. This article will summarize different download methods and point out the advantage and disadvantage of each method.

1. Direct Download With NSData.

[NSData dataWithContentsOfURL:URL];

The disadvantage of this method is obvious, one is blocking the current thread, the other is that as long as the download fails, the data will not be saved, which is obviously not suitable for large file download.

2. Use NSURLSession’s NSURLSessionDownloadTask Method.

To use this method, you need implement below method of NSURLSessionDownloadDelegate.

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
}

But this method also has below drawbacks:

  1. File is downloaded into temp folder, as long as it is not handled in time, it will be deleted by the system,.
  2. When downloading multiple files at the same time, there will be confusion, even if you manually move the downloaded files from temp folder to cache folder, there will be various problems such as file name duplication.

3. Use AFNetworking.

It is also a layer of encapsulation based on task. The specific task still needs to be operated manually by itself, and can not achieve the unified management of break point continuation and network requests. That is, the following methods.

[manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
} destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
} completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
}];

From this method, we can see that AFN is still based on NSURLSession Download Task packaging, and it is not as good as expected.

4. Download Code Improvement.

To solve above problem, i made an improvement, that is, using NSURLSessionDataTask to send get requests directly to achieve the download of breakpoint continuity, and support multi-file download.

  1. Since use NSURLSession to send network requests, then we can lazily load a session in a utility class.
    - (NSURLSession *)session
      {
            if (!_session) {
              _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc]init]];
            }
            return _session;
       }
  2. When you have a session, you also need to have a task, which is also lazy loading. Of course, NSURLDataTask is used here.
    - (NSURLSessionDataTask *)dataTask
            {
            if (!_dataTask) {
            NSError *error = nil;
            NSInteger alreadyDownloadLength = XHRAlreadyDownloadLength;
            //Download has completed.
            if ([self.totalDataLengthDictionary[self.fileName]integerValue] && [self.totalDataLengthDictionary[self.fileName] integerValue] == XHRAlreadyDownloadLength)
            {
            !self.completeBlock?:self.completeBlock(XHRFilePath,nil);
            return nil;
            }
            //If an existing file is larger than the target, that means the download file is executed incorrectly. Delete the file and download again
            else if ([self.totalDataLengthDictionary[self.fileName] integerValue] < XHRAlreadyDownloadLength)
            {
            [[NSFileManager defaultManager]removeItemAtPath:XHRFilePath error:&error];
            if (!error) {
            alreadyDownloadLength = 0;
            }
            else
            {
            NSLog(@"Creation task failed. Please restart");
            return nil;
            }
            }
            //The downloaded file size is less than the total file size. Continue the download operation
    
            // Create mutableRequest Object
            NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.urlString]];
    
            // Set request header
            //Range:bytes=xxx-xxx
            [mutableRequest setValue:[NSString stringWithFormat:@"bytes=%ld-",alreadyDownloadLength] forHTTPHeaderField:@"Range"];
            _dataTask = [self.session dataTaskWithRequest:mutableRequest];
    
            }
            return _dataTask;
            }
  3. We have operate follow check in above task method, 1. Check whether the download is complete or not. If the download complete, call the completed block directly. 2. Check whether the locally stored file is larger than the target file or not. If it is larger than the target file, it means that the file has errors and the old file must be deleted and downloaded again. 3. Continue downloading while the download do not complete.
  4. Key parameters : 1. The total length of the file is the unique key that is stored in the dictionary and written to the sandbox after MD5 encryption through the url address after the first response is received by the server. 2. Get the currently downloaded file size from the NSFileManager and file name.
    [[[NSFileManager defaultManager]attributesOfItemAtPath:XHRFilePath error:nil][NSFileSize] integerValue];

5. Implementation Of Proxy Method.

Initialized code emulation system, which passes in the block of download progress and the block at completion, to be invoked when appropriate.

/* Download Method. */
- (void)downloadFromURL:(NSString *)urlString progress:(void(^)(CGFloat downloadProgress))downloadProgressBlock complement:(void(^)(NSString *filePath,NSError *error))completeBlock;

5.1 When The Server Response Is Received.

// The proxy method called after the server responds.
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
        {
            //  Receive the server response.
            //  Get the full length of the file.
            self.totalDataLengthDictionary[self.fileName] = @([response.allHeaderFields[@"Content-Length"] integerValue] + XHRAlreadyDownloadLength);
            [self.totalDataLengthDictionary writeToFile:XHRTotalDataLengthDictionaryPath atomically:YES];
            // Open outputStream
            [self.stream open];

            //  Call the block setting to allow further access. 
            completionHandler(NSURLSessionResponseAllow);

         }

5.2 After Receiving Data, Write It To The Sandbox Bit By Bit.

//  A proxy method called after receiving data
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
        {
           //  Write the data returned from the server into sandbox with stream 
           [self.stream write:data.bytes maxLength:data.length];
           self.downloadProgress = 1.0 * XHRAlreadyDownloadLength / [self.totalDataLengthDictionary[self.fileName] integerValue];
        }

5.3 After Download Is Completed.

//  The proxy method invoked after the task is completed.
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
        {
           if (error) {
             self.downloadError = error;
             return;
           }
           // Close stream.
           [self.stream close];
           // Relaese task.
           [self.session invalidateAndCancel];
           self.dataTask = nil;
           self.session = nil;
           // Empty the dictionary that save the file full length.
           [self.totalDataLengthDictionary removeObjectForKey:self.fileName];
           [self.totalDataLengthDictionary writeToFile:XHRTotalDataLengthDictionaryPath atomically:YES];
           !self.completeBlock?:self.completeBlock(XHRFilePath,nil);
        }

5.5 Rewrite downloadProgress, downloadError Methods To Listen For Their Values, And Call Back The Corresponding Block Once The Value Is Available.

- (void)setDownloadProgress:(CGFloat)downloadProgress
        {
           _downloadProgress = downloadProgress;
           !self.downloadProgressBlock?:self.downloadProgressBlock(downloadProgress);
        }

        - (void)setDownloadError:(NSError *)downloadError
        {
           _downloadError = downloadError;
           !self.completeBlock?:self.completeBlock(nil,downloadError);
        }