需求
接到一个新的需求:
收集用户的定位信息,包括前台后台,定位信息不需要持续定位,2-3分钟获取一次即可,需要保存到数据库,每隔30分钟上传到服务器,下次可以通过用户的定位信息绘制地图轨迹图。
那这里就有一个长后台的需求,需要保证程序在后台的也可以定位。
思考点🤔
- 长后台,但是我们要避免耗电,而后台持续定位,或者持续voip播放模式后,虽然会长时间保活,但是耗电还是挺高的
- 那我们就可以使用UIBackgroundTask,这个可以保证后台会延活3分钟
- 定位数据,每次获取到数据数据后就关闭定位,2分钟后再获取定位同时执行新的UIBackgroundTask,同时在合适的时候将旧的task失效掉,保证只有一个最后的后台任务在跑
- 定时器问题,我们使用dispatch_source_t timer,不会出现NSTimer的一些常见的问题(runloop,thread,leak)
- 必须保证有一个活跃的runloop
- NSTimer的创建和撤销必须在同一个线程操作,不能跨线程
- 存在内存泄漏的风险(target 对象A(一般可能是当前控制器)中被timer持有,info也被这个timer强引用,如果是设置repeat为YES,那这时候需要手动将timer cancel,不然页面不会被释放掉)
关键代码
长后台
beginBackgroundTaskWithExpirationHandler //开启后台任务
endBackgroundTask //关闭后台任务
这两个需要成对出现
这里的思路是有个后台的任务数组保存开启的后台任务id,和记录一个当前后台任务ID。
定时器每120秒(两分钟)执行一次获取定位信息,并新增新的后台任务
@property (nonatomic,strong) NSMutableArray *bgTaskIdList;///<后台任务数组
@property (assign) UIBackgroundTaskIdentifier masterTaskId;///<当前后台任务ID
//开启新的后台任务
-(UIBackgroundTaskIdentifier)beginNewBackgroundTask
{
UIApplication *application = [UIApplication sharedApplication];
__block UIBackgroundTaskIdentifier bgTaskId = UIBackgroundTaskInvalid;
if([application respondsToSelector:@selector(beginBackgroundTaskWithExpirationHandler:)])
{
bgTaskId = [application beginBackgroundTaskWithExpirationHandler:^{
NSLog(@"bgTask 过期 %lu",(unsigned long)bgTaskId);
[self.bgTaskIdList removeObject:@(bgTaskId)];//过期任务从后台数组删除
bgTaskId = UIBackgroundTaskInvalid;
[application endBackgroundTask:bgTaskId];
}];
}
//如果上次记录的后台任务已经失效了,就记录最新的任务为主任务
if (_masterTaskId == UIBackgroundTaskInvalid) {
self.masterTaskId = bgTaskId;
NSLog(@"开启后台任务 %lu",(unsigned long)bgTaskId);
}
else //如果上次开启的后台任务还未结束,就提前关闭了,使用最新的后台任务
{
//add this id to our list
NSLog(@"保持后台任务 %lu", (unsigned long)bgTaskId);
[self.bgTaskIdList addObject:@(bgTaskId)];
[self endBackGroundTask:NO];//留下最新创建的后台任务
}
return bgTaskId;
}
//关闭后台任务
-(void)endBackGroundTask:(BOOL)all
{
UIApplication *application = [UIApplication sharedApplication];
//如果为all 清空后台任务数组
//不为all 留下数组最后一个后台任务,也就是最新开启的任务
if ([application respondsToSelector:@selector(endBackGroundTask:)]) {
for (int i = 0; i < (all ? _bgTaskIdList.count :_bgTaskIdList.count -1); i++) {
UIBackgroundTaskIdentifier bgTaskId = [self.bgTaskIdList[0]integerValue];
NSLog(@"关闭后台任务 %lu",(unsigned long)bgTaskId);
[application endBackgroundTask:bgTaskId];
[self.bgTaskIdList removeObjectAtIndex:0];
}
}
///如果数组大于0 所有剩下最后一个后台任务正在跑
if(all){
[application endBackgroundTask:self.masterTaskId];
self.masterTaskId = UIBackgroundTaskInvalid;
}
}
定时器与定位
//开启定位和定时器,外部开启
- (void)initConfigTimerAndLocation{
self.isConfig = YES;
[self startLocation];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
[self saveLocationTimer];
[self uploadLocationsToServer];
}
//开启上传定时器
- (void)uploadLocationsToServer{
//半小时上传 1800s
[BgDispatchTimer scheduleDispatchTimerWithName:@"UpLoad" timeInterval:1800 queue:nil repeats:YES action:^{
//从数据库拿出数据
[[SimpleDB sharedSimpleDB] queryDataInLocationTable:^(NSArray * _Nonnull results) {
if (results.count > 0) {
//上传
[RequestService uploadTrackInfo:results sussess:^(NSString *message) {
//上传成功,删除数据库之前所有旧的定位信息
[[SimpleDB sharedSimpleDB] deleteAllDataLocationTable];
} failure:^(NSError *error) {
}];
}
}];
}];
}
//开启保存定位定时器
- (void)saveLocationTimer{
//120秒后重启定位 2分钟存一下
[BgDispatchTimer scheduleDispatchTimerWithName:@"saveLocation" timeInterval:120 queue:nil repeats:YES action:^{
//开启定位
[self startLocation];
}];
}
//监听进入后台方法
- (void)applicationEnterBackground{
NSLog(@"come in background");
[self startLocation];
}
//重新开启定位
- (void)startLocation{
WeakSelf(self);
//要在主线程,不然定位不回调
dispatch_async(dispatch_get_main_queue(), ^{
[self.mapManager getLastLocation:^(CLLocation *location, NSString *address) {
StrongSelf(self);
[self saveLocationWithinfo:location addderes:address];
}];
});
[self.bgTask beginNewBackgroundTask];
}
Tip
开启定位需要在info.plist添加定位描述代码
//source code 添加
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>请求获取用户定位权限</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>请求获取用户定位权限</string>
代码地址
Demo已经脱敏,网络请求和数据库信息已经删去
长后台定位Demo
参考文章
iOS 关于后台持续运行
CLLocationManager-blocks
GCD实现多个定时器,完美避过NSTimer的三大缺陷