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

wja*_*ans 202 objective-c background-process core-location ios

我正在寻找一种在iOS应用程序中每n分钟更新一次后台位置的方法.我正在使用iOS 4.3,该解决方案适用于非越狱的iPhone.

我尝试/考虑了以下选项:

  • CLLocationManager startUpdatingLocation/startMonitoringSignificantLocationChanges:这根据预期在后台工作,基于配置的属性,但似乎不可能强制它每n分钟更新一次位置
  • NSTimer:当应用程序在前台运行但似乎不是为后台任务设计时,是否有效
  • 本地通知:本地通知可以每n分钟安排一次,但是无法执行某些代码来获取当前位置(用户无需通过通知启动应用程序).这种方法似乎也不是一种干净的方法,因为这不是应该使用的通知.
  • UIApplication:beginBackgroundTaskWithExpirationHandler:据我所知,当应用程序移动到后台而不是实现"长时间运行"的后台进程时,这应该用于在后台完成一些工作(也限制时间).

如何实施这些常规后台位置更新?

wja*_*ans 113

我找到了一个在Apple Developer Forums的帮助下实现这个的解决方案:

  • 指定 location background mode
  • NSTimer在后台创建一个UIApplication:beginBackgroundTaskWithExpirationHandler:
  • 如果n小的UIApplication:backgroundTimeRemaining它会工作得很好.如果n较大的,则location manager应该启用(和残疾人)前再次有剩余,避免后台任务被杀死没有时间.

这是有效的,因为location是三种允许的后台执行类型之一.

注意:我在模拟器中测试它时失去了一些时间.但是,它在我的手机上工作正常.

  • 你碰巧有论坛的链接吗?我正在寻求实现相同类型的位置映射,并且无法使其工作.或者非常感谢一些示例代码. (24认同)
  • 对于那些有兴趣看到一些反映此处讨论内容的实际代码的人,请查看http://stackoverflow.com/questions/10235203/getting-user-location-every-n-minutes-after-app-goes-to-background (12认同)
  • @all:是的,我们的应用程序可以在AppStore中找到.我不会发布所有代码,所有提到的要点都是明确记录的功能.如果您遇到特定问题,请发布您自己的问题,解释您尝试过的内容和出了什么问题. (5认同)
  • 如果你只是停下来并启动位置管理员,你可以解释为什么10分钟后(最长允许时间)后台任务没有被杀死?它是某种预期的功能吗?如果发生这种情况,它听起来更像Apple SDK中的错误.您正在尝试使用哪个iOS版本? (4认同)
  • @ user836026:是的,这就是我指定背景模式的意思.停止位置更新后,应在10分钟内再次启动,以避免应用程序被终止. (3认同)
  • @ user836026:如果您没有停止位置管理器,您的应用将不会被终止.这是按设计工作的.在我的场景中,我停止了位置管理器. (2认同)
  • 在我的测试中没有这样的差异.我做了一个测试比较:我的代码与此方法,mylocus(在appStore上)和"always On"方法. (2认同)
  • 这个方法在iOS 7上存在一些问题,请看http://stackoverflow.com/questions/18901583/start-location-manager-in-ios-7-from-background-task (2认同)

Les*_*ary 54

iOS 8/9/10上,每5分钟更新一次背景位置,请执行以下操作:

  1. 转到项目 - >功能 - >背景模式 - >选择位置更新

  2. 转到项目 - >信息 - >使用空值(或可选的任何文本)添加密钥NSLocationAlwaysUsageDescription

  3. 为了使您的应用程序在后台运行时位置正常工作,并将坐标发送到Web服务或每隔5分钟对它们执行任何操作,就像在下面的代码中一样.

我没有使用任何后台任务或计时器.我已经使用iOS 8.1设备测试了这段代码,我的应用程序在后台运行时,它躺在我的桌面上几个小时.设备已锁定,代码始终正常运行.

@interface LocationManager () <CLLocationManagerDelegate>
@property (strong, nonatomic) CLLocationManager *locationManager;
@property (strong, nonatomic) NSDate *lastTimestamp;

