2
votes

I'm trying to fully understand the below code I put together after researching background tasks in iOS and am hoping for some help,

I understand the basic concept,

First we get the app singleton, then we create a block and register with the system the background task, and then finally we asynchronously dispatch the task to run.

So here are the pieces I'm looking for help with:

  1. When background_task is assigned the block, the actual block does not have the code we want run inside it, only the cleanup code in it's completion handler, why is that?

  2. I understand dispatch_async basically starts a new thread and starts working through the code in the block, but where in this dispatch_async request is the background_task referenced? I don't see how the system understands that the code we want executed in the dispatch_async request is related to the background_task we registered earlier.

  3. Why do we need the cleanup code both at the end of the dispatch_async block and in the completion handler of the background_task?

Sorry if these are stupid questions, but I just don't get the syntax,

Here is the code i've cobbled together:

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

__block UIBackgroundTaskIdentifier background_task; //Create a task object

background_task = [application beginBackgroundTaskWithExpirationHandler: ^ { //Register background_task    
                [application endBackgroundTask: background_task]; //Tell the system that we are done with the tasks
                background_task = UIBackgroundTaskInvalid; //Set the task to be invalid
                //Above code called when endBackgroundTask is called
            }];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                //Perform your tasks that your application requires

                [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(updateText) userInfo:nil repeats:YES];
                NSLog(@"\n\nRunning in the background!\n\n");

                [application endBackgroundTask: background_task]; //End the task so the system knows that you are done with what you need to perform
                background_task = UIBackgroundTaskInvalid; //Invalidate the background_task
            });
5
you want someone to explain the code you wrote? - hooleyhoop
@hooleyhoop, no I want someone to help explain the finer details of the code I explicitly stated I "cobbled" together, but thanks for your input. - Woodstock
I wonder whether you actually know what you are trying to do. "Background tasks" are tasks executing when the user switches to another app, and should generally be avoided. And they have nothing at all to do with GCD, which is about tasks executing in background threads instead of the main thread, while your app is the active app. - gnasher729
Indeed, I was confused but I understand now thanks. - Woodstock

5 Answers

4
votes

There is no relationship between the background task identifier and the work you do on a secondary thread. The background task represents a request for some extra time to run. That's all. You need to end it to tell the OS that you've completed the work you wanted to do. If you fail to do that in the time available, your app will be terminated. The task ID is just a token representing the permission from the OS to keep working for a bit of time.

You have to clean up in both places, the expiration handler and the end of your async-dispatched block, because they represent two different occurrences. In the block you dispatch to the concurrent queue, you end the task because you've completed your work in time and you want to let the OS know so it can suspend your app; it doesn't need to terminate it. In the expiration handler, that's your last chance to end the task to prevent your app from being terminated. You haven't completed your work, but you've run out of time. If you didn't end the background task at that point, the OS would kill your app.

By the way, scheduling a timer in a task running on a dispatch queue won't work. A timer is scheduled on a thread's run loop. The worker threads which service dispatch queues can be terminated at any time and, in any case, don't run their run loop.

2
votes

It seem quite long time since this question was asked and answered. I just would like to make a note here as I have a feeling that the user Woodstock (and may be many of us) a bit confused about "background thread" and "background task".

  • The "background thread" is considering in the context of UI thread and background thread.

  • The "background task" is considering in the context of the task allowable to take longer than normal duration (10 seconds in iOS). The method beginBackgroundTaskWithExpirationHandler: tells iOS that you need more time to complete whatever you’re doing in case the app is backgrounded (please refer to app's life cycle concept). After this call, if your app is backgrounded it will still get CPU time until you call endBackgroundTask: or system signals it's going to be expired.

1
votes

Just for clarity I'm setting up the background task as follows:

@interface myClass ()
    @property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundUpdateTask;
@end

- (void)importantStuffToCompleteInBackground
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
    ^{
        [self beginBackgroundUpdateTask];

        // do important background stuff

        [self endBackgroundUpdateTask];
    });
}

- (void)beginBackgroundUpdateTask
{
    self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        [self endBackgroundUpdateTask];
    }];
}

- (void)endBackgroundUpdateTask
{
    [[UIApplication sharedApplication] endBackgroundTask:self.backgroundUpdateTask];
    self.backgroundUpdateTask = UIBackgroundTaskInvalid;
}

According to beginBackgroundTaskWithExpirationHandler: docs the block will be called only shortly before the remaining background time reaches 0 in case you didn't complete the important background stuff block of code in time by calling the endBackgroundTask in the dispatch_async.

1
votes

You might have all kinds of long running background tasks running in your app at any one time. There are several ways you can run a background task.

You could spawn a thread or you could use Grand Central Dispatch to push a block onto a worker queue which might look as simple as..

dispatch_async( queue, ^{
 [self saveAllUserDataToServer];
} );

but don't be fooled, anything involving multithreading is difficult and dangerous. It's a big subject and it's not clear from your question if you have a specific task to do that you would like help with.

One such danger is that your app can be stopped by the user or System at any point. A background task you have already started could be half way complete and then just stop. This could be a disaster or just inconvenient.

iOS provides a way to not have these tasks halted abruptly. Anytime you start such a task you must let the system know, and then tell it when the task has finished, like so..

- (void)saveAllUserDataToServer {
  UIBackgroundTaskIdentifier bt = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler: ^ {}];
  ... do the work ...
  [application endBackgroundTask: background_task];
}

..now you know it is safe to call -saveAllUserDataToServer on a background thread and it will run to completion even if the app is closed half way through.

The one caveat is that you only have a certain time limit to complete the task.. if you take too long the ExpirationHandler block will be called, and you MUST clean up properly in here. This is way the setup code is more likely to look like..

__block UIBackgroundTaskIdentifier bt = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler: ^ {
    .. task didn't complete within time limit.. do additional cleanup
    [[UIApplication sharedApplication] endBackgroundTask: background_task];
}];
0
votes

I was getting the following error when my app went to the background because of a continually running process in the background:

Can't endBackgroundTask: no background task exists with identifier 9, or it may >have already been ended. Break in UIApplicationEndBackgroundTaskError() to debug.

Thankfully I came across hooleyhoop's answer and I was able to get around it with something like this code example:

func run() {
  DispatchQueue(label: "ping").async {
    let tid = UIApplication.shared.beginBackgroundTask(withName: "ping", expirationHandler: {})
    //network code
    //save data to database code
    UIApplication.shared.endBackgroundTask(tid)      
    run() //repeat process
  }
}

You can also use:

UIApplication.shared.beginBackgroundTask(expirationHandler:{}) 

if you don't need a nice name when debugging