在iOS 7中从后台任务启动位置管理器

27

在iOS 7中,似乎一个应用程序不能再通过调用startUpdatingLocation从后台任务中启动位置管理器了。

在iOS 6中,我使用了这里描述的方法:https://dev59.com/8mw15IYBdhLWcg3w3vle#6465280,每n分钟运行后台位置更新。想法是使用定时器运行后台任务,并在定时器触发时启动位置管理器。此后关闭位置管理器并开始另一个后台任务。

升级到iOS 7后,这种方法不再起作用。启动位置管理器后,应用程序不会接收到任何locationManager:didUpdateLocations。有什么想法吗?


我的iPhone电池非常高兴苹果做出了不让应用程序这样做的决定 :) - Krumelur
3个回答

43
我找到了问题/解决方案。当启动位置服务并停止后台任务时,应该延迟停止后台任务(我使用了1秒)。否则,位置服务将无法启动。此外,位置服务应该保持开启几秒钟(在我的示例中为3秒)。
另一个重要的注意事项是,在iOS 7中,最大后台时间现在是3分钟,而不是10分钟。
更新于2016年10月29日
有一个名为APScheduledLocationManager的Cocoapod可以让您按所需的位置精度每n秒获取后台位置更新。
let manager = APScheduledLocationManager(delegate: self)
manager.startUpdatingLocation(interval: 170, acceptableLocationAccuracy: 100)

该代码库还包含一个使用Swift 3编写的示例应用程序。
更新于2014年5月27日
Objective-C示例:
1)在“.plist”文件中将UIBackgroundModes设置为“location”。
2)在任何您想要的地方创建ScheduledLocationManager实例。
@property (strong, nonatomic) ScheduledLocationManager *slm;

3) 设置它

self.slm = [[ScheduledLocationManager alloc]init];
self.slm.delegate = self;
[self.slm getUserLocationWithInterval:60]; // replace this value with what you want, but it can not be higher than kMaxBGTime

4) 实现委托方法

-(void)scheduledLocationManageDidFailWithError:(NSError *)error
{
    NSLog(@"Error %@",error);
}

-(void)scheduledLocationManageDidUpdateLocations:(NSArray *)locations
{
    // You will receive location updates every 60 seconds (value what you set with getUserLocationWithInterval)
    // and you will continue to receive location updates for 3 seconds (value of kTimeToGetLocations).
    // You can gather and pick most accurate location
    NSLog(@"Locations %@",locations);
}

这是ScheduledLocationManager的实现:

ScheduledLocationManager.h

#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>

@protocol ScheduledLocationManagerDelegate <NSObject>

-(void)scheduledLocationManageDidFailWithError:(NSError*)error;
-(void)scheduledLocationManageDidUpdateLocations:(NSArray*)locations;

@end

@interface ScheduledLocationManager : NSObject <CLLocationManagerDelegate>

-(void)getUserLocationWithInterval:(int)interval;

@end

ScheduledLocationManager.m

#import "ScheduledLocationManager.h"

int const kMaxBGTime = 170; // 3 min - 10 seconds (as bg task is killed faster)
int const kTimeToGetLocations = 3; // time to wait for locations

@implementation ScheduledLocationManager
{
    UIBackgroundTaskIdentifier bgTask;
    CLLocationManager *locationManager;
    NSTimer *checkLocationTimer;
    int checkLocationInterval;
    NSTimer *waitForLocationUpdatesTimer;
}

- (id)init
{
    self = [super init];
    if (self) {
        locationManager = [[CLLocationManager alloc] init];
        locationManager.delegate = self;
        locationManager.desiredAccuracy = kCLLocationAccuracyBest;
        locationManager.distanceFilter = kCLDistanceFilterNone;

        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
    }
    return self;
}

-(void)getUserLocationWithInterval:(int)interval
{
    checkLocationInterval = (interval > kMaxBGTime)? kMaxBGTime : interval;
    [locationManager startUpdatingLocation];
}

- (void)timerEvent:(NSTimer*)theTimer
{
    [self stopCheckLocationTimer];
    [locationManager startUpdatingLocation];

    // in iOS 7 we need to stop background task with delay, otherwise location service won't start
    [self performSelector:@selector(stopBackgroundTask) withObject:nil afterDelay:1];
}

-(void)startCheckLocationTimer
{
    [self stopCheckLocationTimer];
    checkLocationTimer = [NSTimer scheduledTimerWithTimeInterval:checkLocationInterval target:self selector:@selector(timerEvent:) userInfo:NULL repeats:NO];
}

-(void)stopCheckLocationTimer
{
    if(checkLocationTimer){
        [checkLocationTimer invalidate];
        checkLocationTimer=nil;
    }
}

