iOS uses NSURLSession to implement background upload and download

The basic logic of NSURLSession background upload is: first create a background mode NSURLSessionConfiguration, then create an NSURLSession through this configuration, then create the relevant NSURLSessionTask, and finally process the relevant proxy events.

1. Create NSURLSession

- (NSURLSession *)backgroundURLSession {
    static NSURLSession *session = nil;
    static dispatch_once_t onceToken;
    dispatch_once( & amp;onceToken, ^{
        NSURLSessionConfiguration* sessionConfig = nil;
        NSString *identifier = [NSString stringWithFormat:@"%@.%@", [NSBundle mainBundle].bundleIdentifier, @"HttpUrlManager"];
        sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
        //Requested caching strategy
        sessionConfig.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
        //Data transmission timeout, will be cleared when transmission is resumed
        sessionConfig.timeoutIntervalForRequest = 60;
        //Single request timeout determines the maximum life cycle of a request
        sessionConfig.timeoutIntervalForResource = 60;
        //Requested service type
        sessionConfig.networkServiceType = NSURLNetworkServiceTypeDefault;
        //Whether to allow the use of mobile networks (telephone networks) default is YES
        sessionConfig.allowsCellularAccess = YES;
        //Background mode takes effect, YES allows adaptive system performance adjustment
        sessionConfig.discretionary = YES;
        sessionConfig.HTTPMaximumConnectionsPerHost = 20;
        sessionConfig.sessionSendsLaunchEvents = NO;
        sessionConfig.multipathServiceType = NSURLSessionMultipathServiceTypeHandover;

        session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
    });
    
    return session;
}

NSURLSessionConfiguration configuration has three modes:

//The default mode is similar to the original NSURLConnection, you can use cached Cache, Cookie, and authentication
 + (NSURLSessionConfiguration *)defaultSessionConfiguration;

//Timely mode does not use cached Cache, Cookie, authentication
 + (NSURLSessionConfiguration *)ephemeralSessionConfiguration;