@end

@implementation LocationManager

+ (instancetype)sharedInstance
{
    static id sharedInstance = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
        LocationManager *instance = sharedInstance;
        instance.locationManager = [CLLocationManager new];
        instance.locationManager.delegate = instance;
        instance.locationManager.desiredAccuracy = kCLLocationAccuracyBest; // you can use kCLLocationAccuracyHundredMeters to get better battery life
        instance.locationManager.pausesLocationUpdatesAutomatically = NO; // this is important
    });

    return sharedInstance;
}

- (void)startUpdatingLocation
{
    CLAuthorizationStatus status = [CLLocationManager authorizationStatus];

    if (status == kCLAuthorizationStatusDenied)
    {
        NSLog(@"Location services are disabled in settings.");
    }
    else
    {
        // for iOS 8
        if ([self.locationManager respondsToSelector:@selector(requestAlwaysAuthorization)])
        {
            [self.locationManager requestAlwaysAuthorization];
        }
        // for iOS 9
        if ([self.locationManager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)])
        {
            [self.locationManager setAllowsBackgroundLocationUpdates:YES];
        }

        [self.locationManager startUpdatingLocation];
    }
}

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
    CLLocation *mostRecentLocation = locations.lastObject;
    NSLog(@"Current location: %@ %@", @(mostRecentLocation.coordinate.latitude), @(mostRecentLocation.coordinate.longitude));

    NSDate *now = [NSDate date];
    NSTimeInterval interval = self.lastTimestamp ? [now timeIntervalSinceDate:self.lastTimestamp] : 0;

    if (!self.lastTimestamp || interval >= 5 * 60)
    {
        self.lastTimestamp = now;
        NSLog(@"Sending current location to web service.");
    }
}

@end
Run Code Online (Sandbox Code Playgroud)

  • 这可以工作超过几个小时吗?即使应用程序未终止,设备似乎也会在应用程序转到后台一小时后停止推送位置更新. (3认同)
  • 背景提取在我的项目中被禁用(如果它关闭或打开则无关紧要).但是,必须将pausesLocationUpdatesAutomatically设置为NO才能使上述示例正常工作.如果先前由系统暂停,则一旦再次开始移动,它将不会自动恢复.这就是为什么在上面的例子中我将此属性设置为NO. (2认同)
  • @LeszekS你需要添加以下代码以支持iOS 9 for background - `if([self.locationManager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates :)]){[self.locationManager setAllowsBackgroundLocationUpdates:YES]; }` (2认同)
  • 这样做有什么方便?您仍然会因为始终保持高精度而耗尽电池电量。你唯一不做的事情是你实际上没有使用 ** 已经收到** 位置,直到你达到 5 分钟的间隔...... (2认同)

Bus*_*hid 35

我在我正在开发的应用程序中这样做了.当应用程序在后台但应用程序不断收到位置更新时,计时器不起作用.我在文档中的某处读过(我现在似乎无法找到它,当我这样做时会发布更新),当应用程序在后台时,只能在活动的运行循环上调用方法.即使在bg中,app委托也有一个活跃的运行循环,所以你不需要创建自己的循环来使这个工作.[我不确定这是否是正确的解释,但这就是我从我所读到的内容中理解的]

首先,在应用程序的info.plist中添加location密钥对象UIBackgroundModes.现在,您需要做的是在应用中的任何位置启动位置更新:

    CLLocationManager locationManager = [[CLLocationManager alloc] init];
    locationManager.delegate = self;//or whatever class you have for managing location
    [locationManager startUpdatingLocation];
Run Code Online (Sandbox Code Playgroud)

接下来,编写一个方法来处理位置更新,例如-(void)didUpdateToLocation:(CLLocation*)location,在应用程序委托中.然后实现方法locationManager:didUpdateLocation:fromLocationCLLocationManagerDelegate在其中您开始位置管理类(因为我们设置的位置经理授人以"自我").在此方法中,您需要检查是否已经过了必须处理位置更新的时间间隔.您可以通过每次保存当前时间来完成此操作.如果该时间已过,请从您的app delegate调用方法UpdateLocation:

