iOS 停止后台位置更新是因为它挂起了你的应用,而不是因为 Core Location 失败。除非应用声明了位置后台模式,并在其 CLLocationManager中启用后台更新,且在应用仍处于前台时启动这些更新,否则 iOS 会在用户离开后几秒内冻结进程,并排队或丢弃定位数据。本指南列出了我们在 Capacitor 应用中看到的每一个原因及其修复方法,并展示了 @capgo/background-geolocation 如何处理这些问题。
iOS 如何处理进入后台的应用
当用户滑动到主屏幕或锁定手机时,iOS 会给应用一个短暂的窗口期,然后将其挂起。被挂起的应用无法获得 CPU 时间。对于 Capacitor 应用而言,这意味着原生端和 WebView 都会停止运行,因此你的 JavaScript 回调也会停止触发。
Apple 为正在积极使用位置的应用提供了例外。如果应用配置了后台位置,并且在前台启动了活跃的位置会话,iOS 会保持其运行并继续提供更新。以下内容均关于如何满足该契约,以及系统故意破坏该契约的情况。
原因 1:缺少后台定位模式
如果不使用 location 中的 UIBackgroundModes,无论使用多少 code 都无法保持更新流动。请将其添加到 ios/App/App/Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
你也可以在 Xcode 的 Signing & Capabilities > Background Modes > Location updates 中启用它。这是一个 plist 键,而非 entitlement,因此无需更改 provisioning profile。
其背后的原生属性是 allowsBackgroundLocationUpdates。将其设置为 true 如果 plist 中缺少后台模式,应用会崩溃。当你传入 Capgo 时,插件会自动为你设置该属性 backgroundMessage 改为 start(),因此发布前必须确保 plist 中包含该键。
App Review 会检查后台模式是否有正当理由。请在审核备注中说明哪个功能使用了该模式,并确保该功能对审核人员可见。
原因 2:更新是在后台启动的
Apple 的规则:如果会话是在应用处于前台时启动的,后台更新将继续进行。尝试从静默推送、后台 fetch 或 BGTaskScheduler 任务启动会话,对于标准位置更新是无效的。
从用户操作或应用恢复时开始跟踪:
import { App } from '@capacitor/app';
import { BackgroundGeolocation } from '@capgo/background-geolocation';
let tracking = false;
export async function startTracking() {
if (tracking) return;
tracking = true;
await BackgroundGeolocation.start(
{
backgroundMessage: 'Recording your route',
backgroundTitle: 'Route recording',
requestPermissions: true,
distanceFilter: 10,
url: 'https://api.example.com/v1/points',
},
(location, error) => {
if (error) return console.error(error);
if (location) console.log(location.latitude, location.longitude);
},
);
}
// Re-arm when the user comes back, if the session should be active
App.addListener('appStateChange', ({ isActive }) => {
if (isActive && shouldBeTracking()) startTracking();
});
declare function shouldBeTracking(): boolean;
原因 3: backgroundMessage 未传入
这是 Capgo 插件特有的问题,也是最常见的 bug 报告。如果 backgroundMessage 未定义,插件将仅配置位置管理器用于前台使用: allowsBackgroundLocationUpdates 保持 false ,当应用挂起时更新就会停止。请在两个平台上都传递消息。在 Android 上,它还会成为前台服务通知的文本。
原因 4:授权级别错误,或选择了“仅一次”
许多开发者认为需要“始终”权限才能在后台追踪。其实不然。Apple 文档指出,拥有“使用期间”授权的应用,只要启用了后台更新,在拥有活跃位置服务时就会继续在后台运行。
“始终”权限对另一件事很重要:重启已终止的应用。只有区域监控、显著位置变化和访问记录才能重启应用,且必须拥有“始终”权限。
两个用户选择会悄悄破坏后台追踪:
- “仅一次”。 这是一种临时的“使用期间”授权,当应用停止使用时即过期。下一次会话将从“未确定”状态重新开始。
- 先请求“使用期间”,然后“始终”提示从未显示。 当你请求“始终”权限时,iOS 可能会稍后显示升级提示,时机由系统决定,通常是在应用不在屏幕上时。
在启动会话前,请先检查实际状态:
import { BackgroundGeolocation } from '@capgo/background-geolocation';
const status = await BackgroundGeolocation.checkPermissions();
if (status.location !== 'granted') {
await BackgroundGeolocation.requestPermissions({ permissions: ['location'] });
}
if (status.backgroundLocation === 'when_in_use') {
// Background tracking still works for standard updates.
// Explain why Always helps if you also rely on geofences.
}
在 iOS 上, backgroundLocation 返回 when_in_use 或 granted / always,这样你就能显示正确的说明,而不是靠猜。
原因 5:Core Location 自动暂停更新
CLLocationManager.pausesLocationUpdatesAutomatically 默认为 true。当 iOS 判断用户没有移动时,会暂停更新以省电。对于 When In Use 应用,暂停会中断位置访问,直到应用再次启动,而且 Core Location 不会自行恢复。
Capgo 插件会设置 pausesLocationUpdatesAutomatically = false 其原生 code 中的该项,因此这一项已经处理好了。如果你自己编写原生 code,或使用其他插件,请先检查这个属性。
原因 6:设备静止且过滤范围过大
使用 distanceFilter 50 米的过滤精度,放在桌上的手机不会产生任何更新。这在你 logs 里看起来像“追踪已停止”,但其实是按设计工作的。
- 如果你需要在静止时也有心跳,请使用较小或为零的
distanceFilter. - 使用
minIntervalMs限制原生 POST 请求的发送频率,而不是设置很大的距离过滤器。
await BackgroundGeolocation.start(
{
backgroundMessage: 'Vehicle tracking active',
distanceFilter: 0,
minIntervalMs: 120_000, // at most one native POST every 2 minutes
url: 'https://api.example.com/v1/vehicle-points',
},
() => {},
);
原因 7:精确位置已关闭
自 iOS 14 起,用户可以关闭应用的“精确位置”。你仍能收到更新,但位置是近似的(误差数公里),且频率低得多。可通过数据检测到这一点(数值很大 accuracy )并向用户解释精确位置对你的功能为何重要。如果你开发的是配送或健身应用,请在 onboarding 阶段说明,而不是等行程出问题后才提示。
原因 8:用户终止了应用
如果用户在应用切换器中划掉应用,iOS 会停止标准位置更新。没有任何 plugin 能让它继续运行。对于从 Android 转过来的团队来说,这通常很令人意外,因为在 Android 上,即使任务被移除,前台服务仍可继续运行。
选项:
- 告知用户在会话期间不要强制退出应用。大多数车队应用都会在 onboarding 中提示这一点。
- 在重要地点周围添加带 Always 授权的地理围栏 (geofences),以便 iOS 能在用户进入或离开时在后台重新启动应用。
- 当你的服务器停止接收某个会话的点位时,发送 push 提醒用户重新打开应用。
原因 9:原生层在运行,但 JavaScript 未运行
这个问题比较隐蔽。当 WebView 处于挂起状态,或者虽然还活着但处理定时器或网络请求不够及时时,原生 code 仍能收到位置。如果你用 fetch 来自回调的数据可能延迟到达,甚至完全不到达。
使用插件的 url 选项。每个点位都由原生 code 以与以下相同的 JSON POST: Location 类型加上 "source": "native",且独立于 WebView。回调仅用于 UI 更新。投递采用尽力而为机制且没有磁盘队列,所以如果某个点位不能丢,请在回调触发时也存到本地,之后再对账。
如需了解位置以外的后台工作,请阅读 后台任务在 Capacitor 中如何工作 并查看 @capgo/capacitor-background-task.
原因 10:低电量模式与热状态
低电量模式会降低整个系统的后台活动。定位仍然可用,但你的会话依赖的其他环节(网络重试、后台刷新)会变慢。在发布前,请务必开启低电量模式测试。
蓝色指示器
当获得“使用 App 期间”授权的 App 在后台使用定位时,iOS 会在状态栏显示蓝色胶囊。Capgo 插件会设置 showsBackgroundLocationIndicator 改为 true 对于后台会话,这是用户期望的,也是审核人员会关注的。不要试图隐藏它,用户信任它。
能够捕获这些 Bug 的测试方案
对于后台定位,仅靠模拟器测试是不够的。请使用真机 iPhone 并执行以下操作:
- 构建 Release 配置,而不只是 Debug。挂到 Xcode 上的 Debug 构建,生命周期处理方式不同。
- 在应用处于前台时启动会话,然后锁定手机,步行或驾车 20 分钟。
- 分别在“允许一次”、关闭“精确位置”、开启“低电量模式”的情况下重复测试。
- 检查你的服务器日志中的
source: "native"数据点,而不只是应用内日志。 - 在会话期间强制退出应用,并确认应用在下次启动时能解释发生了什么。
如果你需要设备 UDID 用于 ad hoc 构建, iOS UDID 查询工具 从 Safari 获取。如果你没有配备 Xcode 26 的 Mac 用于构建, Capgo 构建 可以在云端生成已签名的 iOS 构建。
故障排查清单
| 现象 | 可能原因 | 修复 |
|---|---|---|
| 锁屏后几秒即停止 | 否 location 后台模式,开还是不开? backgroundMessage |
添加 plist 键并传入 backgroundMessage |
| 启动时崩溃 | allowsBackgroundLocationUpdates 未开启后台模式 |
添加 UIBackgroundModes → location |
| 仅生效一次,随后失效 | 用户选择了“允许一次” | 解释原因并再次请求,或发送至 openSettings() |
| 停车时停止 | 较大 distanceFilter |
调低它,然后使用 minIntervalMs |
| 坐标点间距过大,精度低 | 未开启“精确位置” | 提示用户启用 |
| 划掉应用后停止 | 用户终止了应用 | iOS 上属预期行为,请用地理围栏重新启动 |
| 原生端能获取数据点,服务端不能 | JavaScript fetch 在回调中 |
使用 url 选项 |
发布修复
这些修复大多涉及 Info.plist 或原生配置,因此需要新的 App Store 构建。JavaScript 部分,如权限说明、onboarding 文案和重试逻辑,可以通过 Capgo 实时更新 在原生构建发布后推送给现有用户。
如果你正在比较插件,我们的 Transistorsoft 与免费 Capgo 插件对比 涵盖了其中的权衡。