//Background mode completes upload and download in the background. When creating the Configuration object, you need to give an NSString ID to track which Session is used to complete the work.
 + (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier
  • The allowsCellularAccess property specifies whether cellular connections are allowed
  • When the discretionary attribute is YES, it means that the system itself selects the best network connection configuration when the program is running in the background. This attribute can save bandwidth through the cellular connection.

When using background transfer data, it is recommended to use the discretionary attribute instead of the allowsCellularAccess attribute because it takes WiFi and power availability into account. Added: This flag allows the system to perform performance optimizations for assigned tasks. This means that the device will only transfer data over Wifi when the device has enough power. If the battery is low, or there is only one cellular connection, the transmission task will not run. Background transfers always run in discretionary mode.

2. Background upload

- (void)upload:(NSString *)urlStr data:(NSData *)data headers:(NSDictionary *)headers parameters:(NSDictionary *)parameters name:(NSString *)name filename:(NSString *)filename mimeType :(NSString *)mimeType success:(void (^)(id responseObject))success failure:(void (^)(int code, NSString *message))failure {
    NSURL *url = [NSURL URLWithString:urlStr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = @"POST";
    NSString *string = [NSString stringWithFormat:@"multipart/form-data; charset=utf-8; boundary=%@", kBoundary];
    [request setValue:string forHTTPHeaderField:@"Content-Type"];
    if (headers != nil) {
        for (NSString *key in headers.allKeys) {
            [request setValue:headers[key] forHTTPHeaderField:key];
        }
    }

    NSData *bodyData = [self bodyFormData:data parameters:parameters name:name filename:filename mimeType:mimeType];
    NSString *tempPath = NSTemporaryDirectory();
    NSTimeInterval interval = [NSDate.now timeIntervalSince1970];
    NSString *tempName = [NSString stringWithFormat:@"temp%.0f_%@", interval, filename];
    NSString *tempPath = [tempPath stringByAppendingPathComponent:tempName];
    [bodyData writeToFile:tempPath atomically:YES];

    NSURLSession *session = self.backgroundURLSession;
    NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromFile:[NSURL fileURLWithPath:tempPath]];
    [uploadTask resume];
}

- (NSData *)bodyFormData:(NSData *)data parameters:(NSDictionary *)parameters name:(NSString *)name filename:(NSString *)filename mimeType:(NSString *)mimeType {
    if (data == nil || data.length == 0) {
        return nil;
    }
    NSMutableData *formData = [NSMutableData data];
    NSData *lineData = [@"\r\
" dataUsingEncoding:NSUTF8StringEncoding];
    NSData *boundary = [[NSString stringWithFormat:@"--%@", kBoundary] dataUsingEncoding:NSUTF8StringEncoding];
    
    if (parameters != nil) {
        [parameters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
            [formData appendData:boundary];
            [formData appendData:lineData];
            NSString *thisFieldString = [NSString stringWithFormat:@"Content-Disposition: form-data; name="%@"\r\
\r\
%@", key, obj];
            [formData appendData:[thisFieldString dataUsingEncoding:NSUTF8StringEncoding]];
            [formData appendData:lineData];
        }];
    }
    
    [formData appendData:boundary];
    [formData appendData:lineData];
    NSString *thisFieldString = [NSString stringWithFormat:@"Content-Disposition: form-data; name="%@"; filename="%@"\r\
Content-Type: %@", name, filename, mimeType];
    [formData appendData:[thisFieldString dataUsingEncoding:NSUTF8StringEncoding]];
    [formData appendData:lineData];
    [formData appendData:lineData];
    
    [formData appendData:data];
    [formData appendData:lineData];
    [formData appendData:[[NSString stringWithFormat:@"--%@--\r\
", kBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
    
    return formData;
}

There are 4 ways to upload:

/* Creates an upload task with the given request. The body of the request will be created from the file referenced by fileURL */
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;

/* Creates an upload task with the given request. The body of the request is provided from the bodyData. */
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;

- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;

- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(nullable NSData *)bodyData completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;

The background mode does not support the use of upload methods with callbacks, otherwise an error will be reported:

Completion handler blocks are not supported in background sessions. Use a delegate instead.

The background mode does not support the NSData upload method, otherwise an error will be reported:

Upload tasks from NSData are not supported in background sessions

So if you use background mode to upload, choose the uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL method.

NSURLSessionDataDelegate upload proxy event

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
    NSLog(@"URLSession didSendBodyData progress: %f" ,totalBytesSent/(float)totalBytesExpectedToSend);
}

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
    NSLog(@"%s", __func__);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    NSMutableData *responseData = self.responsesData[@(dataTask.taskIdentifier)];
    if (!responseData) {
        responseData = [NSMutableData dataWithData:data];
        self.responsesData[@(dataTask.taskIdentifier)] = responseData;
    } else {
        [responseData appendData:data];
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error) {
        NSLog(@"URLSession didCompleteWithError %@ failed: %@", task.originalRequest.URL, error);
    }
    NSMutableData *responseData = self.responsesData[@(task.taskIdentifier)];
    if (responseData) {
        NSDictionary *response = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:nil];
        if (response) {
            NSLog(@"response = %@", response);
        } else {
            NSString *errMsg = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
            NSLog(@"responseData = %@", errMsg);
        }
        [self.responsesData removeObjectForKey:@(task.taskIdentifier)];
    } else {
        NSLog(@"responseData is nil");
    }
}

//Download event
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    NSLog(@"URLSession downloadTask didFinishDownloadingToURL %@", downloadTask.originalRequest.URL);
    NSData *responseData = [NSData dataWithContentsOfURL:location];
    if (responseData != nil) {
        self.responsesData[@(downloadTask.taskIdentifier)] = responseData;
    }
}

3. Background request

