後臺長時間定時定位-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%左右。但在實踐的過程中發現它存在一些問題。
- 傳送資料到伺服器方法的定時器,最大支援3分鐘;
- 傳送資料到伺服器方法的定時器,在執行2小時後就停止了;
- 傳送資料到伺服器方法的定時器,不準確;
經過一些嘗試,廢棄了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];
}
}