Search code examples
iosios8nstimercllocationmanager

Why does beginBackgroundTaskWithExpirationHandler allow an NSTimer to run in the background?


I know there are a lot of questions on similar topics, but I don't think any quite address this question (although I'm happy to be proved wrong!).

First some background; I have developed an app that monitors user location in the background and this is working fine, but battery usage is high. To try and improve this, my objective is to 'wake up' every x minutes, call startUpdatingLocation get a position, then call stopUpdatingLocation.

Consider the following sample that I put together for testing:

@interface ViewController ()
@property (strong, nonatomic) NSTimer *backgroundTimer;
@property (strong, nonatomic) CLLocationManager *locationManager;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // Add observers to monitor background / foreground transitions
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil];

    _locationManager = [[CLLocationManager alloc] init];
    [_locationManager requestAlwaysAuthorization];
    _locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters;
    _locationManager.delegate = self;
    _locationManager.distanceFilter=100;
    [_locationManager startUpdatingLocation];


    NSTimeInterval time = 60.0;
    _backgroundTimer =[NSTimer scheduledTimerWithTimeInterval:time target:self selector:@selector(startLocationManager) userInfo:nil repeats:YES];

}

-(void)applicationEnterBackground{
    NSLog(@"Entering background");
}

-(void)applicationEnterForeground{
    NSLog(@"Entering foreground");
}

-(void)startLocationManager{
    NSLog(@"Timer fired, starting location update");
    [_locationManager startUpdatingLocation];
}

-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations{
    NSLog(@"New location received");
    [_locationManager stopUpdatingLocation];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];    
}
@end

As expected, when the application enters the background the CLLocationManager is not updating location, so the NSTimer is paused until the application enters the foreground.

Some similar SO questions have asked about keeping NSTimers running in the background, and the use of beginBackgroundTaskWithExpirationHandler so I added this to the viewDidLoad method just before the NSTimer is started.

UIBackgroundTaskIdentifier bgTask = 0;
UIApplication *app = [UIApplication sharedApplication];
NSLog(@"Beginning background task");
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
    NSLog(@"Background task expired");
    [app endBackgroundTask:bgTask];
}];
NSLog(@"bgTask=%d", bgTask);

Over short tests (whether it works long term is yet to be proved) this seems to address the issue, but I don't understand why.

If you take a look at the log output below, you can see that calling beginBackgroundTaskWithExpirationHandler achieves the desired objective as the NSTimer continues to fire when the app enters the background:

2015-03-26 15:45:38.643 TestBackgroundLocation[1046:1557637] Beginning background task
2015-03-26 15:45:38.645 TestBackgroundLocation[1046:1557637] bgTask=1
2015-03-26 15:45:38.703 TestBackgroundLocation[1046:1557637] New location received
2015-03-26 15:45:46.459 TestBackgroundLocation[1046:1557637] Entering background
2015-03-26 15:46:38.682 TestBackgroundLocation[1046:1557637] Timer fired, starting location update
2015-03-26 15:46:38.697 TestBackgroundLocation[1046:1557637] New location received
2015-03-26 15:47:38.666 TestBackgroundLocation[1046:1557637] Timer fired, starting location update
2015-03-26 15:47:38.677 TestBackgroundLocation[1046:1557637] New location received
2015-03-26 15:48:38.690 TestBackgroundLocation[1046:1557637] Timer fired, starting location update
2015-03-26 15:48:38.705 TestBackgroundLocation[1046:1557637] New location received
2015-03-26 15:48:42.357 TestBackgroundLocation[1046:1557637] Background task expired
2015-03-26 15:49:38.733 TestBackgroundLocation[1046:1557637] Timer fired, starting location update
2015-03-26 15:49:38.748 TestBackgroundLocation[1046:1557637] New location received
2015-03-26 15:50:38.721 TestBackgroundLocation[1046:1557637] Timer fired, starting location update
2015-03-26 15:50:38.735 TestBackgroundLocation[1046:1557637] New location received
2015-03-26 15:50:44.361 TestBackgroundLocation[1046:1557637] Entering foreground

From this, you can see that the background task expires after approximately 3 minutes as expected, but the NSTimer continues to fire after that.

Is this behaviour expected? I will run some more tests to see if this solution works in the long term, but I can't help but feel I'm missing something with the use of the background task?


Solution

  • After lots of testing, it turns out that the NSTimer does not run indefinitely after the task has expired. At some point (can be hours, can be immediately) the timer ceases to fire.

    In the end, the solution to my problem was reducing the GPS accuracy and increasing the distance filter at times where I did not need to monitor the position, as discussed here:

    Periodic iOS background location updates