iOS 8 iBeacon触发的后台位置更新
iOS 8 Background Location Update triggered by iBeacon
我正在尝试制作一个可以由 iBeacon 触发唤醒(从 killed/suspended/terminated)以记录每秒 GPS 信息的应用程序。当 phone 超出信标范围时,GPS 记录应该停止。我已经成功地让我的应用程序在进入和超出 iBeacon 范围时识别 didEnterRegion 和 didExitRegion 方法。在 didEnterRegion 方法中,我基本上想说类似 [locationManager startUpdatingLocation]
的内容,以便我可以开始跟踪用户的位置。但是,当我尝试添加这行代码时,位置更新会在大约 10 秒后停止。
后来我找到了一个article about background location updates that came with this Github project。我将 BackgroundTaskManager、LocationShareModel 和 LocationTracker 文件添加到我的项目中。基本上,这个解决方案背后的想法是不断重启位置管理器,这样它就没有机会让后台任务过期并停止发送更新。但是,即使使用此解决方案,我也只能获得 3 分钟多一点的位置更新。
我启用了 "Location Updates" 和 "Use Bluetooth LE accessories" 后台模式。 "Background Fetch"(后台应用刷新)未启用,根据 Apple 的引述:"In iOS 8 and later, disabling the Background App Refresh setting for the current app or for all apps does not prevent the delivery of location events in the background." 我的应用请求 "Always" 位置更新授权。
尽管查阅了看似无穷无尽的 Whosebug 文章和教程,但我还是想不出如何解决这个问题。我正在 iPhone 5S 运行 iOS 8.3.0 上测试它。任何见解将不胜感激。请参阅下面的代码摘录。
在AppDelegate.m中:
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
if ([region isKindOfClass:[CLBeaconRegion class]]) {
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.alertBody = @"Start recording trip";
notification.soundName = @"Default";
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
self.recording = YES;
[self startAutoTrip];
}
}
- (void) startAutoTrip {
self.locationTracker = [[LocationTracker alloc]init];
[self.locationTracker startLocationTracking];
}
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
if ([region isKindOfClass:[CLBeaconRegion class]]) {
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.alertBody = @"Stop recording trip";
notification.soundName = @"Default";
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
[self stopAutoTrip];
self.recording = NO;
}
}
- (void)stopAutoTrip {
// stop recording the locations
CLSLog(@"Trying to stop location updates");
[self.locationTracker stopLocationTracking:self.managedObjectContext];
CLSLog(@"Stop location updates");
}
在 LocationTracker.m 中(从上面引用的教程中,将 60 秒和 10 秒的时间间隔更改为 5 秒和 2 秒)。基本上这些是 startLocationTracking、didUpdateLocations 和 stopLocationTracking 方法。
- (void)startLocationTracking {
NSLog(@"startLocationTracking");
if ([CLLocationManager locationServicesEnabled] == NO) {
NSLog(@"locationServicesEnabled false");
UIAlertView *servicesDisabledAlert = [[UIAlertView alloc] initWithTitle:@"Location Services Disabled" message:@"You currently have all location services for this device disabled" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[servicesDisabledAlert show];
} else {
CLAuthorizationStatus authorizationStatus= [CLLocationManager authorizationStatus];
if(authorizationStatus == kCLAuthorizationStatusDenied || authorizationStatus == kCLAuthorizationStatusRestricted){
NSLog(@"authorizationStatus failed");
} else {
NSLog(@"authorizationStatus authorized");
CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.distanceFilter = 10; //meters
locationManager.activityType = CLActivityTypeAutomotiveNavigation;
locationManager.pausesLocationUpdatesAutomatically = NO;
if(IS_OS_8_OR_LATER) {
[locationManager requestAlwaysAuthorization];
}
[locationManager startUpdatingLocation];
}
}
}
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations{
NSLog(@"locationManager didUpdateLocations");
for(int i=0;i<locations.count;i++){
CLLocation * newLocation = [locations objectAtIndex:i];
NSDate *eventDate = newLocation.timestamp;
NSTimeInterval howRecent = [eventDate timeIntervalSinceNow];
if (fabs(howRecent) < 10.0 && newLocation.horizontalAccuracy < 20 && locations.count > 0) {
CLLocationCoordinate2D theLocation = newLocation.coordinate;
CLLocationAccuracy theAccuracy = newLocation.horizontalAccuracy;
self.myLastLocation = theLocation;
self.myLastLocationAccuracy= theAccuracy;
CLLocationCoordinate2D coords[2];
coords[0] = ((CLLocation *)locations.lastObject).coordinate;
coords[1] = newLocation.coordinate;
[self.shareModel.myLocationArray addObject:newLocation];
}
}
//If the timer still valid, return it (Will not run the code below)
if (self.shareModel.timer) {
return;
}
self.shareModel.bgTask = [BackgroundTaskManager sharedBackgroundTaskManager];
[self.shareModel.bgTask beginNewBackgroundTask];
//Restart the locationMaanger after 1 minute (5 sec)
self.shareModel.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self
selector:@selector(restartLocationUpdates)
userInfo:nil
repeats:NO];
//Will only stop the locationManager after 10 seconds, so that we can get some accurate locations
//The location manager will only operate for 10 seconds to save battery
// 2 sec
if (self.shareModel.delay10Seconds) {
[self.shareModel.delay10Seconds invalidate];
self.shareModel.delay10Seconds = nil;
}
self.shareModel.delay10Seconds = [NSTimer scheduledTimerWithTimeInterval:2 target:self
selector:@selector(stopLocationDelayBy10Seconds)
userInfo:nil
repeats:NO];
}
- (void) restartLocationUpdates
{
NSLog(@"restartLocationUpdates");
if (self.shareModel.timer) {
[self.shareModel.timer invalidate];
self.shareModel.timer = nil;
}
CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.distanceFilter = 10; //meters
locationManager.activityType = CLActivityTypeAutomotiveNavigation;
locationManager.pausesLocationUpdatesAutomatically = NO;
if(IS_OS_8_OR_LATER) {
[locationManager requestAlwaysAuthorization];
}
[locationManager startUpdatingLocation];
}
- (void)stopLocationTracking:(NSManagedObjectContext *)managedObjectContext {
NSLog(@"stopLocationTracking");
CLSLog(@"stopLocationTracking");
CLSLog(@"set managedObjectContext %@", managedObjectContext);
self.managedObjectContext = managedObjectContext;
if (self.shareModel.timer) {
[self.shareModel.timer invalidate];
self.shareModel.timer = nil;
}
CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
[locationManager stopUpdatingLocation];
[self saveRun];
[self sendRun];
}
如果您在 plist 中设置位置背景模式,您可以无限期地测距信标并获取 GPS 位置更新。让它工作的关键是启动后台线程。
您可以在 this blog post 中看到关于如何在后台扩展信标范围的示例。虽然博客 post 提到这限制为 3 分钟,但当您将位置背景模式添加到您的 plist 时,该时间限制就会消失。
请注意,除非 Apple 认可您这样做的理由,否则您可能无法获得 AppStore 批准使用此后台模式。
因此在 iOS 中,位置更新将无限期地在后台运行,前提是 -
1. 您已经开始在前台进行位置更新并且
2. 您已在您的 plist 中添加背景位置。
在您的情况下,OS 在后台唤醒您,正如您所说的正确,在 OS 暂停您的应用程序之前,您只有 10 秒的执行时间。解决方法基本上是启动后台任务,因为您已经完成了额外的 180 秒执行时间(这个数字可以根据 OS 版本而改变)。
要深入了解您的问题,您需要知道只有某些事件(如 geofence/ibeacon/significant 位置更新)会在后台唤醒您的应用,我们称它们为 "wakeup" 事件.一旦这些事件中的任何一个发生,你有最多 180 秒的后台执行时间(使用后台任务),之后你的应用程序将被暂停,除非这些事件中的任何一个再次被触发,之后你需要重新启动你的后台任务。我不确定你的应用程序是如何工作的,但如果你能确保你在需要位置更新的持续时间内不断从 OS 获取这些 "wakeup" 事件,你几乎可以保持你的应用在后台唤醒。
补充一下,我看到很多博客文章声称保持计时器并使用该计时器定期重新启动位置更新是有效的,但我从未成功使用它。
感谢大家的回复。与 Øyvind Hauge 所说的相反,可以使用 iBeacons 将您的应用程序从 killed/suspended/terminated 唤醒。不幸的是,正如其他人所建议的那样,将后台位置模式添加到您的 plist 并不能启用无限期的位置更新;使用该方法我只能执行 3 分钟。
我实际上在这个 Whosebug article 中找到了我的问题的解决方案。解决方案是向您的应用委托添加几行代码——您需要启动另一个位置管理器来监视重要的位置更新。以下是我在将 anotherLocationManager
声明为 属性...
后添加到 AppDelegate.m 文件中的 didFinishLaunchingWithOptions
方法的代码行
self.anotherLocationManager = [[CLLocationManager alloc] init];
self.anotherLocationManager.delegate = self;
[self.anotherLocationManager startMonitoringSignificantLocationChanges];
我从来没有使用这个位置管理器做任何其他事情,我只是将它永久地 运行 留在后台,并且出于某种原因,这使得定期调用 [locationManager startUpdatingLocation]
可以无限期地更新位置。我不再让位置更新在 3 分钟后神秘停止。这就是解决方案,这看起来很奇怪,但实施起来非常简单,希望这能帮助其他正在处理同样问题的人。
我正在尝试制作一个可以由 iBeacon 触发唤醒(从 killed/suspended/terminated)以记录每秒 GPS 信息的应用程序。当 phone 超出信标范围时,GPS 记录应该停止。我已经成功地让我的应用程序在进入和超出 iBeacon 范围时识别 didEnterRegion 和 didExitRegion 方法。在 didEnterRegion 方法中,我基本上想说类似 [locationManager startUpdatingLocation]
的内容,以便我可以开始跟踪用户的位置。但是,当我尝试添加这行代码时,位置更新会在大约 10 秒后停止。
后来我找到了一个article about background location updates that came with this Github project。我将 BackgroundTaskManager、LocationShareModel 和 LocationTracker 文件添加到我的项目中。基本上,这个解决方案背后的想法是不断重启位置管理器,这样它就没有机会让后台任务过期并停止发送更新。但是,即使使用此解决方案,我也只能获得 3 分钟多一点的位置更新。
我启用了 "Location Updates" 和 "Use Bluetooth LE accessories" 后台模式。 "Background Fetch"(后台应用刷新)未启用,根据 Apple 的引述:"In iOS 8 and later, disabling the Background App Refresh setting for the current app or for all apps does not prevent the delivery of location events in the background." 我的应用请求 "Always" 位置更新授权。
尽管查阅了看似无穷无尽的 Whosebug 文章和教程,但我还是想不出如何解决这个问题。我正在 iPhone 5S 运行 iOS 8.3.0 上测试它。任何见解将不胜感激。请参阅下面的代码摘录。
在AppDelegate.m中:
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
if ([region isKindOfClass:[CLBeaconRegion class]]) {
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.alertBody = @"Start recording trip";
notification.soundName = @"Default";
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
self.recording = YES;
[self startAutoTrip];
}
}
- (void) startAutoTrip {
self.locationTracker = [[LocationTracker alloc]init];
[self.locationTracker startLocationTracking];
}
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
if ([region isKindOfClass:[CLBeaconRegion class]]) {
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.alertBody = @"Stop recording trip";
notification.soundName = @"Default";
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
[self stopAutoTrip];
self.recording = NO;
}
}
- (void)stopAutoTrip {
// stop recording the locations
CLSLog(@"Trying to stop location updates");
[self.locationTracker stopLocationTracking:self.managedObjectContext];
CLSLog(@"Stop location updates");
}
在 LocationTracker.m 中(从上面引用的教程中,将 60 秒和 10 秒的时间间隔更改为 5 秒和 2 秒)。基本上这些是 startLocationTracking、didUpdateLocations 和 stopLocationTracking 方法。
- (void)startLocationTracking {
NSLog(@"startLocationTracking");
if ([CLLocationManager locationServicesEnabled] == NO) {
NSLog(@"locationServicesEnabled false");
UIAlertView *servicesDisabledAlert = [[UIAlertView alloc] initWithTitle:@"Location Services Disabled" message:@"You currently have all location services for this device disabled" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[servicesDisabledAlert show];
} else {
CLAuthorizationStatus authorizationStatus= [CLLocationManager authorizationStatus];
if(authorizationStatus == kCLAuthorizationStatusDenied || authorizationStatus == kCLAuthorizationStatusRestricted){
NSLog(@"authorizationStatus failed");
} else {
NSLog(@"authorizationStatus authorized");
CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.distanceFilter = 10; //meters
locationManager.activityType = CLActivityTypeAutomotiveNavigation;
locationManager.pausesLocationUpdatesAutomatically = NO;
if(IS_OS_8_OR_LATER) {
[locationManager requestAlwaysAuthorization];
}
[locationManager startUpdatingLocation];
}
}
}
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations{
NSLog(@"locationManager didUpdateLocations");
for(int i=0;i<locations.count;i++){
CLLocation * newLocation = [locations objectAtIndex:i];
NSDate *eventDate = newLocation.timestamp;
NSTimeInterval howRecent = [eventDate timeIntervalSinceNow];
if (fabs(howRecent) < 10.0 && newLocation.horizontalAccuracy < 20 && locations.count > 0) {
CLLocationCoordinate2D theLocation = newLocation.coordinate;
CLLocationAccuracy theAccuracy = newLocation.horizontalAccuracy;
self.myLastLocation = theLocation;
self.myLastLocationAccuracy= theAccuracy;
CLLocationCoordinate2D coords[2];
coords[0] = ((CLLocation *)locations.lastObject).coordinate;
coords[1] = newLocation.coordinate;
[self.shareModel.myLocationArray addObject:newLocation];
}
}
//If the timer still valid, return it (Will not run the code below)
if (self.shareModel.timer) {
return;
}
self.shareModel.bgTask = [BackgroundTaskManager sharedBackgroundTaskManager];
[self.shareModel.bgTask beginNewBackgroundTask];
//Restart the locationMaanger after 1 minute (5 sec)
self.shareModel.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self
selector:@selector(restartLocationUpdates)
userInfo:nil
repeats:NO];
//Will only stop the locationManager after 10 seconds, so that we can get some accurate locations
//The location manager will only operate for 10 seconds to save battery
// 2 sec
if (self.shareModel.delay10Seconds) {
[self.shareModel.delay10Seconds invalidate];
self.shareModel.delay10Seconds = nil;
}
self.shareModel.delay10Seconds = [NSTimer scheduledTimerWithTimeInterval:2 target:self
selector:@selector(stopLocationDelayBy10Seconds)
userInfo:nil
repeats:NO];
}
- (void) restartLocationUpdates
{
NSLog(@"restartLocationUpdates");
if (self.shareModel.timer) {
[self.shareModel.timer invalidate];
self.shareModel.timer = nil;
}
CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.distanceFilter = 10; //meters
locationManager.activityType = CLActivityTypeAutomotiveNavigation;
locationManager.pausesLocationUpdatesAutomatically = NO;
if(IS_OS_8_OR_LATER) {
[locationManager requestAlwaysAuthorization];
}
[locationManager startUpdatingLocation];
}
- (void)stopLocationTracking:(NSManagedObjectContext *)managedObjectContext {
NSLog(@"stopLocationTracking");
CLSLog(@"stopLocationTracking");
CLSLog(@"set managedObjectContext %@", managedObjectContext);
self.managedObjectContext = managedObjectContext;
if (self.shareModel.timer) {
[self.shareModel.timer invalidate];
self.shareModel.timer = nil;
}
CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
[locationManager stopUpdatingLocation];
[self saveRun];
[self sendRun];
}
如果您在 plist 中设置位置背景模式,您可以无限期地测距信标并获取 GPS 位置更新。让它工作的关键是启动后台线程。
您可以在 this blog post 中看到关于如何在后台扩展信标范围的示例。虽然博客 post 提到这限制为 3 分钟,但当您将位置背景模式添加到您的 plist 时,该时间限制就会消失。
请注意,除非 Apple 认可您这样做的理由,否则您可能无法获得 AppStore 批准使用此后台模式。
因此在 iOS 中,位置更新将无限期地在后台运行,前提是 - 1. 您已经开始在前台进行位置更新并且 2. 您已在您的 plist 中添加背景位置。
在您的情况下,OS 在后台唤醒您,正如您所说的正确,在 OS 暂停您的应用程序之前,您只有 10 秒的执行时间。解决方法基本上是启动后台任务,因为您已经完成了额外的 180 秒执行时间(这个数字可以根据 OS 版本而改变)。
要深入了解您的问题,您需要知道只有某些事件(如 geofence/ibeacon/significant 位置更新)会在后台唤醒您的应用,我们称它们为 "wakeup" 事件.一旦这些事件中的任何一个发生,你有最多 180 秒的后台执行时间(使用后台任务),之后你的应用程序将被暂停,除非这些事件中的任何一个再次被触发,之后你需要重新启动你的后台任务。我不确定你的应用程序是如何工作的,但如果你能确保你在需要位置更新的持续时间内不断从 OS 获取这些 "wakeup" 事件,你几乎可以保持你的应用在后台唤醒。
补充一下,我看到很多博客文章声称保持计时器并使用该计时器定期重新启动位置更新是有效的,但我从未成功使用它。
感谢大家的回复。与 Øyvind Hauge 所说的相反,可以使用 iBeacons 将您的应用程序从 killed/suspended/terminated 唤醒。不幸的是,正如其他人所建议的那样,将后台位置模式添加到您的 plist 并不能启用无限期的位置更新;使用该方法我只能执行 3 分钟。
我实际上在这个 Whosebug article 中找到了我的问题的解决方案。解决方案是向您的应用委托添加几行代码——您需要启动另一个位置管理器来监视重要的位置更新。以下是我在将 anotherLocationManager
声明为 属性...
didFinishLaunchingWithOptions
方法的代码行
self.anotherLocationManager = [[CLLocationManager alloc] init];
self.anotherLocationManager.delegate = self;
[self.anotherLocationManager startMonitoringSignificantLocationChanges];
我从来没有使用这个位置管理器做任何其他事情,我只是将它永久地 运行 留在后台,并且出于某种原因,这使得定期调用 [locationManager startUpdatingLocation]
可以无限期地更新位置。我不再让位置更新在 3 分钟后神秘停止。这就是解决方案,这看起来很奇怪,但实施起来非常简单,希望这能帮助其他正在处理同样问题的人。