- (void)request:(NSString *)urlStr method:(NSString *)method headers:(NSDictionary *)headers parameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure: (void (^)(int code, NSString *message))failure {
    urlStr = [self getFullUrlString:urlStr parameters:parameters];
    NSURL *url = [NSURL URLWithString:urlStr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = method;
    if (headers != nil) {
        for (NSString *key in headers.allKeys) {
            [request setValue:headers[key] forHTTPHeaderField:key];
        }
    }

    NSURLSession *session = self.backgroundURLSession;
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
    [task resume];
}

- (NSString *)getFullUrlString:(NSString *)urlStr parameters:(NSDictionary *)parameters {
    NSMutableString *newStr = [NSMutableString stringWithString:urlStr];
    if (parameters.allKeys.count > 0) {
        BOOL isFirst = NO;
        for (NSString *key in parameters) {
            isFirst = YES;
            [newStr appendString:isFirst?@"?":@" & amp;"];
            [newStr appendFormat:@"%@=%@", key, parameters[key]];
        }
    }
    return newStr;
}

4. Background download

- (void)download:(NSString *)urlStr headers:(NSDictionary *)headers parameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(int code, NSString *message))failure {
    urlStr = [self getFullUrlString:urlStr parameters:parameters];
    NSURL *url = [NSURL URLWithString:urlStr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    if (headers != nil) {
        for (NSString *key in headers.allKeys) {
            [request setValue:headers[key] forHTTPHeaderField:key];
        }
    }
    NSURLSession *session = self.backgroundURLSession;
    NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request];
    [task resume];
}

5. Interaction between Session and ApplicationDelegate

Using the BackgroundSession background mode, when the user switches to the background while the Task is executing, the Session will interact with the ApplicationDelegate, and the Task in the BackgroundSession will continue to download/upload.

Now analyze the relationship between Session and Application in three scenarios:

(1) When multiple Tasks are added, the program does not switch to the background.

In this case, Task will be downloaded normally according to the settings of NSURLSessionConfiguration and will not interact with ApplicationDelegate.

(2) When multiple Tasks are added, the program switches to the background and all Tasks are downloaded

After switching to the background, the Session’s Delegate will no longer receive Task-related messages until all Tasks are completed. The system will call ApplicationDelegate’s application:handleEventsForBackgroundURLSession:completionHandler: callback, and then “report” the download work. For each The background download Task calls URLSession:downloadTask:didFinishDownloadingToURL: (if successful) and URLSession:task:didCompleteWithError: (will be called on success or failure) in the Session’s Delegate.

AppDelegate:

@property (copy, nonatomic) void(^backgroundSessionCompletionHandler)();

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier
  completionHandler:(void (^)())completionHandler {
    self.backgroundSessionCompletionHandler = completionHandler;
}

Session Delegate

@interface MyViewController()<NSURLSessionDelegate>
@end

@implementation MyViewController

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    if (appDelegate.backgroundSessionCompletionHandler) {
        void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
        appDelegate.backgroundSessionCompletionHandler = nil;
        completionHandler();
    }
    NSLog(@"All tasks are finished");
}

@end

(3) When multiple Tasks are added, the program switches to the background, several Tasks are downloaded, and then the user switches to the foreground. (The program does not exit)

After switching to the background, the Session’s Delegate still cannot receive messages. After downloading several Tasks and then switching to the foreground, the system will first report the status of the Tasks that have been downloaded, and then continue to download the Tasks that have not been downloaded. The subsequent process is the same as the first case.

(4) When multiple Tasks are added, the program switches to the background, and several Tasks have been completed, but there are still Tasks that have not been downloaded, turn off the force exit program, and then enter the program again. (The program exited)

Since the program has already exited, subsequent Tasks that are no longer available before the Session is downloaded must have failed. However, those Tasks that have been downloaded successfully and newly started programs do not have the opportunity to listen to the “report”. After experiments, it was found that the NSString type ID previously set in NSURLSessionConfiguration worked at this time. When the IDs are the same, once the Session object is generated and the Delegate is set, you can immediately receive the end of the Task that did not report the work before closing the program last time. (success or failure).