我如何在我的iOS应用程序中每n分钟得到一个后台位置更新?

我正在寻找一种方法来获得背景位置更新每n分钟在我的iOS应用程序。我使用的是iOS 4.3,解决方案应该适用于未越狱的iphone。

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

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

如何实现这些定期的后台位置更新?

190794 次浏览

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

CLLocationManager只会在手机接收到位置更新时调用locationManager:didUpdateToLocation:fromLocation:,这只会发生在三个位置服务(手机信号塔、gps、wifi)之一感知到变化时。

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

  • 开始,停止服务会导致didUpdateToLocation委托方法被调用,但是newLocation可能有旧的时间戳。

  • 区域监控可能会有所帮助

  • 当在后台运行时,请注意可能很难获得苹果批准的“完整”LocationServices支持。据我所知,他们专门为需要后台位置支持的应用程序设计了startMonitoringSignificantLocationChanges作为低功耗的替代方案,并强烈鼓励开发人员使用它,除非应用程序绝对需要它。

好运!

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

在苹果开发者论坛的帮助下,我找到了一个解决方案:

  • 指定location background mode
  • 在后台使用UIApplication:beginBackgroundTaskWithExpirationHandler:创建一个NSTimer
  • n而不是UIApplication:backgroundTimeRemaining时,它将正常工作。当n更大的时,应该在没有剩余时间之前再次启用(并禁用)location manager,以避免后台任务被终止。

这是因为位置是三种允许的后台执行类型之一

注意:我在模拟器中测试了一些时间,在那里它不起作用。不过,它在我的手机上运行得很好。

我在我正在开发的应用程序中做到了这一点。当应用程序处于后台时,计时器不工作,但应用程序不断接收位置更新。我在文档的某个地方读到(我现在似乎找不到它,当我做的时候我会更新),一个方法只能在应用程序在后台的活动运行循环上被调用。应用程序委托有一个活动的运行循环,甚至在bg中,所以你不需要创建自己的工作。 [我不确定这是否是正确的解释,但这是我从我读到的东西中理解的]

首先,在应用程序的info.plist中添加键UIBackgroundModeslocation对象。现在,你需要做的是启动位置更新在你的应用程序的任何地方:

    CLLocationManager locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;//or whatever class you have for managing location
[locationManager startUpdatingLocation];
接下来,写一个方法来处理位置更新, 在应用程序委托中写入-(void)didUpdateToLocation:(CLLocation*)location。然后在你启动位置管理器的类中实现CLLocationManagerDelegatelocationManager:didUpdateLocation:fromLocation方法(因为我们将位置管理器委托设置为'self')。在此方法中,您需要检查必须处理位置更新的时间间隔是否已过。您可以通过每次保存当前时间来做到这一点。如果时间已经过去,从应用委托调用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];
}
}
}
这将每5分钟调用你的方法,即使你的应用程序在后台。 小鬼:这个实现耗尽电池,如果你的位置数据的准确性不是关键,你应该使用[locationManager startMonitoringSignificantLocationChanges]

在把它添加到你的应用程序之前,请阅读位置感知编程指南

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


}

就像这样测试:

我打开应用程序,进入后台,几分钟后开车。然后我回家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;
}

给其他做噩梦的人想想这个。我有一个简单的解决办法。

  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];
    
    
    }
    
  3. Just don't forget to add "App registers for location updates" in info.plist.

我使用xs2bush的方法获取一个区间(使用timeIntervalSinceDate),并对它进行了一点扩展。我想确保我得到了我所需要的精度,同时我也没有因为保持gps收音机开得太久而耗尽电池。

我保持位置运行持续与以下设置:

locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers;
locationManager.distanceFilter = 5;

这是一个相对低的电池消耗。当我准备好获得下一个周期性位置读数时,我首先检查位置是否在我期望的精度范围内,如果是,然后使用该位置。如果不是,那么我用这个来提高准确性:

locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters;
locationManager.distanceFilter = 0;

得到我的位置,然后一旦我有了位置,我把精度再次降低,以尽量减少电池的消耗。我已经写了一个完整的工作样例,我也写了源代码的服务器端代码来收集位置数据,存储到数据库,并允许用户实时查看gps数据或检索和查看以前存储的路线。我有客户端iOS,安卓,windows手机和java我。所有客户端都是本地编写的,它们都在后台正常工作。这个项目是MIT授权的。

iOS项目的目标是iOS 6,使用iOS 7的基础SDK。你可以得到代码在这里

如果你看到任何问题,请在github上提交一个问题。谢谢。

我确实写了一个使用位置服务的应用程序,应用程序必须每10秒发送一次位置。

