前言上一篇文章提到过,我现在做的是一个基于LBS定位的社交APP。其中一个主要功能就是能够实时定位到每个成员在社交圈中的位置。后台实时上传位置很重要。一个技术点。接下来,我将谈谈我在这方面的实践经验。我们来看看实现这个功能的具体要求。由于我们是一个实时定位的生活社交APP,所以我们需要做到以下几点:1.如果用户的位置是不断变化的,那么每隔一段时间就上报一次 既然我们希望能够反馈用户在APP中的位置实时变化,定时上报即可。2.如果用户的移动速度很慢,在一定距离报告一次 如果用户处于低速状态(比如步行速度在1m/s左右)这时候,如果你还按照(1)中的方法report,地图上的点由于变化很小会很密集这种数据意义不大(而且如果要做轨迹服务就必须花费这些密集的点),所以这个时候我们按照距离间隔3上报,如果用户到达某个地方后位置没有变化,那么就不会继续上报 我们只关心位置的变化。如果用户位置没有变化或变化很小,则无需上报位置(如进入公司或等红灯时间长)。这个时候我们就不上报了(达到省电的目的)4.切换到后台也能定位上报 在后台报告是必要的。用户不可能一直运行我们的APP(iOS4已经支持)5.APP由于各种原因终止后(用户主动关闭,系统kill掉),也可以定位上报用户主动关闭APP。几率不高,但是因为系统调度被kill的情况很常见。这时候我们应该也能报到(iOS7已经支持被杀后唤醒)。分析完需求后,我们将介绍如何实施准备工作。首先做一些准备工作在target的Capabilities选项中打开BackgroundModes并勾选Locationupdates,然后在plist中添加NSLocationAlawaysUsageDescription键,在value中键入任意内容即可完成这两步。我们的前期工作已经完成。BackgroundModes是iOS7引入的新功能。NSLocationAlawaysUsageDescription为了增强权限机制,引入的提示说明不加。如果不加这个,就不会使用location函数,但是代码location必须和CLLocationManager打交道。所以我们先定义一个CLLocationManager的子类,根据需求定义三个变量@interfaceMMLocationManager:CLLocationManager+(instancetype)sharedManager;@property(nonatomic,assign)CGFloatminSpeed;//最小速度@property(nonatomic,assign)CGFloatminFilter;//最小range@property(nonatomic,assign)CGFloatminInteval;//更新间隔@end这里解释一下这几个参数minSpeed,如果当前移动速度大于这个值,则满足要求(1)时间作为更新依据(minFilter)如果当前移动速度小于这个值,则满足要求(2)Range作为更新依据(minInteval)minFilter为最小触发Range用于要求(1)minInteval更新间隔用于要求(2)接着是初始化函数-(instancetype)init{self=[superinit];if(self){self.minSpeed=3;self.minFilter=50;选择f.minInteval=10;self.delegate=self;self.distanceFilter=self.minFilter;self.desiredAccuracy=kCLLocationAccuracyBest;}returnsself;}这里的默认值可以根据需要调整,位置更新后的处理逻辑其实很简单,-(void)locationManager:(CLLocationManager*)managerdidUpdateLocations:(NSArray*)locations{CLLocation*location=locations[0];NSLog(@"%@",location);//根据触发范围调整根据实际情况[selfadjustDistanceFilter:location];//上传数据[selfuploadLocation:location];}而这个adjustDistanceFilter函数是整个代码的核心,它会根据当前的速度动态调整distanceFilter参数来满足我们的需求/***规则:如果速度如果小于minSpeedm/s,则设置触发范围为50m*否则,设置触发范围为minSpeed*minInteval*此时,如果速度变化超过10%,更新当前触发范围(这里的限制是因为distanceFilter不能连续设置,*否则会连续触发uploadLocation)*/-(void)adjustDistanceFilter:(CLLocation*)location{//NSLog(@"adjust:%f",location.speed);if(location.speed0.1f){self.distanceFilter=self.minFilter;}}else{CGFloatlastSpeed=self.distanceFilter/self.minInteval;if((fabs(lastSpeed-location.speed)/lastSpeed>0。1f)||(lastSpeed<0)){CGFloatnewSpeed=(int)(location.speed+0.5f);CGFloatnewFilter=newSpeed*self.minInteval;self.distanceFilter=newFilter;}}}这里是distanceFilter的参数不能一直设置是因为每次设置后下一秒就会立即触发didUpdateLocations回调(系统标准的最小更新间隔为1秒,即更新频率为1hz),所以这里只有当变化超过10%会重置distanceFilter#p#接下来,为了在被杀的情况下能够正确唤醒,我们还要做最后一步。在AppDelegate的didFinishLaunchingWithOptions中添加如下代码if([launchOptionsobjectForKey:UIApplicationLaunchOptionsLocationKey]){if([[MMLocationManagersharedManager]respondsToSelector:@selector(requestAlwaysAuthorization)]){[[MMLocationManagersharedManager]requestAlwaysAuthorization];}//这是后台启动的新属性在iOS9中的定位。如果不设置,顶部会有一个蓝色条(类似于热点连接)if([selfrespondsToSelector:@selector(allowsBackgroundLocationUpdates)]){[MMMocationManagersharedManager].allowsBackgroundLocationUpdates=YES;}[[MMLocationManagersharedManager]startUpdatingLocation];}这是因为launchOptionsLunchOptions中会包含UIApplicationKeyLaunch,当被杀死的APP在后台被系统唤醒时**字段来识别,然后我们重新启动定位功能,完成满足我们需求的定位功能。为此,我写了一个demo来验证(使用模拟器,选择Debug->Location->FreewayDrive)结果如下接下来,我们将讨论几个相关的问题,讨论为什么不使用定时器来控制定位间隔。网上有很多教程都是用NSTimer实现的,但其实都不是很好。虽然定位间隔是固定的,但是无法解决功耗问题。无论当前位置是否更新,后台都会持续更新定位。当然,如果你的使用场景是每隔一段时间。上传的时间到了,可以用定时器来处理。使用distanceFilter来处理一些问题。由于distanceFilter=currentSpeed*minInteval,间隔时间会因为速度变化而出现波动,但这种波动在可以接受的范围内。如果速度增加或者变慢了,下次更新的时间会相应的缩短或者加长,但是因为我们是在现实环境中,速度不可能变的那么快,所以这个误差是可以接受的。另外,我们针对速度修正了distanceFilter,所以整体的区间还是会保持在我们的范围内。为什么不用allowDeferredLocationUpdatesUntilTraveled:timeout:allowDeferredLocationUpdatesUntilTraveled是iOS6引入的新API。看名字就知道这个函数的作用是延迟位置更新,直到移动xx米或者时间超过xx秒,这个函数不正好满足我们所有的要求吗?但万万没想到,事实并非如此。这个功能并不容易使用。接下来是吐槽时间?(????)为什么这个功能不好用?首先,这个功能有很多要求。让我们来看看这个功能需要满足什么条件才能发挥作用。desiredAccuracy必须设置为kCLLocationAccuracyBest或者kCLLocationAccuracyBestForNavigationdistanceFilter必须设置为kCLDistanceFilterNone,只有APP在后台运行时才会生效。只有当系统处于低功耗状态(LowPowerState)时才不会进行延时处理关于iOS中LowPowerState的描述,我在苹果官网的文档中只找到了部分定义。iOS非常擅长让设备在不使用时进入低功耗状态。闲置时,消耗的功率非常少,能量影响也很低。当任务正在积极发生时,系统资源正在被使用,而这些资源需要能量。但是,零星的任务可能会导致设备进入中间状态——既不空闲也不活动——当设备不做任何事情时。在这些中间状态期间,可能没有足够的时间让设备在下一个任务执行之前达到绝对空闲。发生这种情况时,能源会被浪费,用户的电池会消耗得更快。根据我对这个“**LowPowerState”的简单理解只有在黑屏(不只是锁屏)时才会触发。只要在电量屏上有任何操作(甚至是push),APP就会退出这个状态。同时,如果是充电状态,是进不去的。我试过我在真机和模拟器上都用了这个API,结果APP还是定位在1HZ的频率(设置kCLDistanceFilterNone的原因)虽然在指定时间后成功回调了locationManager:didFinishDeferredUpdatesWithError:,但是结果还是没有deffer。于是查了下发现这个函数不能直接调试,原因是:不支持模拟器deferredLocationUpdatesAvailable用于检测设备是否支持模拟器,返回NO。不支持真机调试,因为Xcode在调试时会阻止程序休眠,程序无法进入低功耗状态。结论是……这东西连调试都调试不了,所以我也没有那么多时间去外面测试这东西……再说了,用我上面说的方法基本可以满足需求。..所以我已经放弃继续研究这个API了,因为即使我用了这个东西,也只是锦上添花。如果有谁知道如何正确使用这个东西,请留言告诉我,万分感谢!