定期iOS后台位置更新

93

我正在编写一个需要高精度低频率后台位置更新的应用程序。解决方案似乎是启动位置管理器更新的后台NSTimer任务,然后立即关闭。这个问题以前已经被问过:

如何在iOS应用程序中每n分钟获取一次后台位置更新?

在应用程序进入后台后每隔n分钟获取用户位置

iOS不是典型的后台位置跟踪计时器问题

iOS具有“位置”后台模式的长时间运行后台计时器

基于位置跟踪的iOS全时间后台服务

但我还没有得到一个最小化示例工作。尝试了上面每个已接受答案的排列组合后,我编写了一个起点。进入后台:

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    self.bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        NSLog(@"ending background task");
        [[UIApplication sharedApplication] endBackgroundTask:self.bgTask];
        self.bgTask = UIBackgroundTaskInvalid;
    }];


    self.timer = [NSTimer scheduledTimerWithTimeInterval:60
                                                  target:self.locationManager
                                                selector:@selector(startUpdatingLocation)
                                                userInfo:nil
                                                 repeats:YES];
}

并且委托方法:

- (void)locationManager:(CLLocationManager *)manager 
    didUpdateToLocation:(CLLocation *)newLocation 
           fromLocation:(CLLocation *)oldLocation {

    NSLog(@"%@", newLocation);

    NSLog(@"background time: %f", [UIApplication sharedApplication].backgroundTimeRemaining);
    [self.locationManager stopUpdatingLocation];

}
当前的行为是,backgroundTimeRemaining在记录位置时从180秒递减到零,然后过期处理程序执行并且不再生成位置更新。如何修改上述代码以便在后台无限期地接收周期性位置更新更新: 我的目标是iOS 7,并且有一些证据表明后台任务的行为有所不同: 在后台任务中启动位置管理器(iOS 7)

你没有提到你的目标iOS版本。例如,iOS 7与其前身有不同的多任务处理系统。 - Jason Whitehorn
@JasonWhitehorn 好观点。我已经更新了问题。 - pcoving
2
@pcoving:即使应用程序被杀死或终止,您的解决方案是否仍然有效? - Nirav
9个回答

62

看起来是stopUpdatingLocation触发了后台看门狗计时器,所以我在didUpdateLocation中用以下代码替换了它:


看起来是stopUpdatingLocation触发了后台看门狗计时器,所以我在didUpdateLocation中用以下代码替换了它:
[self.locationManager setDesiredAccuracy:kCLLocationAccuracyThreeKilometers];
[self.locationManager setDistanceFilter:99999];

它似乎可以有效地关闭GPS电源。然后,背景NSTimer的选择器变为:

- (void) changeAccuracy {
    [self.locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
    [self.locationManager setDistanceFilter:kCLDistanceFilterNone];
}

我所做的就是定期切换准确度,以获取每几分钟一次的高精度坐标,并且由于locationManager没有停止,所以backgroundTimeRemaining保持在最大值。这将我的设备上的电池消耗从常量后台kCLLocationAccuracyBest每小时约10%降低到每小时约2%。


4
请问您能否更新您的问题并提供一个完整的可行示例?例如:“更新:我已经解决了,这是我的解决方案...”请让内容更加通俗易懂,但不要改变原来的意思。 - smholloway
这种方法不会按照你的期望工作: 1)增加distanceFilter属性的值并不能节省电池,因为GPS仍然需要不断计算你的位置。 2)在iOS 7中,后台时间是不连续的。 3)你可以考虑使用significantChangeLocation服务。 4)除了在iOS 7中采用你的解决方案外,你无法从后台启动UIBackgroundModes - location... - aMother
2
@aMother 1) 我意识到增加距离过滤器(并不处理回调)并不能节省多少电池。但是,setDesiredAccuracy可以!它会退而求其次使用基本免费的基站信息。2) 不连续的?3) 我已经用粗体指定了我需要高精度。重大位置更改无法提供此功能。4) 定位服务未从后台启动/停止。 - pcoving
@pcoving 你尝试过调用stopUpdatingLocation,然后调用startMonitoringSignificantLocationChanges而不是更改所需的精度吗?当你需要更精确的位置时,调用stopMonitoringSignificantLocationchanges,然后再调用startUpdatingLocation。如果可行的话,这样做可以节省更多电池电量。 - François Marceau
2
我知道这是一个旧帖子了,但是使用这个技巧的我的应用程序用户自从升级到iOS9以来已经注意到电池使用量显著增加。我还没有进行调查,但我有一种感觉,这个“技巧”可能不再起作用了? - Gary Wright
显示剩余7条评论

