iOS App 后台长定位方案

需求

接到一个新的需求:
收集用户的定位信息,包括前台后台,定位信息不需要持续定位,2-3分钟获取一次即可,需要保存到数据库,每隔30分钟上传到服务器,下次可以通过用户的定位信息绘制地图轨迹图。
那这里就有一个长后台的需求,需要保证程序在后台的也可以定位。

思考点

  1. 长后台,但是我们要避免耗电,而后台持续定位,或者持续voip播放模式后,虽然会长时间保活,但是耗电还是挺高的
  2. 那我们就可以使用UIBackgroundTask,这个可以保证后台会延活3分钟
  3. 定位数据,每次获取到数据数据后就关闭定位,2分钟后再获取定位同时执行新的UIBackgroundTask,同时在合适的时候将旧的task失效掉,保证只有一个最后的后台任务在跑
  4. 定时器问题,我们使用dispatch_source_t timer,不会出现NSTimer的一些常见的问题(runloop,thread,leak)
    1. 必须保证有一个活跃的runloop
    2. NSTimer的创建和撤销必须在同一个线程操作,不能跨线程
    3. 存在内存泄漏的风险(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 添加
    NSLocationAlwaysAndWhenInUseUsageDescription
    请求获取用户定位权限
    NSLocationWhenInUseUsageDescription
    请求获取用户定位权限

代码地址

Demo已经脱敏,网络请求和数据库信息已经删去
长后台定位Demo

参考文章
iOS 关于后台持续运行
CLLocationManager-blocks
GCD实现多个定时器,完美避过NSTimer的三大缺陷

你可能感兴趣的:(iOS App 后台长定位方案)