1. 程式人生 > >後臺長時間定時定位-Location實踐經驗

後臺長時間定時定位-Location實踐經驗

前言

最近接到這樣一個需求,每隔固定時間採集使用者的位置,然後再把這些資料上傳到伺服器。研究了下ios的定位功能,在後臺定時遇到了一些困難。當app進入後臺狀態,定時器就不再執行,導致無法取到使用者的位置。
在網上查了一些資料,發現有人已經實現了這個功能,它是一個Github上的第三方庫,叫Location,不僅能在後臺定時採集位置資料,還優化了定位方式,減少耗電。
接下來我們來看看Location是如何實現的以及它存在的問題。

程式碼結構


Location程式碼結構

BackgroundTaskManager

負責建立、管理後臺任務,提供兩個方法,開始和結束後臺任務。它實現了後臺任務的無限期執行。
@interface BackgroundTaskManager : NSObject

+(instancetype)sharedBackgroundTaskManager;

-(UIBackgroundTaskIdentifier)beginNewBackgroundTask;
-(void)endAllBackgroundTasks;

@end

LocationShareModel

包裝了後臺任務類,也管理定位定時器。

@interface LocationShareModel : NSObject
@property (nonatomic) NSTimer *timer;
@property (nonatomic) NSTimer * delay10Seconds;
@property
(nonatomic) BackgroundTaskManager * bgTask; @property (nonatomic) NSMutableArray *myLocationArray; +(id)sharedModel; @end

LocationTracker

負責管理後臺定時定位的主類。
我們先看一下它提供的介面。

+ (CLLocationManager *)sharedLocationManager;

//開始追蹤定位
- (BOOL)startLocationTracking;
//停止追蹤定位
- (void)stopLocationTracking;
//向伺服器傳送已獲取的裝置位置資料
- (void)updateLocationToServer;

LocationTracker類的初始化

+ (CLLocationManager *)sharedLocationManager {
    static CLLocationManager *_locationManager;
    @synchronized(self) {
        if (_locationManager == nil) {
            _locationManager = [[CLLocationManager alloc] init];
            //裝置位置精度,這裡設定為最高精度
            _locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
            //是否暫停更新
            _locationManager.pausesLocationUpdatesAutomatically = NO;             
            //iOS9的新特性,開啟後臺位置更新,還要在plist中配置
            if (IOS9ORLATER) {
                _locationManager.allowsBackgroundLocationUpdates = YES;
            }
        }
    }
    return _locationManager;
}

當應用進入後臺,藉助BackgroundTaskManager無限延長後臺任務的存活時間,進行後臺採集位置資訊。

- (id)init {
    if (self==[super init]) {
        //Get the share model and also initialize myLocationArray
        self.shareModel = [LocationShareModel sharedModel];
        self.shareModel.myLocationArray = [[NSMutableArray alloc]init];

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

-(void)applicationEnterBackground{
    CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
    locationManager.delegate = self;
    locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
    locationManager.distanceFilter = kCLDistanceFilterNone;

    if(IOS8ORLATER) {
        [locationManager requestAlwaysAuthorization];
    }
    [locationManager startUpdatingLocation];

    //Use the BackgroundTaskManager to manage all the background Task
    self.shareModel.bgTask = [BackgroundTaskManager sharedBackgroundTaskManager];
    [self.shareModel.bgTask beginNewBackgroundTask];
}

持續獲取裝置的位置,是個特別耗電的任務。為了減少耗電,定時關閉位置服務。在接收到位置資訊10秒後,關閉位置服務,一分鐘後再重新開啟位置服務,這樣就達到減少耗電的目的。我們看程式碼的實現

-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations{

    ......

    self.shareModel.bgTask = [BackgroundTaskManager sharedBackgroundTaskManager];
    [self.shareModel.bgTask beginNewBackgroundTask];

    //Restart the locationMaanger after 1 minute
    self.shareModel.timer = [NSTimer scheduledTimerWithTimeInterval:60 target:self
                                                           selector:@selector(restartLocationUpdates)
                                                           userInfo:nil
                                                            repeats:NO];

    //Will only stop the locationManager after 10 seconds, so that we can get some accurate locations
    //The location manager will only operate for 10 seconds to save battery
    if (self.shareModel.delay10Seconds) {
        [self.shareModel.delay10Seconds invalidate];
        self.shareModel.delay10Seconds = nil;
    }

    self.shareModel.delay10Seconds = [NSTimer scheduledTimerWithTimeInterval:10 target:self
                                                                    selector:@selector(stopLocationDelayBy10Seconds)
                                                                    userInfo:nil
                                                                     repeats:NO];

}

重啟位置服務

- (void) restartLocationUpdates
{
    if (self.shareModel.timer) {
        [self.shareModel.timer invalidate];
        self.shareModel.timer = nil;
    }

    CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
    locationManager.delegate = self;
    locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
    locationManager.distanceFilter = kCLDistanceFilterNone;

    if(IOS8ORLATER) {
        [locationManager requestAlwaysAuthorization];
    }
    [locationManager startUpdatingLocation];
}

按照Location庫的設計,在updateLocationToServer方法中,處理向伺服器傳送資訊。

- (void)updateLocationToServer {

    .........

    //TODO: 在這裡插入你的程式碼,處理向伺服器傳送位置資訊


    ........

}

實踐

這個類庫解決了後臺定時定位的問題,使用也很簡單,而且還處理了定位耗電的問題,使耗電量降低到每小時5%左右。但在實踐的過程中發現它存在一些問題。

  1. 傳送資料到伺服器方法的定時器,最大支援3分鐘;
  2. 傳送資料到伺服器方法的定時器,在執行2小時後就停止了;
  3. 傳送資料到伺服器方法的定時器,不準確;

經過一些嘗試,廢棄了updateLocationToServer方法,將傳送資料的程式碼移到locationManager中,才解決了這幾個問題。具體解決方法如下:
首先為LocationTracker類新增一個私有的屬性,記錄上次執行傳送資料的時間。

@interface LocationTracker ()
@property (nonatomic, strong) NSDate *lastSendDate;
@end

在locationManager回撥方法中新增自己的程式碼。

    .......
   //Select only valid location and also location with good accuracy
        if(newLocation!=nil&&theAccuracy>0
           &&theAccuracy<2000
           &&(!(theLocation.latitude==0.0&&theLocation.longitude==0.0))){

            self.myLastLocation = theLocation;
            self.myLastLocationAccuracy= theAccuracy;

            NSMutableDictionary * dict = [[NSMutableDictionary alloc]init];
            [dict setObject:[NSNumber numberWithFloat:theLocation.latitude] forKey:@"latitude"];
            [dict setObject:[NSNumber numberWithFloat:theLocation.longitude] forKey:@"longitude"];
            [dict setObject:[NSNumber numberWithFloat:theAccuracy] forKey:@"theAccuracy"];

            //Add the vallid location with good accuracy into an array
            //Every 1 minute, I will select the best location based on accuracy and send to server
            [self.shareModel.myLocationArray addObject:dict];

            //這裡插入程式碼,處理髮送位置資料到伺服器
                if (!self.lastSendDate || [[self.lastSendDate dateByAddingHours:1] compare:[NSDate date]] <= 0) {
                    self.lastSendDate = [NSDate date];
                    }
            }