Chaining background NSURLSession uploads

2020-06-23 06:30发布

问题:

Has anyone been successful in chaining NSURLSession background uploads?

I am trying to upload a huge video file in 5 MB parts using background upload of NSURLSession. The uploads has to be in order. The whole thing works fine in foreground. I am using AFNetwoking for this, and its a multi part upload. But when the app is in background, the first item uploads fine and starts the second one in background (in setDidFinishEventsForBackgroundURLSessionBlock of AFURLSessionManager). But it stops abruptly (my best guess is in 30 seconds, as an app woken up in background has a max lifetime of 30 sec) and then nothing happens. I expected the second session will finish in background and call up the third etc - a chain behaviour, but this just does not seem to work.

I have tried adding all file parts to a single NSURLSession in one go with a HTTPMaximumConnectionsPerHost = 1 - this works fine and uploads the full file in parts. But the file parts are picked in random order, i.e. part 1 gets uploaded, then part 5, part 3, part 10 etc …. I tried adding this in an NSOperationQueue with dependency between operations and this seems to mess up the entire thing - the upload does not work at all.

I know that the video file can be uploaded as a single file in background, but the server expects this in 5 MB parts. Hence I guess my only option is to chain uploads, or add all the parts to a NSURLSession, but make sure they are always uploaded in the order they are added.

Any help would be appreciated.

My code:

 - (void)viewDidLoad {

    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[NSString stringWithFormat:@"%d", rand()]];
    AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:config];
    config.HTTPMaximumConnectionsPerHost = 1;
    [manager setDidFinishEventsForBackgroundURLSessionBlock:^(NSURLSession *session) {

        dispatch_async(dispatch_get_main_queue(), ^{
            // Call the completion handler to tell the system that there are no other background transfers.
            //                completionHandler();
            [self upload];
        });
    }];
}

- (IBAction)start:(id)sender {

    [self upload];
}

-(void) upload {

    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"Sample" ofType:@"mp4"];
    AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];

    NSDictionary *parameters = [NSDictionary dictionaryWithObjectsAndKeys:@"234", @"u", @"Sample.mp4", @"f",nil];
    NSMutableURLRequest *request = [serializer multipartFormRequestWithMethod:@"POST" URLString:urlString parameters:parameters constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        [formData appendPartWithFileURL:[NSURL fileURLWithPath:filePath] name:@"data" fileName:@"Sample.mp4" mimeType:@"video/mp4" error:nil];
    } error:nil];


    __block NSString *tempMultipartFile = [NSTemporaryDirectory() stringByAppendingPathComponent:@"Test"];
    tempMultipartFile = [tempMultipartFile stringByAppendingString:[NSString stringWithFormat:@"%d", rand()]];
    NSURL *filePathtemp = [NSURL fileURLWithPath:tempMultipartFile];
    __block NSProgress *progress = nil;
    [serializer requestWithMultipartFormRequest:request writingStreamContentsToFile:filePathtemp completionHandler:^(NSError *error) {
        NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithRequest:request fromFile:filePathtemp progress:&progress completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {

            NSLog(@"Request--> %@.\n Response --> %@ \n%@",  request.URL.absoluteString ,responseObject, error? [NSString stringWithFormat:@" with error: %@", [error localizedDescription]] : @""); //Lets us know the result including failures
            [[NSFileManager defaultManager] removeItemAtPath:tempMultipartFile error:nil];

        }];
        [uploadTask resume];
        [manager setTaskDidSendBodyDataBlock:^(NSURLSession *session, NSURLSessionTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {

            NSLog(@"uploading");
        }];
    }];
}

回答1:

Well, finally I reached out to Apple for clarifications on chaining background uploads - This is not possible in iOS.NSURLSession has a Resume Rate Limiter which prevents apps from executing chained tasks in background as explained in https://forums.developer.apple.com/thread/14854. Instead, apple suggests batch transfers or other options like https://forums.developer.apple.com/thread/14853. The other thing I was asking was to order the multiple tasks in upload queue - i.e, force NSURLSession to upload tasks in the order in which they are added. As pointed by dgatwood, using an NSOperationQueue is not possible and Apple also mentioned the same.As mentioned by eskimo in response mail "NSURLSession does not guarantee to run your requests in order and there’s no way to enforce that." So I am pretty much left option less on my original problem.



回答2:

An NSOperationQueue goes away when your app does, which isn't too long after you put it into the background. So that's not going to work very well.

Instead, store a list of files remaining to upload, in order—either in a file on disk or in NSUserDefaults, depending on your personal preference. Then, use an upload task in a background session to start the first task. When it finishes, if your app isn't running, it should automatically get relaunched in the background to handle the data.

To support this behavior, in your application:handleEventsForBackgroundURLSession:completionHandler: method, re-create the background session just like you did originally, and store the completion handler.

Shortly thereafter, your delegate methods for the request should be called just as though your app were still running when the download finished. Among other things those methods can provide your app with the response data from the server, the response object (for checking the status code), etc.

When you get the didCompleteWithError delegate call (which is nil on success, IIRC), if the transfer failed, try it again or whatever. If it succeeded, start uploading the next one and update your list of files on disk.

Either way, when your session delegate's ** URLSessionDidFinishEventsForBackgroundURLSession:** method is called, call the handler you stored earlier, roughly like this:

[[NSOperationQueue mainQueue] addOperationWithBlock:^{
    storedHandler();
}];

By calling the completion handler, you're telling the OS that you don't need to keep running.

Rinse, repeat.

If your app is still running when the request completes, everything happens just as described above, except that you don't get the application:handleEventsForBackgroundURLSession:completionHandler: or URLSessionDidFinishEventsForBackgroundURLSession: calls, which means you don't have to store the completion handler or call it.

See URL Session Programming Guide for details.