Amos
Amos

Reputation: 888

Background task block function not finishing

I'm working on an iphone app that occasionally fires a task in the background to rearrange some data and upload it to a server. I've used a lot of the principles from Grand Central Dispatch (GCD) with CoreData to get things running, since I'm editing objects that persist in Core Data, but the code only occasionally finishes running despite the application saying it has almost the full 600 seconds of execution time remaining.

The code i'm using:

__block UIBackgroundTaskIdentifier bgTask;
UIApplication *application = [UIApplication sharedApplication]; //Get the shared application instance

NSLog(@"BackgroundTimeRemaining before block: %f", application.backgroundTimeRemaining);

bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
    // Clean up any unfinished task business by marking where you.
    // stopped or ending the task outright.
    [application endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}];

// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    // Do the work associated with the task, preferably in chunks.

    NSLog(@"BackgroundTimeRemaining after block: %f", application.backgroundTimeRemaining);
    NSLog(@"Fixing item in the background");

    //Create secondary managed object context for new thread
    NSManagedObjectContext *backgroundContext = [[NSManagedObjectContext alloc] init];
    [backgroundContext setPersistentStoreCoordinator:[self.managedObjectContext persistentStoreCoordinator]];

    /* Save the background context and handle the save notification */
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(backgroundContextDidSave:)
                                                 name:NSManagedObjectContextDidSaveNotification
                                               object:backgroundContext];

    //creating runloop to kill location manager when done
    NSDate *stopDate = [[NSDate date] dateByAddingTimeInterval:60];
    [[NSRunLoop currentRunLoop] runUntilDate:stopDate];
    NSLog(@"Stop time = %@", stopDate);

    MasterViewController *masterViewContoller = [[MasterViewController alloc] init];
    masterViewContoller.managedObjectContext = backgroundContext;
    [[masterViewContoller locationManager] startUpdatingLocation];
    NSLog(@"Successfully fired up masterViewController class");

    [masterViewContoller adjustDataInBackground:FALSE];
    NSLog(@"Fixed Object!");

    //save background context
    [backgroundContext save:NULL];

    //unregister self for notifications
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:NSManagedObjectContextDidSaveNotification
                                                  object:backgroundContext];

    [application endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
});

The issue is that "adjustDataInBackground:FALSE" is a pretty long method that calls additional supporting methods (including creation and saving of core data objects), and when the background task doesn't allow all of those methods to finish it corrupts my data.

Is there a better way of handling this kind of an operation? Do i need to put all my raw code into the background task block directly?

Upvotes: 2

Views: 2791

Answers (1)

Amos
Amos

Reputation: 888

So it turns out I had two weird things going on that were tripping up the background task:

  • Asynchronous URL connections (when their initiating method finished, iOS thought the background task was done even if the response hadn't yet been received)
  • A location manager specific to the background task (apparently a major no-no...apple's got some documentation on this but the console would spit out an error about it sometimes)

Here's the code I'm now using (it works so far):

    __block UIBackgroundTaskIdentifier bgTask;
UIApplication *application = [UIApplication sharedApplication]; //Get the shared application instance

NSLog(@"BackgroundTimeRemaining before block: %f", application.backgroundTimeRemaining);

bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
    // Clean up any unfinished task business by marking where you.
    // stopped or ending the task outright.
    [application endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}];

// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    // Do the work associated with the task, preferably in chunks.

    NSLog(@"BackgroundTimeRemaining after block: %f", application.backgroundTimeRemaining);

    //Create secondary managed object context for new thread
    NSManagedObjectContext *backgroundContext = [[NSManagedObjectContext alloc] init];
    [backgroundContext setPersistentStoreCoordinator:[self.managedObjectContext persistentStoreCoordinator]];

    /* Save the background context and handle the save notification */
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(backgroundContextDidSave:)
                                                 name:NSManagedObjectContextDidSaveNotification
                                               object:backgroundContext];

    //Set a grace period during which background updates can't stack up...
    //This number should be more than the longest combo of timeout values in adjustDataInBackground
    NSDate *stopDate = [[NSDate date] dateByAddingTimeInterval:90];
    __lastBackgroundSnapshot = stopDate;

    NSLog(@"Stop time = %@", stopDate);

    MasterViewController *masterViewContoller = [[MasterViewController alloc] init];
    masterViewContoller.managedObjectContext = backgroundContext;
    NSLog(@"Successfully fired up masterViewController class");

    [masterViewContoller adjustDataInBackground];
    NSLog(@"adjustDataInBackground!");

    //just in case
    [[self locationManager] stopUpdatingLocation];

    //save background context
    [backgroundContext save:NULL];

    NSLog(@"Uploading in background");
    //send results to server
    postToServer *uploadService = [[postToServer alloc] init];
    uploadService.managedObjectContext = backgroundContext;
    [uploadService uploadToServer];

    //save background context after objects are marked as uploaded
    [backgroundContext save:NULL];

    //unregister self for notifications
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:NSManagedObjectContextDidSaveNotification
                                                  object:backgroundContext];

    [application endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
});

In addition, I added the following runloop to my asynchronous URLConnection objects so they stayed alive long enough to finish their business. While it's not the most graceful way of handling it, it works as long as you can handle the failure gracefully if the runloop ends without the server exchange finishing.

A runloop (adjusted for different timeouts depending on the task):

//marks the attempt as beginning
self.doneUpload = [NSNumber numberWithBool:FALSE];

[[uploadAttempt alloc] fireTheUploadMethod];

//if uploading in the background, initiate a runloop to keep this object alive until it times out or finishes
if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground)
{
    //Timeout length to wait in seconds to allow for async background execution
    NSDate *stopDate = [[NSDate date] dateByAddingTimeInterval:120];

    do {
        NSLog(@"Waiting for upload to return, time left before timeout: %f", [stopDate timeIntervalSinceNow]);
        [[NSRunLoop currentRunLoop] runUntilDate:stopDate];
    } while ([stopDate timeIntervalSinceNow] > 0 && self.doneUpload == [NSNumber numberWithBool:FALSE]);
}

Hope this helps anyone who runs into this in the future!

Upvotes: 3

Related Questions