为什么 beginBackgroundTaskWithExpirationHandler 允许 NSTimer 在后台 运行?

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

我知道有很多关于类似主题的问题,但我认为没有任何问题能完全解决这个问题(尽管我很高兴被证明是错误的!)。

首先介绍一些背景;我开发了一个在后台监控用户位置的应用程序,它工作正常,但电池使用率很高。为了尝试改进这一点,我的 objective 是每 x 分钟 'wake up',调用 startUpdatingLocation 获取位置,然后调用 stopUpdatingLocation.

考虑以下我放在一起进行测试的示例:

@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

正如预期的那样,当应用程序进入后台时,CLLocationManager 不会更新位置,因此 NSTimer 会暂停,直到应用程序进入前台。

一些类似的 SO 问题询问了关于在后台保留 NSTimers 运行ning 以及 beginBackgroundTaskWithExpirationHandler 的使用,所以我将其添加到 viewDidLoad 方法之前的 NSTimer 已启动。

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);

经过短期测试(是否长期有效还有待证明)这似乎解决了问题,但我不明白为什么。

如果您查看下面的日志输出,您可以看到调用 beginBackgroundTaskWithExpirationHandler 实现了所需的 objective,因为 NSTimer 在应用程序进入后台时继续触发:

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

由此可以看出,后台任务按预期在大约 3 分钟后过期,但 NSTimer 在此之后继续触发。

这种行为是预期的吗?我会 运行 进行更多测试,看看这个解决方案是否长期有效,但我不禁觉得我在使用后台任务时遗漏了一些东西?

有一个选项可以为长时间的 运行 任务使用后台任务,然后 iOS 不会暂停执行。

根据 Background execution 文档要使用它,应该将 UIBackgroundModes 键添加到 Info.plist ,值为 location (用于位置更新)。可能还有另一个原因:

摘自 实施长期 运行 任务

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.

大概初始化CLLocationManager就可以让后台任务不被挂起。

经过大量测试,事实证明 NSTimer 不会在任务过期后无限期地运行。在某个时间点(可以是几个小时,也可以是立即)计时器停止触发。

最后,我的问题的解决方案是降低 GPS 精度并在我不需要监视位置的时候增加距离过滤器,如此处讨论:

Periodic iOS background location updates