-(void)startBackgroundTask
{
    [self stopBackgroundTask];
    bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        //in case bg task is killed faster than expected, try to start Location Service
        [self timerEvent:checkLocationTimer];
    }];
}

-(void)stopBackgroundTask
{
    if(bgTask!=UIBackgroundTaskInvalid){
        [[UIApplication sharedApplication] endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }
}

-(void)stopWaitForLocationUpdatesTimer
{
    if(waitForLocationUpdatesTimer){
        [waitForLocationUpdatesTimer invalidate];
        waitForLocationUpdatesTimer =nil;
    }
}

-(void)startWaitForLocationUpdatesTimer
{
    [self stopWaitForLocationUpdatesTimer];
    waitForLocationUpdatesTimer = [NSTimer scheduledTimerWithTimeInterval:kTimeToGetLocations target:self selector:@selector(waitForLoactions:) userInfo:NULL repeats:NO];
}

- (void)waitForLoactions:(NSTimer*)theTimer
{
    [self stopWaitForLocationUpdatesTimer];

    if(([[UIApplication sharedApplication ]applicationState]==UIApplicationStateBackground ||
        [[UIApplication sharedApplication ]applicationState]==UIApplicationStateInactive) &&
       bgTask==UIBackgroundTaskInvalid){
        [self startBackgroundTask];
    }

    [self startCheckLocationTimer];
    [locationManager stopUpdatingLocation];
}

#pragma mark - CLLocationManagerDelegate methods

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
    if(checkLocationTimer){
        //sometimes it happens that location manager does not stop even after stopUpdationLocations
        return;
    }

    if (self.delegate && [self.delegate respondsToSelector:@selector(scheduledLocationManageDidUpdateLocations:)]) {
        [self.delegate scheduledLocationManageDidUpdateLocations:locations];
    }

    if(waitForLocationUpdatesTimer==nil){
        [self startWaitForLocationUpdatesTimer];
    }
}

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
{
    if (self.delegate && [self.delegate respondsToSelector:@selector(scheduledLocationManageDidFailWithError:)]) {
        [self.delegate scheduledLocationManageDidFailWithError:error];
    }
}

#pragma mark - UIAplicatin notifications

- (void)applicationDidEnterBackground:(NSNotification *) notification
{
    if([self isLocationServiceAvailable]==YES){
        [self startBackgroundTask];
    }
}

- (void)applicationDidBecomeActive:(NSNotification *) notification
{
    [self stopBackgroundTask];
    if([self isLocationServiceAvailable]==NO){
        NSError *error = [NSError errorWithDomain:@"your.domain" code:1 userInfo:[NSDictionary dictionaryWithObject:@"Authorization status denied" forKey:NSLocalizedDescriptionKey]];

        if (self.delegate && [self.delegate respondsToSelector:@selector(scheduledLocationManageDidFailWithError:)]) {
            [self.delegate scheduledLocationManageDidFailWithError:error];
        }
    }
}

#pragma mark - Helpers

-(BOOL)isLocationServiceAvailable
{
    if([CLLocationManager locationServicesEnabled]==NO ||
       [CLLocationManager authorizationStatus]==kCLAuthorizationStatusDenied ||
       [CLLocationManager authorizationStatus]==kCLAuthorizationStatusRestricted){
        return NO;
    }else{
        return YES;
    }
}

@end

2
这似乎不起作用且难以理解。getUserLocationWithInterval从未被调用过?请发布一个可运行的示例。 - pcoving
1
你是否在App Delegate中创建实例? - Taylor Abernethy Newman
1
嗨,当我使用上面的示例代码时,我遇到了以下错误:-[__NSDictionaryI applicationDidBecomeActive:]: unrecognized selector sent to instance 0x14db5cf0。请给予一些指导 :) - The iOSDev
3
这个方法完美地运作了,它节省了我的研发时间,谢谢作者。我也点了一个赞 :) 感谢@sash分享这个解决方案。 - The iOSDev
1
@AskeAnker 后台进程将会在您进行发布构建时停止。 - sash
显示剩余16条评论

7
我尝试了你的方法,但在我的设备上无法运行。你能给我展示一下你的代码吗?
我实际上找到了解决iOS 7中位置服务问题的方法。
在iOS 7中,你不能在后台启动位置服务。如果你想让位置服务在后台持续运行,你必须在前台启动它,然后它将在后台继续运行。
如果你像我一样,停止位置服务并使用计时器在后台重新启动它,它在iOS 7中是不起作用的。
如需更详细信息,请观看WWDC 2013视频307的前8分钟:https://developer.apple.com/wwdc/videos/ 更新:位置服务也可以在后台工作。请查看iOS 7中后台位置服务不起作用的更新文章,其中包含完整的解决方案和详细说明。

