Keep just one background task
Asked Answered
T

2

2

I'm developing an application that uses a background task to keep tracking of the user position every 20 seconds. All is fine, except that when I enter the application in background, a new background tasks is created, so that I can have in final multiple background tasks that are running. I tried to add [[UIApplication sharedApplication] endBackgroundTask:bgTask]; in applicationWillEnterForeground, but that do nothing. The point is that I want to invalidate/disable all running background tasks when I enter the app and create a new one when I enter in background, or to keep a just one background task running.

- (void)applicationDidEnterBackground:(UIApplication *)application
{
[self runBackgroundTask:10];
}

-(void)runBackgroundTask: (int) time{
    //check if application is in background mode
    if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {

        __block UIBackgroundTaskIdentifier bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
            [[UIApplication sharedApplication] endBackgroundTask:bgTask];
            bgTask = UIBackgroundTaskInvalid;
        }];

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            NSTimer* t = [NSTimer scheduledTimerWithTimeInterval:time target:self selector:@selector(startTracking) userInfo:nil repeats:NO];
            [[NSRunLoop currentRunLoop] addTimer:t forMode:NSDefaultRunLoopMode];
            [[NSRunLoop currentRunLoop] run];
        });
    }
}

-(void)startTracking{
//Location manager code that is running well
}
Tiphani answered 18/11, 2015 at 17:28 Comment(4)
By the way, if you want to keep track of the user's location while in background, using a timer every 20 seconds is a pretty power hungry way of doing that. Generally one would use the significant change service (which keeps running in the background, getting around the 3 minute limit of UIBackgroundTaskIdentifier, but also uses low power consumption location services).Keelin
@Rob: I need to track the user location, thus the speed, to know if he is in a car or not. Okay, the significant location changes can work, but the first proportion of his trip (about 3 minutes), will not be detected until the significant location executes the didUpdateLocations delegate method.Tiphani
Oh, if you're trying to track the speed of the car, then significant change service will not be sufficiently accurate. You'll have to stay with standard location services, unfortunately. But if the user is in his or her car, they presumably will have access to a power adapter, so in this particular case, perhaps the power consumption is acceptable to the user.Keelin
Exactly! You all said ! A power adapter is extremely useful for such apps (tracking), I just need to optimize the battery consumption when idle. That's why I'm trying to optimize every line of code when in background.Tiphani
K
2

I would suggest changing UIBackgroundTaskIdentifier to be a property of the app delegate class and initialize it to UIBackgroundTaskInvalid in didFinishLaunchingWithOptions. Then, in your other app delegate methods, you can just check the value of this property to determine whether there is a background task identifier to end or not.

--

An unrelated observation, but you don't need that runloop stuff. Just schedule the timer on the main thread/runloop (with scheduledTimerWithTimeInterval) and get rid of all of that runloop stuff (because you already added it to the main runloop and that runloop is already running).

For example, let's assume I wanted to do something every 10 seconds while the app was in background, I'd do something like the following:

@interface AppDelegate ()

@property (atomic) UIBackgroundTaskIdentifier bgTask;
@property (nonatomic, weak) NSTimer *timer;

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.bgTask = UIBackgroundTaskInvalid;
    return YES;
}

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    self.bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        if (self.bgTask != UIBackgroundTaskInvalid) {
            [[UIApplication sharedApplication] endBackgroundTask:self.bgTask];
            self.bgTask = UIBackgroundTaskInvalid;
        }
    }];

    self.timer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(startTracking) userInfo:nil repeats:YES];
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    // invalidate the timer if still running

    [self.timer invalidate];

    // end the background task if still present

    if (self.bgTask != UIBackgroundTaskInvalid) {
        [[UIApplication sharedApplication] endBackgroundTask:self.bgTask];
        self.bgTask = UIBackgroundTaskInvalid;
    }
}

- (void)startTracking{
    NSLog(@"%s", __PRETTY_FUNCTION__);
}

Now, in your code sample, the timer wasn't a repeating timer, so if you only wanted to fire the timer once, then set repeats to NO, but then make sure that startTracking then ended the background task (no point in keeping the app alive if you're not going to do anything else).

BTW, make sure you run this code on a device, and do not run it from Xcode (as being attached to Xcode changes the background behavior of apps).

Keelin answered 18/11, 2015 at 17:50 Comment(9)
For my case the currentRunLoop is obviously the mainRunLoop, no ? And you say that adding a timer is unnecessary, in fact, I have to use a timer to keep the app running in the background for a period greater than 180 seconds.Tiphani
No, I'm saying that scheduledTimerWithTimeInterval already starts the timer in the current runloop. So you don't have to add it to the run loop. Nor do you have to run that runloop (it's already running). The only time you have to manually instantiate a timer and add it to a run loop is when you're doing it on a runloop other than the current one.Keelin
I was testing your observation, I just instantiated scheduledTimerWithTimeInterval without adding it on the currentRunLoop, and the timer is never called.Tiphani
However, you gave me an idea, is to invalidate the timer if it's valid, and now I do not have multiple background tasks, what do you say about that ?Tiphani
@Tiphani - I tested the code in my question and without the runloop stuff, the timer keeps running in the background mode, but stops when the app comes back to foreground. In answer to the "multiple background tasks" question, I'm unclear why you need multiple ones. I start background task when app goes into background and stop it (if necessary) when it enters foreground. So I'm unclear about to which "multiple background tasks" you are referring.Keelin
It seems to work Rob! I'm running location update for 5 seconds every 20 seconds (it's a tracking app), so I'm adding [self runBackgroundTask:5]; at the end of the startTracking method. It's working flawlessly, and I guess that's it's power friendly than my previous method. Just a question, is it safe to let the NSTimer as weak reference, it can not be freed ? And then setting atomic for the UIBackgroundTaskIdentifier is it not a small resource hog ? Anyway, we can never have concurrent access to that property, no ?Tiphani
Re weak timer, when you schedule a repeating timer, it's retained for you until it's invalidated (or, if non-repeating, until it fires). So you don't need a strong reference to it (and you want it nil-ed for you when it is invalidated). Re atomic access to bgTask I did that out of caution because I am not entirely sure if the expiration handler is guaranteed to run on the main thread (probably is, but I'm not sure). You probably don't need to make it atomic, but I was just being careful (and the overhead is negligible).Keelin
@Tiphani If you're worried about power consumption (and we all should be) the use of standard location services and background execution is far more of an issue. It's generally advised to use significant change service for background location updates unless you absolutely need fine grained location services.Keelin
Thank you Rob for all that explanation. Crystal clear! Million thanks :).Tiphani
R
1

Specify location background mode Use an NSTimer in the background by using UIApplication:beginBackgroundTaskWithExpirationHandler: In case n is smaller than UIApplication:backgroundTimeRemaining ,it will work just fine, in case n is larger, the location manager should be enabled (and disabled) again before there is no time remaining to avoid the background task being killed.

This does work since location is one of the three allowed types of background execution.

Note: Did loose some time by testing this in the simulator where it doesn't work, works fine on phone.

Roux answered 18/11, 2015 at 17:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.