46

我编写了一个使用位置服务的应用程序,该应用每10秒必须发送一次位置信息。它运行得非常好。

只需按照苹果文档中的 "allowDeferredLocationUpdatesUntilTraveled:timeout" 方法即可。

步骤如下:

必要条件:注册后台模式以更新位置信息。

1. 创建 LocationManger 并使用所需的精度和过滤距离调用 startUpdatingLocation:

-(void) initLocationManager    
{
    // Create the manager object
    self.locationManager = [[[CLLocationManager alloc] init] autorelease];
    _locationManager.delegate = self;
    // This is the most important property to set for the manager. It ultimately determines how the manager will
    // attempt to acquire location and thus, the amount of power that will be consumed.
    _locationManager.desiredAccuracy = 45;
    _locationManager.distanceFilter = 100;
    // Once configured, the location manager must be "started".
    [_locationManager startUpdatingLocation];
}

2.为了在后台中使用"allowDeferredLocationUpdatesUntilTraveled:timeout"方法使应用程序一直运行,当应用程序进入后台时,您必须使用新的参数重新启动updatingLocation,例如:

- (void)applicationWillResignActive:(UIApplication *)application {
     _isBackgroundMode = YES;

    [_locationManager stopUpdatingLocation];
    [_locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
    [_locationManager setDistanceFilter:kCLDistanceFilterNone];
    _locationManager.pausesLocationUpdatesAutomatically = NO;
    _locationManager.activityType = CLActivityTypeAutomotiveNavigation;
    [_locationManager startUpdatingLocation];
 }

3. 应用程序通过 "locationManager:didUpdateLocations:" 回调正常更新位置信息:

-(void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
//  store data
    CLLocation *newLocation = [locations lastObject];
    self.userLocation = newLocation;

   //tell the centralManager that you want to deferred this updatedLocation
    if (_isBackgroundMode && !_deferringUpdates)
    {
        _deferringUpdates = YES;
        [self.locationManager allowDeferredLocationUpdatesUntilTraveled:CLLocationDistanceMax timeout:10];
    }
}

4. 但是你应该在“locationManager:didFinishDeferredUpdatesWithError:”回调函数中处理数据以实现你的目的。

- (void) locationManager:(CLLocationManager *)manager didFinishDeferredUpdatesWithError:(NSError *)error {

     _deferringUpdates = NO;

     //do something 
}

5. 注意: 我认为每次应用在后台/前台模式之间切换时,我们都应该重置LocationManager的参数。


这很好用,我确实觉得这是去年WWDC中提到的正确方法!感谢您的帖子。 - prasad1250
@samthui7,请查看此问题...http://stackoverflow.com/questions/37338466/sending-current-location-to-server-in-background-as-well-as-in-running-app-using - Suraj Sukale
2
@amar:我已经在iOS 10 beta上进行了检查,它仍然可以工作。请注意,在startUpdatingLocation之前我们需要添加一些代码:allowsBackgroundLocationUpdates = YESrequestAlwaysAuthorizationrequestWhenInUseAuthorization。例如:CLLocationManager *locationManager = [[CLLocationManager alloc] init]; locationManager.delegate = self; locationManager.allowsBackgroundLocationUpdates = YES; [locationManager requestAlwaysAuthorization]; [locationManager startUpdatingLocation]; - samthui7
1
@Nirav 不,我认为苹果不会让我们这样做,据我所知。 - samthui7
@samthui7 不,我们可以做到这一点,搜索一个名为“zenly”的应用程序,即使您的朋友没有打开“zenly”应用程序,它也可以跟踪您的朋友位置。 - Nirav
显示剩余10条评论

39
  1. 如果您的 plist 文件中有 location 键的 UIBackgroundModes,则不需要使用 beginBackgroundTaskWithExpirationHandler 方法。这是多余的。此外,您正在错误地使用它(请参见这里),但由于您的 plist 已经设置好了,所以这是无关紧要的。

  2. 在 plist 中拥有 UIBackgroundModes location 后,只要 CLLocationManger 运行,应用程序将无限期在后台运行。如果您在后台调用 stopUpdatingLocation,则应用程序将停止,并且不会重新启动。

    也许在调用 stopUpdatingLocation 前可以调用 beginBackgroundTaskWithExpirationHandler,然后在调用 startUpdatingLocation 后调用 endBackgroundTask 以保持它在 GPS 停止时处于后台运行状态,但我从未尝试过这种方法 - 这只是一个想法。

    另一种选择(我也没有尝试过)是在后台保持位置管理器运行,但是一旦您获得准确的位置,就将 desiredAccuracy 属性更改为 1000m 或更高,以关闭 GPS 芯片(以节省电池电量)。然后,10 分钟后当您需要另一个位置更新时,将 desiredAccuracy 再次更改为 100m,以开启 GPS 直到您获得准确的位置。

  3. 在位置管理器上调用 startUpdatingLocation 时,必须给它时间来获取位置。不应立即调用 stopUpdatingLocation。我们最多运行 10 秒钟或直到获得非缓存的高精度位置。

  • 您需要过滤掉缓存位置,并检查所获取的位置精度是否符合您的最低要求(请参见这里)。您所接收到的第一个更新可能是10分钟或10天前的,而您所接收到的第一次精度可能为3000米。

  • 考虑使用重大位置变化API。一旦您收到重大位置变化通知,您可以启动CLLocationManager几秒钟以获取高精度位置。我不确定,因为我从没用过重大位置变化服务。


  • 苹果CoreLocation参考似乎建议在后台处理位置数据时使用beginBackgroundTaskWithExpirationHandler: "如果iOS应用程序需要更多时间来处理位置数据,则可以使用UIApplication类的beginBackgroundTaskWithName:expirationHandler:方法请求更多后台执行时间。" - https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/LocationAwarenessPG/CoreLocation/CoreLocation.html - eliocs
    4
    不要忘记在iOS9+中添加_locationManager.allowsBackgroundLocationUpdates = YES;,否则应用程序将在180秒后进入睡眠状态。 - kolyancz
    @kolyancz 我刚在iPhone 6+上的iOS 10.1.1上尝试了一下。它大约需要 ~17分钟才能进入睡眠状态并触发locationManagerDidPauseLocationUpdates。我验证了这种行为10次! - mfaani
    我真的不明白你为什么说这是无用的,然后又说你应该将它包装在backgroundTask中... - mfaani

    10

    当需要启动位置服务并停止后台任务时,应该延迟一段时间停止后台任务(1秒足够)。否则位置服务将无法启动。此外,位置服务应该保持开启状态几秒钟(例如3秒)。

    有一个 CocoaPods 插件 APScheduledLocationManager 可以按照所需的位置精度每隔 n 秒获得后台位置更新。

    let manager = APScheduledLocationManager(delegate: self)
    manager.startUpdatingLocation(interval: 170, acceptableLocationAccuracy: 100)
    

    这个代码库还包括一个用 Swift 3 编写的示例应用程序。


    1

    您需要在应用程序中添加位置更新模式,方法是在info.plist中添加以下键。

    <key>UIBackgroundModes</key>
    <array>
        <string>location</string>
    </array>
    

    didUpdateToLocation 方法将被调用(即使您的应用程序在后台运行)。 您可以根据此方法调用执行任何操作。


    位置更新模式已经在info.plist中。正如我所提到的,我会在后台超时180秒之前收到更新。 - pcoving

    1
    试试使用startMonitoringSignificantLocationChanges: API,它的触发频率较低,准确度也相当不错。此外,它比其他locationManager API具有更多优势。
    关于这个API已经在链接上进行了详细讨论。

    1
    我已尝试过这种方法,但它违反了精度要求 - 来自文档该接口只在检测到设备关联的基站发生更改时提供新事件 - pcoving

    0

    要在应用程序在后台运行时使用标准位置服务,您需要首先在目标设置的“能力”选项卡中打开背景模式,并选择位置更新。

    或者,直接将其添加到Info.plist中。

    <key>NSLocationAlwaysUsageDescription</key>
     <string>I want to get your location Information in background</string>
    <key>UIBackgroundModes</key>
     <array><string>location</string> </array>
    

    然后您需要设置CLLocationManager

    Objective C

    //The Location Manager must have a strong reference to it.
    _locationManager = [[CLLocationManager alloc] init];
    _locationManager.delegate = self;
    //Request Always authorization (iOS8+)
    if ([_locationManager respondsToSelector:@selector(requestAlwaysAuthorization)]) { [_locationManager requestAlwaysAuthorization];
    }
    //Allow location updates in the background (iOS9+)
    if ([_locationManager respondsToSelector:@selector(allowsBackgroundLocationUpdates)]) { _locationManager.allowsBackgroundLocationUpdates = YES;
    }
    [_locationManager startUpdatingLocation];
    

    Swift

    self.locationManager.delegate = self
    if #available (iOS 8.0,*) {
        self.locationManager.requestAlwaysAuthorization()
    }
    if #available (iOS 9.0,*) {
        self.locationManager.allowsBackgroundLocationUpdates = true
    }
    self.locationManager.startUpdatingLocation()
    
    

    Ref:https://medium.com/@javedmultani16/location-service-in-the-background-ios-942c89cd99ba


    0

    iOS 8之后,苹果公司对CoreLocation框架中的后台获取和更新进行了几项更改。

    1)苹果公司引入了AlwaysAuthorization请求,以在应用程序中获取位置更新。

    2)苹果公司引入了backgroundLocationupdates,以在iOS中从后台获取位置。

    要在后台获取位置,您需要在Xcode的功能中启用位置更新。

    //
    //  ViewController.swift
    //  DemoStackLocation
    //
    //  Created by iOS Test User on 02/01/18.
    //  Copyright © 2018 iOS Test User. All rights reserved.
    //
    
    import UIKit
    import CoreLocation
    
    class ViewController: UIViewController {
    
        var locationManager: CLLocationManager = CLLocationManager()
        override func viewDidLoad() {
            super.viewDidLoad()
            startLocationManager()
        }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
            // Dispose of any resources that can be recreated.
        }
    
        internal func startLocationManager(){
            locationManager.requestAlwaysAuthorization()
            locationManager.desiredAccuracy = kCLLocationAccuracyBest
            locationManager.delegate = self
            locationManager.allowsBackgroundLocationUpdates = true
            locationManager.startUpdatingLocation()
        }
    
    }
    
    extension ViewController : CLLocationManagerDelegate {
    
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]){
            let location = locations[0] as CLLocation
            print(location.coordinate.latitude) // prints user's latitude
            print(location.coordinate.longitude) //will print user's longitude
            print(location.speed) //will print user's speed
        }
    
    }
    

    -3
    locationManager.allowsBackgroundLocationUpdates = true
    

    4
    你能否编辑你的回答并添加一些解释,说明为什么/如何解决这个问题? - barbsan

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接