1

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?

Gary Wright
  • 2,441
  • 1
  • 20
  • 32

2 Answers2

1

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

Community
  • 1
  • 1
Gary Wright
  • 2,441
  • 1
  • 20
  • 32
0

There is an option to use background task for long running tasks and then iOS doesn't suspednd execution.

According to Background execution document to use it one should add UIBackgroundModes key to Info.plist with value location (for location updates). There could be also another reason:

From section Implementing Long-Running Tasks:

Apps that implement these services must declare the services they support and use system frameworks to implement the relevant aspects of those services. Declaring the services lets the system know which services you use, but in some cases it is the system frameworks that actually prevent your application from being suspended.

Probably it is enough to initialize CLLocationManager to cause background task not to be suspended.

DanielS
  • 182
  • 14