NSDate *newLocationTimestamp = newLocation.timestamp;
NSDate *lastLocationUpdateTiemstamp;

int locationUpdateInterval = 300;//5 mins

NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
if (userDefaults) {

        lastLocationUpdateTiemstamp = [userDefaults objectForKey:kLastLocationUpdateTimestamp];

        if (!([newLocationTimestamp timeIntervalSinceDate:lastLocationUpdateTiemstamp] < locationUpdateInterval)) {
            //NSLog(@"New Location: %@", newLocation);
            [(AppDelegate*)[UIApplication sharedApplication].delegate didUpdateToLocation:newLocation];
            [userDefaults setObject:newLocationTimestamp forKey:kLastLocationUpdateTimestamp];
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

即使您的应用处于后台,这也会每隔5分钟调用您的方法.Imp:如果你应该使用你的位置数据的准确性并不重要,这个实现会耗尽电池电量[locationManager startMonitoringSignificantLocationChanges]

在将此添加到您的应用程序之前,请阅读位置感知编程指南

  • 您可以将位置管理器精度设置为1km - 这将使您的电池几乎完好无损.5分钟后,您将精度设置为1米.当您获得满意的位置(通常在5秒后),只需将精度设置回1km. (3认同)
  • 这样定位服务不断启用(实际上是电池耗尽),我不希望这样.我想每隔n分钟启用一次位置服务,并在我有一个很好的修复时立即禁用它(只是注意到我没有在我的问题中明确解释).我可以在我描述的解决方案中实现此行为. (2认同)

Ale*_*ngo 24

现在,iOS6已经成为永久运行位置服务的最佳方式......

- (void)applicationWillResignActive:(UIApplication *)application
{
/*
 Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
 Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
 */

NSLog(@"to background");

app.isInBackground = TRUE;

UIApplication *app = [UIApplication sharedApplication];

// Request permission to run in the background. Provide an
// expiration handler in case the task runs long.
NSAssert(bgTask == UIBackgroundTaskInvalid, nil);

bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
    // Synchronize the cleanup call on the main thread in case
    // the task actually finishes at around the same time.
    dispatch_async(dispatch_get_main_queue(), ^{

        if (bgTask != UIBackgroundTaskInvalid)
        {
            [app endBackgroundTask:bgTask];
            bgTask = UIBackgroundTaskInvalid;
        }
    });
}];

// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    // Do the work associated with the task.

    locationManager.distanceFilter = 100;
    locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
    [locationManager startMonitoringSignificantLocationChanges];
    [locationManager startUpdatingLocation];

    NSLog(@"App staus: applicationDidEnterBackground");
    // Synchronize the cleanup call on the main thread in case
    // the expiration handler is fired at the same time.
    dispatch_async(dispatch_get_main_queue(), ^{
        if (bgTask != UIBackgroundTaskInvalid)
        {
            [app endBackgroundTask:bgTask];
            bgTask = UIBackgroundTaskInvalid;
        }
    });
});

NSLog(@"backgroundTimeRemaining: %.0f", [[UIApplication sharedApplication] backgroundTimeRemaining]);

}
Run Code Online (Sandbox Code Playgroud)

刚刚测试过它:

我启动了应用程序,去了背景并在车上移动了几分钟.然后我回家1小时再开始移动(不再打开应用程序).地点再次开始.然后停了两个小时又重新开始了.一切都还好......

请勿忘记使用iOS6中的新位置服务

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{   
    CLLocation *loc = [locations lastObject];

    // Lat/Lon
    float latitudeMe = loc.coordinate.latitude;
    float longitudeMe = loc.coordinate.longitude;
}
Run Code Online (Sandbox Code Playgroud)


Hel*_*miB 13