你好,我该如何在前台启动位置服务?我也遇到了同样的问题。谢谢。 - Guerrix
嗨Guerrix,你可以在这里查看我的完整解决方案:iOS 7后台服务 - Ricky
@Ricky,如果我们想要在应用程序通过双击主屏幕从后台移除时仍然获取位置更新,应该怎么做? - Azhar Bandri
@Azhar 我听说自从 iOS 7.1 版本以来,即使应用程序被杀死(不在前台或后台),也可以发送位置更新,但我仍然没有找到可靠的解决方案。 - Ricky
你说定位服务必须在前台启动?如果进入后台后,我需要每隔15分钟获取用户的位置怎么办? - SleepNot
@jeraldo 这是一篇旧帖子。我已经发布了一个详细的博客文章和一个在Github上完成的解决方案,教你如何使位置服务在后台工作:https://dev59.com/umMk5IYBdhLWcg3w0RCU#21966662 如果这篇文章对你有帮助,请给它点赞。 - Ricky

6
实现此操作的步骤如下:
  1. Add "App registers for location updates" at item 0 in "Required background modes" in info.plist of your project.

  2. Write below code at application did finish launching.

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startFetchingLocationsContinously) name:START_FETCH_LOCATION object:nil];
    
  3. Write below code from where you want to start tracking

    [[NSNotificationCenter defaultCenter] postNotificationName:START_FETCH_LOCATION object:nil];
    
    AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
                [appDelegate startUpdatingDataBase];
    
  4. Paste following code to AppDelegate.m

    #pragma mark - Location Update
    -(void)startFetchingLocationsContinously{
        NSLog(@"start Fetching Locations");
        self.locationUtil = [[LocationUtil alloc] init];
        [self.locationUtil setDelegate:self];
        [self.locationUtil startLocationManager];
    }
    
    -(void)locationRecievedSuccesfullyWithNewLocation:(CLLocation*)newLocation oldLocation:(CLLocation*)oldLocation{
        NSLog(@"location received successfullly in app delegate for Laitude: %f and Longitude:%f, and Altitude:%f, and Vertical Accuracy: %f",newLocation.coordinate.latitude,newLocation.coordinate.longitude,newLocation.altitude,newLocation.verticalAccuracy);
    }
    
    -(void)startUpdatingDataBase{
        UIApplication*    app = [UIApplication sharedApplication];
    
        bgTask = UIBackgroundTaskInvalid;
    
        bgTask = [app beginBackgroundTaskWithExpirationHandler:^(void){
            [app endBackgroundTask:bgTask];
        }];
    
        SAVE_LOCATION_TIMER =  [NSTimer scheduledTimerWithTimeInterval:300
                                                                target:self selector:@selector(startFetchingLocationsContinously) userInfo:nil repeats:YES];
    }
    
  5. Add a class by name "LocationUtil" and paste following code into the header file:

    #import <Foundation/Foundation.h>
    #import <CoreLocation/CoreLocation.h>
    @protocol LocationRecievedSuccessfully <NSObject>
    @optional
    -(void)locationRecievedSuccesfullyWithNewLocation:(CLLocation*)newLocation oldLocation:(CLLocation*)oldLocation;
    -(void)addressParsedSuccessfully:(id)address;
    
    @end
    @interface LocationUtil : NSObject <CLLocationManagerDelegate> {
    }
    
    //Properties
    @property (nonatomic,strong) id<LocationRecievedSuccessfully> delegate;
    -(void)startLocationManager;
    

    And paste following code in LocationUtil.m

    -(void)startLocationManager{
    
         locationManager = [[CLLocationManager alloc] init];
         locationManager.delegate = self;
         [locationManager setPausesLocationUpdatesAutomatically:YES]; //Utkarsh 20sep2013
         //[locationManager setActivityType:CLActivityTypeFitness];
         locationManager.distanceFilter = kCLDistanceFilterNone;
         locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
         [locationManager startUpdatingLocation];
    
         //Reverse Geocoding.
         geoCoder=[[CLGeocoder alloc] init];
    
        //set default values for reverse geo coding.
    }
    
    //for iOS<6
    - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
      //call delegate Method
      [delegate locationRecievedSuccesfullyWithNewLocation:newLocation oldLocation:oldLocation];
    
      NSLog(@"did Update Location");
    }
    
    //for iOS>=6.
    
    - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
    
      CLLocation *newLocation = [locations objectAtIndex:0];
      CLLocation *oldLocation = [locations objectAtIndex:0];
    
      [delegate locationRecievedSuccesfullyWithNewLocation:newLocation oldLocation:oldLocation];
      NSLog(@"did Update Locationsssssss");
    }
    

1
"START_FETCH_LOCATION" 定义在哪里? - DTDev

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