CLLocationManager responsiveness

eric.mitchell picture eric.mitchell · Mar 17, 2012 · Viewed 10.5k times · Source

I have an app that revolves around the device's GPS and the information that comes from it. It is important that the location data be accurate and up-to-date. I know that the device is limited by its GPS and the GPS's limits, but I was wondering if there is anything I can do to tweak/improve the performance of the iPhone GPS, particularly in the speed area. Because location updates lag about 3-5 seconds behind the real-time location of the device, the velocity reported by the location manager also lags that far behind the real-time value. In my case, that is simply too long. I understand that there might not be anything I can do, but has anyone had any success in improving the responsiveness of the iPhone GPS? Every little bit makes a difference.

Edit 1:

My location manager is inside a singleton class, as Apple recommends.

Inside SingletonDataController.m:

static CLLocationManager* locationManager;
locationManager = [CLLocationManager new];
locationManager.distanceFilter = kCLDistanceFilterNone;
locationManager.headingFilter = kCLHeadingFilterNone;

if(([[UIDevice currentDevice] batteryState] == UIDeviceBatteryStateCharging) || ([[UIDevice currentDevice] batteryState] == UIDeviceBatteryStateFull)) {
    locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
} else {
    locationManager.desiredAccuracy = kCLLocationAccuracyBest;
}

[sharedSingleton setLocationManager:locationManager];
[locationManager release];

Inside MapView.m (where the location manager is actually used):

- (id)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
    //setup
    [SingletonDataController sharedSingleton].locationManager.delegate = self;
    //more setup
}

- (void)batteryChanged {
    if(([[UIDevice currentDevice] batteryState] == UIDeviceBatteryStateCharging) || ([[UIDevice currentDevice] batteryState] == UIDeviceBatteryStateFull)) {
        [SingletonDataController sharedSingleton].locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
    } else {
        [SingletonDataController sharedSingleton].locationManager.desiredAccuracy = kCLLocationAccuracyBest;
    }
}

- (void)viewDidLoad {
    //setup
    [[NSNotificationCenter defaultCenter]
      addObserver:self 
         selector:@selector(batteryChanged) 
             name:UIDeviceBatteryStateDidChangeNotification 
           object:nil];
    //other setup
}

The data handling happens inside locationManager:didUpdateToLocation:fromLocation:. I don't believe that inefficiency here is the cause of the lag.

locationManager:didUpdateToLocation:fromLocation: calls this method to update the UI:

- (void)setLabels:(CLLocation*)newLocation fromOldLocation:(CLLocation*)oldLocation {
    //set speed label
    if(iterations > 0) {
        if(currentSpeed > keyStopSpeedFilter) {
            if(isFollowing) {
                [mapViewGlobal setRegion:MKCoordinateRegionMake([newLocation coordinate], mapViewGlobal.region.span)];
            }

            NSString* currentSpeedString;
            if(isCustomary) {
                currentSpeedString = [[NSString alloc] initWithFormat:@"%.1f miles per hour", (currentSpeed * 2.23693629f)];
            } else {
                currentSpeedString = [[NSString alloc] initWithFormat:@"%.1f km per hour", (currentSpeed * 3.6f)];
            }

            [speedLabel setText:currentSpeedString];
            [currentSpeedString release];
        } else {
            speedLabel.text = @"Not moving";
        }
    }

    //set average speed label
    if(iterations > 4 && movementIterations > 2) {
        NSString* averageSpeedString;
        if(isCustomary) {
            averageSpeedString = [[NSString alloc] initWithFormat:@"%.1f miles per hour", (float)((speedAverages / (long double)movementIterations) * 2.23693629f)];
        } else {
            averageSpeedString = [[NSString alloc] initWithFormat:@"%.1f km per hour", (float)((speedAverages / (long double)movementIterations) * 3.6f)];
        }
        [averageSpeedLabel setText:averageSpeedString];
        [averageSpeedString release];
    }

    //set elapsed time label
    NSInteger seconds = [[NSDate date] timeIntervalSinceDate:dataObject.locationManagerStartDate];
    NSInteger minutes = seconds / 60;
    NSInteger hours = minutes / 60;

    //get remainder
    seconds %= 60;

    NSString* timeString;
    NSString* secondsString;
    NSString* minutesString;
    NSString* hoursString;

    if((seconds % 60) < 10) {
        secondsString = [[NSString alloc] initWithFormat:@"0%i", seconds];
    } else {
        secondsString = [[NSString alloc] initWithFormat:@"%i", seconds];
    }

    if((minutes % 60) < 10) {
        minutesString = [[NSString alloc] initWithFormat:@"0%i", minutes];
    } else {
        minutesString = [[NSString alloc] initWithFormat:@"%i", minutes];
    }

    if((hours % 60) < 10) {
        hoursString = [[NSString alloc] initWithFormat:@"0%i", hours];
    } else {
        hoursString = [[NSString alloc] initWithFormat:@"%i", hours];
    }

    timeString = [[NSString alloc] initWithFormat:@"%@:%@:%@", hoursString, minutesString, secondsString];

    [elapsedTimeLabel setText:timeString];

    [timeString release], timeString = nil;
    [secondsString release], secondsString = nil;
    [minutesString release], minutesString = nil;
    [hoursString release], hoursString = nil;

    NSString* totalDistanceString;
    if(isCustomary) {
        totalDistanceString = [[NSString alloc] initWithFormat:@"Total: %.2f mi", (float)distance * 0.000621371192f];
    } else {
        totalDistanceString = [[NSString alloc] initWithFormat:@"Total: %.2f km", (float)distance / 1000.0f];
    }
    [customTopBar setTitle:totalDistanceString];
    [totalDistanceString release];
}

With a couple of NSDates and NSLogs I have found that the execution of the entire locationManager:didUpdateToLocation:fromLocation: (not just the label updating method) never takes more than about 8ms on my iPhone 4; in other words, the data handling isn't the problem.

Answer

fishinear picture fishinear · Mar 31, 2012

OK, a couple of things could improve your lag. First of all, use kCLLocationAccuracyBestForNavigation always. There is no real battery usage difference between that and kCLLocationAccuracyBest, they both use the GPS at top speed. The main difference is in the post-processing that Apple does.

Second, there is no need to filter for speed == 0. Apple already does that filtering: if your speed from the GPS drops below a certain threshold (about 4 km/h), the OS assumes you are standing still, and it substitutes the same location value for all subsequent samples. It does that until it thinks you are moving again. I assume they do that to avoid "jittering" on the map when you are standing still. In fact, speed drops to 0 already for the last real value of a sequence of "standing-still" values, so if you filter on speed == 0 than you are missing one real GPS sample.

Unfortunately, they is no way to avoid that filtering and get real GPS samples. I talked to Apple about it, and their response was that they are not going to change the behaviour. kCLLocationAccuracyBestForNavigation does less aggressive filtering than kCLLocationAccuracyBest, so it's best to use that.

Third, you probably are already doing this, but make sure that you call "setNeedsDisplay" on your view right from the "didUpdateFromLocation:", to make sure that the map is actually redrawn.

If you do all that, you should have a lag of about 1 second. If you want to improve on the 1 second than you can try to use predictive techniques. From the last two locations, and the given speed, you can calculate where the next location is likely to be, and already display that location. I have had mixed results with that. It works well for fast movement that does not change speed suddenly, like driving a car. It works less well for slower movement like walking or biking.