对于有噩梦的人来说,找出这个.我有一个简单的解决方案.

  1. raywenderlich.com看这个例子- >有示例代码,这很好用,但遗憾的是在后台定位期间没有计时器.这将无限期地运行.
  2. 使用以下方法添加计时

    -(void)applicationDidEnterBackground {
    [self.locationManager stopUpdatingLocation];
    
    UIApplication*    app = [UIApplication sharedApplication];
    
    bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
    
     self.timer = [NSTimer scheduledTimerWithTimeInterval:intervalBackgroundUpdate
                                                  target:self.locationManager
                                                selector:@selector(startUpdatingLocation)
                                                userInfo:nil
                                                 repeats:YES];
    
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 只是不要忘记在info.plist中添加"应用寄存器以进行位置更新".


hmi*_*kov 7

这是我使用的:

import Foundation
import CoreLocation
import UIKit

class BackgroundLocationManager :NSObject, CLLocationManagerDelegate {

    static let instance = BackgroundLocationManager()
    static let BACKGROUND_TIMER = 150.0 // restart location manager every 150 seconds
    static let UPDATE_SERVER_INTERVAL = 60 * 60 // 1 hour - once every 1 hour send location to server

    let locationManager = CLLocationManager()
    var timer:NSTimer?
    var currentBgTaskId : UIBackgroundTaskIdentifier?
    var lastLocationDate : NSDate = NSDate()

    private override init(){
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
        locationManager.activityType = .Other;
        locationManager.distanceFilter = kCLDistanceFilterNone;
        if #available(iOS 9, *){
            locationManager.allowsBackgroundLocationUpdates = true
        }

        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.applicationEnterBackground), name: UIApplicationDidEnterBackgroundNotification, object: nil)
    }

    func applicationEnterBackground(){
        FileLogger.log("applicationEnterBackground")
        start()
    }

    func start(){
        if(CLLocationManager.authorizationStatus() == CLAuthorizationStatus.AuthorizedAlways){
            if #available(iOS 9, *){
                locationManager.requestLocation()
            } else {
                locationManager.startUpdatingLocation()
            }
        } else {
                locationManager.requestAlwaysAuthorization()
        }
    }
    func restart (){
        timer?.invalidate()
        timer = nil
        start()
    }

    func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
        switch status {
        case CLAuthorizationStatus.Restricted:
            //log("Restricted Access to location")
        case CLAuthorizationStatus.Denied:
            //log("User denied access to location")
        case CLAuthorizationStatus.NotDetermined:
            //log("Status not determined")
        default:
            //log("startUpdatintLocation")
            if #available(iOS 9, *){
                locationManager.requestLocation()
            } else {
                locationManager.startUpdatingLocation()
            }
        }
    }
    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

        if(timer==nil){
            // The locations array is sorted in chronologically ascending order, so the
            // last element is the most recent
            guard let location = locations.last else {return}

            beginNewBackgroundTask()
            locationManager.stopUpdatingLocation()
            let now = NSDate()
            if(isItTime(now)){
                //TODO: Every n minutes do whatever you want with the new location. Like for example sendLocationToServer(location, now:now)
            }
        }
    }

    func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
        CrashReporter.recordError(error)

        beginNewBackgroundTask()
        locationManager.stopUpdatingLocation()
    }

    func isItTime(now:NSDate) -> Bool {
        let timePast = now.timeIntervalSinceDate(lastLocationDate)
        let intervalExceeded = Int(timePast) > BackgroundLocationManager.UPDATE_SERVER_INTERVAL
        return intervalExceeded;
    }

    func sendLocationToServer(location:CLLocation, now:NSDate){
        //TODO
    }

    func beginNewBackgroundTask(){
        var previousTaskId = currentBgTaskId;
        currentBgTaskId = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({
            FileLogger.log("task expired: ")
        })
        if let taskId = previousTaskId{
            UIApplication.sharedApplication().endBackgroundTask(taskId)
            previousTaskId = UIBackgroundTaskInvalid
        }

        timer = NSTimer.scheduledTimerWithTimeInterval(BackgroundLocationManager.BACKGROUND_TIMER, target: self, selector: #selector(self.restart),userInfo: nil, repeats: false)
    }
}
Run Code Online (Sandbox Code Playgroud)

我在AppDelegate中开始跟踪:

BackgroundLocationManager.instance.start()
Run Code Online (Sandbox Code Playgroud)


