iOS 定位的矛与盾
做后台定位这段时间,我主要在跟两样东西较劲:先是 iOS 本身,后来是 iOS 给我的数据。
我在做一个足迹 App,暂时叫“代号:地图”(什么游戏开发者取名):打开记录后,它全天在后台记下你走过的路,在地图上一点点揭开。要求就四条:不漏记,不乱记,不费电,状态栏上别一直挂着定位提示。听着不多,做起来才发现它们其实互相矛盾并且需要妥协。
标题是怎么来的
一开始我以为矛是 App、盾是 iOS。我想一天 24 小时拿到每一步的位置,系统要替用户看住电量和隐私,于是挂起进程、自动暂停定位、在灵动岛上提示“有 App 在用你的位置”。前半篇讲的就是怎么在它的规则里待住。
待住之后才发现还有后半场。系统每秒送来一个点,里面不少是假的:室内 Wi‑Fi 定位乱跳,手机躺在床头却报出每秒 25 米的速度。这回轮到我举盾,把假点挡在地图外面。
文里的数字都出自 2026 年 10 月上旬我自己手机上的日志。有几处苹果文档没写,或者写了但和我测到的对不上,会单独说。
先得在后台待住
最后留下来的配置就这么几行,每一行都是换过别的写法、吃了亏才定下来的。
manager.pausesLocationUpdatesAutomatically = false // 不让系统自动暂停
manager.showsBackgroundLocationIndicator = false // “始终允许”下不显示定位提示
manager.allowsBackgroundLocationUpdates = true
manager.distanceFilter = kCLDistanceFilterNone // 不设系统距离过滤
manager.startUpdatingLocation()
// 另一个 manager,只负责被杀掉后把 App 拉起来
wakeManager.startMonitoringSignificantLocationChanges()liveUpdates 用不了
CLLocationUpdate.liveUpdates 写起来干净,但它在后台一定显示定位提示。一个全天开着的足迹 App,状态栏常年挂着提示,用户迟早会去把权限关掉。
老的 CLLocationManager 在用户给了“始终允许”时,可以用 showsBackgroundLocationIndicator = false 关掉提示。只给“使用时允许”的话,系统一定会显示,这个没得商量。
distanceFilter 不能设
这条我是完全没想到的。给 distanceFilter 设 10 米,看上去是在帮系统省电,实际效果是 App 被挂起。
10 月 9 日的日志里能看到:设了 5~50 米过滤时,坐车途中 App 每次被显著位置变化唤醒,只记十来秒就又被挂起,下一次唤醒在大约 5 分钟后。一段车程在地图上只剩几截断开的短线。
苹果工程师在开发者论坛的说明是:同时开着显著位置变化时,要“不设距离过滤,加百米或更好的精度”才不会被挂起。所以现在系统每秒回调一次,隔多远记一个点完全由 App 自己筛。
自动暂停也得关
pausesLocationUpdatesAutomatically 默认是开的。系统觉得你不动了就停掉定位,要走出一段距离才恢复。我试过配一个 150 米围栏来恢复,结果等红灯会停,坐久了也会停,开车起步和楼下散步的头一段都丢了。
有的足迹 App 走的就是这条路:省电档在静止时直接停止后台运行,官方说明里也承认会漏记。我选了另一头,静止时只降精度,定位不停。
显著位置变化留着兜底
显著位置变化要移动 500 米以上才触发,最多约 5 分钟一次,位置误差几百米,拿来画线是不行的。留着它只为一件事:App 被系统杀掉之后,它能把进程重新拉起来。
所以它单独用一个 CLLocationManager,回调里只在日志记一笔“系统唤醒过”,粗略位置不进轨迹。还有一条配套的限制:连续定位要在前台开始,后台只有在被显著位置变化唤醒的那一刻开始才有效(苹果的说明)。
用户主动划掉 App 时,我把所有定位都停掉,推一条“足迹可能丢失”的通知。真机上见过系统之后仍把 App 拉起来接着记,但这件事不保证。
省电,但定位不能停
不设距离过滤、不让系统暂停,等于把系统给的两个省电开关都关了。剩下能调的只有两样:要多高的精度,以及拿到点之后做多少事。
让运动传感器决定精度
CMMotionActivityManager 会告诉你手机现在是静止、步行还是在车上,我按它切换定位方式。
坐车这一行有两个取舍。kCLLocationAccuracyBestForNavigation 是系统最费电的一档,苹果建议插着电用;.automotiveNavigation 会把位置吸到公路上,坐火车、地铁时反而会偏。
降档有没有代价,用同一条通勤路线对比过。10 月 10 日用导航精度,13.1 公里收了 264 个点;10 月 12 日用最高精度,12.9 公里收了 311 个点。两天的线横向只差 2 米(中位数),误差中位数都是 8 米,放大到街道级看不出差别。
这里踩过三个坑
静止要等两分钟才降档。 等红灯、地铁停站都会让传感器报静止,马上降精度的话,起步那一段就糊了。
“静止够久”要靠定位回调来检查。 最初的写法只在传感器换状态时检查:第一次报静止还不到两分钟,之后状态不变就再没检查过,结果一直没切到省电。现在每个定位回调都检查一遍。
静止时报的“未知”别当真。 10 月 10 日一整天,传感器从静止跳到未知 475 次,中位持续 16 秒,九成又回到静止,基本就是手机被拿起来看了一眼。如果每次都升回最高精度,两分钟的计时就永远数不完。
回调里少干活
每秒一次的回调本身不贵,贵的是回调里做的事。App 不在前台时,我攒够 100 个点才重算当天的折线;退到后台时把地图上的蓝点也关掉,免得 MapKit 自己再开一路定位。
耗电
这部分我还没有拿得出手的数。10 月 11 日一次往返车程,我这边掉了 7%(亮屏 23 分钟),同时开着的一个同类 App 掉了 4%(亮屏 1 分钟),后台部分估计持平。问题是两个 App 同时开着时,系统会把最高的那份定位发给两边,比不出各自的档位。
夜里静止 8 小时掉电 10%~15%,而且电量只记到 5% 一档。“日常 10% 以下”这个目标还得各单独跑一天才能下结论。
系统给的点不能全信
待住之后,CLLocation 每秒来一个。收不收,我的标准是宁可少记,也不要怪线:少一个点没人看得出来,地图上多一条斜穿小区的直线,一眼就看见了。
室内的误差靠不住
10 月 10 日,我在办公楼里坐了一天,App 收了 24 个点。这些点全都没有卫星速度(speed < 0),自称误差 15~30 米,实际乱跳 20~60 米。
最离谱的一次,位置跳出 158 米,自称误差 22 米,在那边停了 10 分钟才跳回来,地图上多了一条斜线。室内靠 Wi‑Fi 和基站定位时,系统报的误差明显偏乐观。
每个点怎么筛
每个样本和上一个收下的点比较,按顺序过这几关:
误差门槛:误差 100 米以内照常处理;100 米到 2 公里的只在坐车时收(地铁、隧道里只有基站定位),记成“粗略点”,只连线、不揭开地图。
间隔随速度变:
max(10, min(speed × 3, 200))米。步行约 10 米一个点,开车最多 200 米一个点。移动距离要超过自己的误差:否则可能只是漂移。室内那一团乱线就是这么来的。
没有卫星速度的点要求更严:要和上一个点的误差范围完全不重叠(距离大于两点误差之和)才收。
最管用的是第 4 条。speed 是负数,说明这个点不是卫星算出来的,它的自述误差就要打折扣。
跳出去的点先当候选
Wi‑Fi 定位有个习性:跳到一两百米外,停住不动,过一阵再跳回来。真的走开了的人不是这样,他会越走越远。
所以传感器报静止时,挪出 100 米的点不直接收,先记作候选。卫星测出在动(比如坐在很稳的车上)就直接收;否则要等 2 分钟内又走远 50 米才算数。
半夜每秒 25 米朝正北
这个最怪。10 月 12 日凌晨两次、13 日凌晨一次,手机躺着没动,系统却报出每秒 25 米、朝正北的速度。
麻烦的是运动传感器也被带偏了,跟着改口报“坐车”,位置被照着这个速度直线推出去。13 日那次是 36 秒推出 624 米,然后弹回原地。苹果为什么会这样,没查到。
13 日那次我已经在原始样本里多记了几列,这才看出不对:22 个带速度的样本里,20 个缺速度误差或方向。而 10 月 12 日真开车、步行的 3219 个带速度的样本,这几项全都有。
/// 报了 1 米/秒以上的速度,却没同时给出速度误差、方向、方向误差:这个速度是假的
static func speedTrusted(speed: Double, speedAccuracy: Double,
course: Double, courseAccuracy: Double) -> Bool {
speed < 1 || (speedAccuracy >= 0 && course >= 0 && courseAccuracy >= 0)
}不可信的点直接不收。拿 13 日的样本重放,原来收进来的 6 个假点一个不剩;拿 12 日全天重放,路上的点一个没少,只少了上车前车库里两个误差三四十米的点。
另一个坑:加偏坐标
手机在大陆时,苹果地图的底图、搜索、反向编码用的都是加偏坐标(GCJ‑02,偏 250~620 米),而 CLLocationManager 给的是真实坐标。苹果不告诉 App 当前是否加偏。
我的做法是比较地图蓝点和原始定位:差出几百米就判定在加偏,之后渲染和进出苹果服务的坐标都换算。轨迹库里始终存真实坐标。
停下来之后
一个点一个点地筛,对付不了“在家待一天”。10 月 11 日我在家一天,App 收了 39 个点,散在直径约 100 米的范围里,连成一团乱线。
原因是保护松得太容易。在家起身走十几秒,传感器报了“步行”,静止的保护一松,室内 Wi‑Fi 定位乱跳的点就全放进来了。
所以在逐点过滤上面又加了一层“停留”。进入停留的条件放得很宽,离开则必须有位置上的证据。
[embedded content: 停留的三个状态 · 进入宽松,离开要证据]
停留期间一个点都不记;要离开,先当候选,后面的点继续走远了才算。
传感器只在说“停了”时可信
传感器说“停了”,可以信,信错了的代价只是少记几个点。传感器说“走了”,不能信:
报步行不解除停留。在家走动和真的出门,传感器分不出来。
报坐车也不解除。上一节那个假速度会带着传感器一起改口,真坐车离开时位置自然会越走越远。
确认需要时间,所以要补记
“走远 30 米,再走远 10 米”才算离开,意味着确认时人已经走出几十米了。如果这段直接丢掉,每次在路口、景点停一下再走,线都会缺一截开头。
所以停留期间虽然不记点,但留着最近 2 分钟里卫星测出在动的样本。一旦确认离开,把这一小段按正常步行间隔补记回去。
两种特殊情况
App 重新启动。 以前启动后的第一个点无条件收下,正好把一个室内漂移点钉在地图上。现在接着数据库里最后一个点判断:还在它 100 米内,就直接算停留。
醒来已经在别处。 App 被挂起了一路,下一个样本在 1 公里外。这时不等确认,直接算离开。
效果
用四天的真机数据重放:停留处没有卫星速度的点从 82 个降到 11 个,路上的点不变。
10 月 12 日实测:在公司的 10:38–18:26 收了 0 个点;18:26 步行离开,补记了 5 个点,线的开头完整。绕公司走两圈,103 个点、1.5 公里,第二圈和第一圈横向只差 2 米。
没解决干净的:到达和出发的那一两分钟,还会漏进一两个没有卫星速度的点,离实际位置二三十米。
这些规则是怎么试出来的
上面这些都是盯着真机日志一条条看出来的。后台定位没法用模拟器调:挂起、漂移、假速度,模拟器里一样都不会发生。所以我在 App 里埋了两份日志。
记录日志:用心跳发现挂起
每个收下的点记一行,状态切换、停留开始和结束、电量各记一行。丢掉的点每秒都有,不逐条写,汇总进每分钟一行的心跳:
心跳 · 后台 · 坐车(最高精度)· 回调 58 次,收 12 丢 46(最近丢:…)· 最新样本 1 秒前心跳用的是普通 Timer,只在进程活着时触发。所以心跳断了,就是 App 被挂起了,前面 distanceFilter 那个问题就是这么发现的。回调到得晚(样本时间戳比现在早 10 秒以上)另记一笔,用来区分“系统攒了一批才送”和“系统根本没送”。
原始样本:让规则可以重放
只看收下的点,调不了过滤规则,因为你不知道改完之后会误伤什么。所以每次定位回调都往 CSV 里写一行,被丢掉的也写:每秒一行,一天 3~4 MB,保留 7 天。
过滤和停留的逻辑全在一个与平台无关的 Swift 包里,不依赖 CoreLocation。电脑上的重放工具用的就是 App 里那套代码:拿一天的原始样本过一遍,报出收几个点、和手机上当时差在哪。
假速度那条规则就是这么定下来的。第一次出现时日志里只有速度,看不出它和真速度有什么区别;于是给样本加了速度误差和方向两列,第二天凌晨它再出现,一眼就能分出来。
数据收齐再改
另外我给自己定了个规矩:不零敲碎打地调数值。通勤、在家和公司、旅行式实测三类场景的数据收齐,怪现象按原因归类,每类给改法,再用同一批样本重放前后对比。
到写这篇文章时,通勤和停留已经验证过。地铁、商场、走走停停的旅行式实测还没做,定位失效后的自动检查和恢复也还没做。后者可以参考两个开源项目:LocoKit2(双 manager、陈旧定位检测和重启退避)和 Overland(系统不过滤、回调里自己筛)。
最后
回头看,这些坑有不少在苹果文档里找不到,是从开发者论坛的回帖和自己的日志里拼出来的。如果你也要做全天后台定位,我只有一个建议:第一天就把原始样本全记下来。规则迟早要改,到时候手里有没有数据是两回事。