只要使用“allowDeferredLocationUpdatesUntilTraveled:超时”方法,遵循苹果的文档。

我所做的是:

注册后台模式更新位置。

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

为了在后台使用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. App通过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];
}
}

但是为了你的目的,你应该在locationManager:didFinishDeferredUpdatesWithError:回调中处理数据

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


_deferringUpdates = NO;


//do something
}

我认为我们应该在每次应用程序在后台/前台模式之间切换时重置LocationManager的参数。

附件是基于以下的Swift解决方案:

在info.plist中定义App registers for location updates

保持locationManager一直运行

BestForNavigation(5秒来获取位置)和ThreeKilometers之间切换kCLLocationAccuracy,以避免电池耗尽

这个例子在前台每1分钟更新一次位置,在后台每15分钟更新一次位置。

这个例子适用于运行在iOS 7设备上的Xcode 6 Beta 6。

在App委托中(mapView是一个指向mapView控制器的可选选项)

func applicationDidBecomeActive(application: UIApplication!) {
if appLaunched! == false { // Reference to mapView used to limit one location update per timer cycle
appLaunched = true
var appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
var window = appDelegate.window
var tabBar = window?.rootViewController as UITabBarController
var navCon = tabBar.viewControllers[0] as UINavigationController
mapView = navCon.topViewController as? MapViewController
}
self.startInitialPeriodWithTimeInterval(60.0)
}


func applicationDidEnterBackground(application: UIApplication!) {
self.startInitialPeriodWithTimeInterval(15 * 60.0)
}


func startInitialPeriodWithTimeInterval(timeInterval: NSTimeInterval) {
timer?.invalidate() // reset timer
locationManager?.desiredAccuracy = kCLLocationAccuracyBestForNavigation
timer = NSTimer.scheduledTimerWithTimeInterval(5.0, target: self, selector: Selector("getFirstLocationUpdate:"), userInfo: timeInterval, repeats: false)
}


func getFirstLocationUpdate(sender: NSTimer) {
let timeInterval = sender.userInfo as Double
timer?.invalidate()
mapView?.canReportLocation = true
timer = NSTimer.scheduledTimerWithTimeInterval(timeInterval, target: self, selector: Selector("waitForTimer:"), userInfo: timeInterval, repeats: true)
}


func waitForTimer(sender: NSTimer) {
let time = sender.userInfo as Double
locationManager?.desiredAccuracy = kCLLocationAccuracyBestForNavigation
finalTimer = NSTimer.scheduledTimerWithTimeInterval(5.0, target: self, selector: Selector("getLocationUpdate"), userInfo: nil, repeats: false)
}


func getLocationUpdate() {
finalTimer?.invalidate()
mapView?.canReportLocation = true
}

在mapView中(locationManager指向AppDelegate中的对象)

override func viewDidLoad() {
super.viewDidLoad()
var appDelegate = UIApplication.sharedApplication().delegate! as AppDelegate
locationManager = appDelegate.locationManager!
locationManager.delegate = self
canReportLocation = true
}


func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
if canReportLocation! {
canReportLocation = false
locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
} else {
//println("Ignore location update")
}
}

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

似乎stopUpdatingLocation是什么触发后台看门狗定时器,所以我在didUpdateLocation替换它:

     [self.locationManager setDesiredAccuracy:kCLLocationAccuracyThreeKilometers];
[self.locationManager setDistanceFilter:99999];

这似乎有效地关闭了GPS。然后,背景NSTimer的选择器变成:

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

我所做的是每隔几分钟定期切换精度以获得高精度坐标,因为locationManager没有停止,backgroundtimerremain保持在其最大值。这将我的设备上的电池消耗从每小时~10%(在后台使用恒定的kCLLocationAccuracyBest)降低到每小时~2%

if ([self.locationManager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)]) {
[self.locationManager setAllowsBackgroundLocationUpdates:YES];
}

自iOS 9以来,这是后台位置跟踪所需要的。

以下是我使用的方法:

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

我像这样在AppDelegate中开始跟踪:

BackgroundLocationManager.instance.start()

cocoapod APScheduledLocationManager允许每n秒获得所需位置精度的后台位置更新。

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

存储库还包含一个用Swift 3编写的示例应用程序。

在iOS 9和watchOS 2.0中,CLLocationManager上有一个新方法,可以让你请求当前位置:CLLocationManager:requestLocation()。这立即完成,然后将位置返回给CLLocationManager委托。

你现在可以使用NSTimer每分钟用这个方法请求一个位置,而不必使用startUpdatingLocation和stopUpdatingLocation方法。

然而,如果你想要根据上次位置X米的变化来捕获位置,只需要设置CLLocationManger的distanceFilter属性并调用startUpdatingLocation()。