Cha*_*bot 6

不幸的是,你所有的假设似乎都是正确的,我认为没有办法做到这一点.为了节省电池寿命,iPhone的定位服务基于移动.如果手机位于一个位置,则位置服务不可见.

CLLocationManager会只调用locationManager:didUpdateToLocation:fromLocation:当手机接收到一个位置更新,其中只有三个位置服务中的一个(手机信号塔,GPS,WIFI)感知的变化发生.

其他一些可能有助于进一步解决方案的事情:

  • 启动和停止服务会导致didUpdateToLocation调用委托方法,但newLocation可能具有旧的时间戳.

  • 区域监控可能有所帮助

  • 在后台运行时,请注意可能很难获得Apple批准的"完整"LocationServices支持.从我所看到的,他们专门设计startMonitoringSignificantLocationChanges为需要后台位置支持的应用程序的低功耗替代品,并强烈鼓励开发人员使用它,除非应用程序绝对需要它.

祝好运!

更新:这些想法现在可能已经过时了.看起来好像人们在上面的@wjans回答中取得了成功.

  • 在您描述的情况下,应用程序很可能使用startMonitoringSignificantLocationChanges方法.在这里,手机在收到位置更新时会暂时"被唤醒",但没有间隔可以设置为在后台"ping"此服务.当手机移动(或从手机切换到GPS或Wifi)时,它会触发更新.关于该主题的斯坦福iTunes U讲座对我很有帮助 - 希望它可以帮助您找到解决方法:http://itunes.apple.com/us/itunes-u/iphone-application-development/id384233225# (2认同)
  • Thx为指针.但是,我仍然不明白应用程序在做什么.即使我的手机在我的办公桌上,根本没有移动,我也可以看到定位服务每10分钟(确切地)触发一次.如果我理解正确,那么`startMonitoringSignificantLocationChanges`将不会给出任何更新. (2认同)

sam*_*ui7 5

我确实使用位置服务编写应用程序,应用程序必须每10秒发送一次位置.而且效果很好.

只需按照Apple的doc 使用" allowDeferredLocationUpdatesUntilTraveled:timeout "方法即可.

我做的是:

必需:注册更新位置的后台模式.

1.创建LocationMangerstartUpdatingLocation,以accuracyfilteredDistance为任何你想要的:

-(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];
}
Run Code Online (Sandbox Code Playgroud)

2.为了让app永远allowDeferredLocationUpdatesUntilTraveled:timeout在后台使用方法运行,updatingLocation当app移动到后台时你必须使用新参数重启,如下所示:

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

    [_locationManager stopUpdatingLocation];
    [_locationManager setDesiredAccuracy:kCLLocationAccuracyBest];
    [_locationManager setDistanceFilter:kCLDistanceFilterNone];
    _locationManager.pausesLocationUpdatesAutomatically = NO;
    _locationManager.activityType = CLActivityTypeAutomotiveNavigation;
    [_locationManager startUpdatingLocation];
 }
Run Code Online (Sandbox Code Playgroud)

3.应用程序通过locationManager:didUpdateLocations:回调正常获取updatedLocations :

-(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];
    }
}
Run Code Online (Sandbox Code Playgroud)

4.但是,你应该处理的数据,然后locationManager:didFinishDeferredUpdatesWithError:回调你的目的

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

     _deferringUpdates = NO;

     //do something 
}
Run Code Online (Sandbox Code Playgroud)

5. 注意:我认为我们应该重置LocationManager每次应用程序在背景/地面模式之间切换的参数.

  • 我确实尝试了你提到的两种解决方案,并且看到@ wjans的一个更节省电池.但是,在iOS 8问世后,似乎该解决方案不再适用.更多细节:大部分时间,应用程序无法在后台模式下长时间使用. (2认同)

Nil*_*esh 5

if ([self.locationManager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)]) {
    [self.locationManager setAllowsBackgroundLocationUpdates:YES];
}
Run Code Online (Sandbox Code Playgroud)

这是iOS 9以来的背景位置跟踪所必需的.