由 iBeacon 触发的 iOS 8 后台位置更新

Posted

技术标签:

【中文标题】由 iBeacon 触发的 iOS 8 后台位置更新【英文标题】:iOS 8 Background Location Update triggered by iBeacon 【发布时间】:2015-08-06 20:31:21 【问题描述】:

我正在尝试制作一个可以由 iBeacon 触发以唤醒(从被杀死/暂停/终止)以记录每秒 GPS 信息的应用。当手机超出信标范围时,GPS 记录应停止。当它进入和超出 iBeacon 的范围时,我已经成功地让我的应用程序识别 didEnterRegion 和 didExitRegion 方法。在 didEnterRegion 方法中,我基本上想说[locationManager startUpdatingLocation] 之类的东西,这样我就可以开始跟踪用户的位置了。但是,当我尝试添加这行代码时,位置更新会在大约 10 秒后停止。

后来我发现了一个article,关于这个 Github project 附带的后台位置更新。我将 BackgroundTaskManager、LocationShareModel 和 LocationTracker 文件添加到我的项目中。基本上,这个解决方案背后的想法是不断地重新启动位置管理器,这样它就没有机会让后台任务过期并停止发送更新。但是,即使使用此解决方案,我也只能在 3 分钟多一点的时间内获得位置更新。

我启用了“位置更新”和“使用蓝牙 LE 配件”后台模式。没有启用“Background Fetch”(后台应用程序刷新),根据 Apple 的这句话:“在 ios 8 及更高版本中,禁用当前应用程序或所有应用程序的后台应用程序刷新设置不会阻止位置的传递背景中的事件。”我的应用请求“始终”授权以更新位置。

尽管查看了看似无穷无尽的 *** 文章和教程,但我无法弄清楚如何解决这个问题。我正在运行 iOS 8.3.0 的 iPhone 5S 上对其进行测试。任何见解将不胜感激。请参阅下面的代码摘录。

在 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];

【问题讨论】:

“我正在尝试制作一个可以由 iBeacon 触发唤醒的应用程序(从被杀死/暂停/终止)” 遗憾的是,这在IOS。应用程序必须在前台/后台运行才能与 ibeacons 通信。 【参考方案1】:

感谢大家的回复。与Øyvind Hauge 所说的相反,可以使用 iBeacons 将您的应用从被杀死/暂停/终止中唤醒。不幸的是,像其他人建议的那样,将后台位置模式添加到您的 plist 不会启用无限期的位置更新;使用该方法我只能执行 3 分钟。

我实际上在 *** article 中找到了我的问题的解决方案。解决方案是向您的应用程序委托添加几行代码 - 您需要启动另一个位置管理器来监视重要的位置更新。以下是我在将 anotherLocationManager 声明为属性后添加到 AppDelegate.m 文件中的 didFinishLaunchingWithOptions 方法的代码行...

self.anotherLocationManager = [[CLLocationManager alloc] init];
self.anotherLocationManager.delegate = self;
[self.anotherLocationManager startMonitoringSignificantLocationChanges];

我从来没有使用这个位置管理器做任何其他事情,我只是让它在后台永久运行,并且出于某种原因,这可以从定期调用 [locationManager startUpdatingLocation] 进行无限期的位置更新。我不再让位置更新在 3 分钟后神秘停止。这是解决方案似乎很奇怪,但实现起来非常简单,希望这可以帮助其他正在处理同样问题的人。

【讨论】:

哇,即使在应用程序因 iBeacon 更新而暂停并重新启动后,这仍然有效吗?启动位置更新会不会让应用无限期地继续运行? 这在应用程序被暂停甚至被用户杀死后才起作用。 iBeacon 触发应用程序重新启动,然后我启动位置管理器。正如我在问题中提到的,我有一个后台任务管理器,它会在计时器上不断重启位置管理器,以使应用程序无限期运行。因此,大规模概述是 iBeacon 监控,它启动了一个位置管理器(它本身由后台任务管理器管理) - 但是如果没有监控重大位置变化,这种设置就无法工作,正如我的回答中所见。【参考方案2】:

如果您在 plist 中设置位置背景模式,您可以无限期地定位信标并获取 GPS 位置更新。让它工作的关键是启动一个后台线程。

您可以在this blog post 中查看如何在背景上扩展信标范围的示例。虽然博客文章提到这限制为 3 分钟,但当您将位置背景模式添加到您的 plist 时,该时间限制就会消失。

请理解,除非 Apple 认可您这样做的理由,否则您可能无法获得 AppStore 的批准使用此后台模式。

【讨论】:

这也是我的理解,但从 Sumant 的回答看来,您实际上需要在前台启动位置更新才能让它们无限期地运行,即使启用了位置后台模式也是如此。 Carielle 的案例似乎证实了这一点。有趣!【参考方案3】:

因此,在 iOS 中,位置更新仅在以下情况下才会无限期地在后台运行: 1.您已在前台开始位置更新并且 2. 您在 plist 中添加了背景位置。

在您的情况下,操作系统正在后台唤醒您,正如您所说的那样,在操作系统暂停您的应用程序之前,您只有 10 秒的执行时间。解决此问题的方法基本上是启动后台任务,因为您已经这样做以获得额外的 180 秒执行时间(此数字可能会根据操作系统版本而变化)。

要深入了解您的问题,您需要知道只有某些事件(如地理围栏/ibeacon/重要位置更新)会在后台唤醒您的应用,我们将它们称为“唤醒”事件。一旦发生这些事件中的任何一个,您最多有 180 秒的后台执行时间(使用后台任务),之后您的应用程序将被挂起,除非再次触发这些事件中的任何一个,否则您需要重新启动后台任务。我不确定您的应用程序是如何工作的,但如果您可以确保在需要位置更新的持续时间内不断从操作系统获取这些“唤醒”事件,您几乎可以让您的应用程序在后台保持唤醒状态。

补充一点,我看到很多博客文章声称保持计时器并使用该计时器定期重新启动位置更新是可行的,但我一直无法成功使用它。

【讨论】:

以上是关于由 iBeacon 触发的 iOS 8 后台位置更新的主要内容,如果未能解决你的问题,请参考以下文章

在后台使用 iBeacon 或 CoreBluetooth 识别 iOS 设备

iBeacons 在 iOS 中以后台模式扫描?

后台 iBeacon 监控

Cordova:在后台模式下扫描 iBeacons / BLE(iOS 和 Android)

iOS 8 后台应用程序位置

当两个应用监控同一个 iBeacon 区域时会发生啥?