Android 的 Foreground Service 类型限制,后台定位还能跑吗
Android 的 Foreground Service 类型限制,后台定位还能跑吗
最近把项目里的 targetSdkVersion 升到 34,测试机上的定位服务直接崩了。不是那种优雅的回调失败,而是系统层面直接抛 ForegroundServiceDidNotStartInTimeException,整个应用被杀死,通知栏甚至连个报错提示都不给。查了半天 logcat,看到那一行 Service.startForeground() not called in time 的时候,我才意识到 Google 在 Android 14 里又把前台服务的脖子掐紧了一圈。以前那种在 onStartCommand 里先干活、慢悠悠再调 startForeground() 的写法,现在直接成了送命题。后台定位这件事,从 Android 8.0 开始说了六年,越说越严,到现在已经成了一团理不清的乱麻。
Android 14 的 `targetSdkVersion 34` 到底改了什么
很多人以为前台服务类型(foregroundServiceType)是在 Android 12 里定死的,其实那只是开胃菜。真正让人头疼的是 Android 14 对前台服务启动时效的强制执行。你的应用如果 target 到 34,调用 Context.startForegroundService() 之后,必须在五秒内调用 startForeground(),而且这次调用必须带上对应的 ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION。超时就是 RemoteServiceException,没有任何商量余地。
更阴间的是权限矩阵的变化。以前你只需要在清单文件里声明 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION,然后在运行时动态申请。现在如果你要用前台服务做定位,必须额外加上 <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />,而且 Android 14 要求这个权限必须是用户明确授予的,不能假设自动获得。如果你漏了这一步,系统在启动服务时直接报 SecurityException: Permission denied: starting Intent { ... } requires android.permission.FOREGROUND_SERVICE_LOCATION。
这还没完。Google 在 Android 14 的兼容性文档里埋了一个细节:如果你的应用使用了 location 类型的前台服务,系统会在后台对服务的存活时间做更激进的限制。具体来说,一旦系统认为用户没有"持续与定位相关的交互",你的前台服务通知可能会被降级,甚至在某些内存压力下被直接移除。这等于说,前台服务这个"免死金牌"在定位场景下已经没那么好使了。
前台服务类型声明的坑远比文档写的深
Android 12 引入 android:foregroundServiceType 属性的时候,官方文档的示例写得轻描淡写,好像只要在 <service> 标签里加一行 android:foregroundServiceType="location" 就万事大吉。实际根本不是这么回事。
| 第一个坑是组合类型。很多应用不只是定位,可能还要蓝牙扫描或者麦克风录音。你以为可以写 `android:foregroundServiceType="location | microphone"`,确实可以这么写,但 Android 14 要求你在 `startForeground(int id, Notification notification, int foregroundServiceType)` 方法调用时,传入的类型必须是清单声明的子集。如果清单里没声明 `microphone`,代码里却传了,直接崩溃。如果清单里声明了,但用户没给对应的运行时权限,服务启动时同样抛异常。这种设计让动态功能开关变得极其恶心,你必须在服务启动前就把所有可能的类型枚举清楚,完全没有运行时灵活调整的余地。 |
|---|
第二个坑是通知渠道(Notification Channel)的耦合。定位类前台服务必须挂一个持续通知,这是老规矩了。但 Android 14 开始,如果你的通知渠道被用户关闭,或者用户选择了"最小化通知",系统有权拒绝你的 startForeground() 调用,或者直接把服务优先级降到普通后台。对于定位类应用,这意味着用户随手点一下通知设置里的"关闭",你的后台定位就断了,而你作为开发者甚至收不到一个明确的回调,只能通过 onTrimMemory 之类间接猜测。
第三个坑是 ACCESS_BACKGROUND_LOCATION 这个权限的叠加。很多人混淆了前台服务定位和后台定位。严格来说,只要你的服务带着 FOREGROUND_SERVICE_TYPE_LOCATION 并且通知栏挂着,系统认为你属于前台定位,不需要 ACCESS_BACKGROUND_LOCATION。但一旦你的服务因为任何原因被杀死、或者用户划掉了通知(在 Android 13 以下某些 OEM 系统里这是可能的),应用退到后台后还想继续拿位置,就必须要有后台位置权限。而申请这个权限在 Google Play 的审核政策里是地狱难度,Google 要求你展示一个全屏的、单独的、说明为什么必须在后台获取位置的对话框,而且理由不够"核心功能"直接拒审。
Google 自家应用的特权与开发者的双标困境
聊到这里就不得不说那个老生常谈的话题:规则是给你们定的,不是给 Google 定的。你可以拿一部原生 Android 14 的 Pixel 手机,打开系统自带的 Google Maps,然后开始导航。退出应用,甚至清掉后台卡片,导航语音还在响,通知栏的箭头图标稳如老狗。你写个第三方导航应用,用完全一样的 API、完全一样的 foregroundServiceType="location",清个后台试试?很大概率服务直接没了,或者被系统延迟重启,中间丢个十几秒的定位数据。
这不是阴谋论,是 Android 的"电源管理白名单"机制在作祟。系统应用和预装应用通常被加入 device_idle 和 app_standby 的白名单,普通应用想走 Requesting ignore battery optimizations 这个口子,Google Play 的审核团队会瞪大眼睛审查你。更隐蔽的是,Google Play 服务(GMS)本身持有大量普通应用无法申请的签名级权限,比如 android.permission.CONTROL_LOCATION_UPDATES 和 android.permission.LOCATION_HARDWARE,这让它可以在不挂前台服务通知的情况下维持定位管道。普通开发者看着 LocationManager.requestLocationUpdates() 的文档,再对比系统日志里 GMS 的行为,只能苦笑。
还有 Find My Device。这个应用可以在设备锁屏、后台被清空、甚至网络切换的情况下上报位置,靠的是系统级权限和特殊的应用 ID 白名单。第三方做儿童手表、老人防走失、宠物追踪的开发者,哪怕功能逻辑完全一致,也只能在 OEM 的权限管理夹缝里求生存。华为 EMUI 的"后台保护"、小米 MIUI 的"自启动管理"、OPPO ColorOS 的"耗电异常自动清理",每一层都是一道鬼门关。
OEM 厂商的"二次创新"让标准成了摆设
如果说 Google 的限制还有文档可查,那国内各大 OEM 的定制就是纯黑箱。小米在 MIUI 12 以后引入了一个叫"隐私保护"里的"返回空信息"功能,对定位类应用影响极大。你的前台服务明明在跑,通知也挂着,但系统给应用返回的位置数据是空的或者是缓存的旧数据,应用层面还拿不到任何错误回调,看起来就像 GPS 信号不好。
华为的 EMUI 和 HarmonyOS 更有意思。它们把前台服务分成"活跃前台服务"和"普通前台服务"两档。即使你的通知显示在状态栏,如果系统判定用户"没有正在查看通知",你的服务可能被归入普通档,CPU 调度优先级大幅下降,导致 FusedLocationProviderClient 的回调间隔从设定的 5 秒变成 30 秒甚至两分钟。你在代码里设的 setInterval(5000) 和 setFastestInterval(2000),在华为手机上就是一张废纸。
OV 系(OPPO、vivo、一加、realme)的杀后台策略更是江湖闻名。它们不看你有没有前台服务通知,而是看一个自己维护的"耗电排行榜"。定位服务因为持续使用 GPS 和移动网络,天然排在耗电前列,系统会在某个不知名的深夜或者低电量场景下,直接发送 SIGKILL 把你的服务干掉。更绝的是,有些版本不会触发 onDestroy() 或 onTaskRemoved(),你的应用连个遗言都留不下。
为了对抗这种环境,国内开发者发明了一套极其丑陋的保活组合拳:双进程守护、JobScheduler 周期性拉活、AlarmManager 整点唤醒、AccountManager 同步触发、甚至还有利用播放无声音乐维持媒体前台服务的邪道。Android 14 这次收紧前台服务类型,等于把这些歪门邪道也堵死了一大半,因为 FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK 和 FOREGROUND_SERVICE_TYPE_LOCATION 的审核标准完全不同,你不能假装自己是音乐播放器来偷跑定位。
WorkManager 不是银弹,FusedLocationProvider 也在装傻
Google 的官方建议是把后台任务迁移到 WorkManager。对于定位这种需要实时、持续、高频上报的场景,WorkManager 完全是个笑话。PeriodicWorkRequest 的最小间隔是 15 分钟,而且受 Doze 模式和 App Standby 的影响,实际执行间隔可能拖到一小时以后。你要是用 OneTimeWorkRequest 搞链式调用来模拟持续定位,电量消耗比前台服务还高,因为每次唤醒都有冷启动开销。
那用 FusedLocationProviderClient 的后台定位接口呢?在 Android 10 以上,这个接口在没有前台服务的情况下,返回的频率会被系统严重限制。Google 的官方说法是"为了用户隐私和电池寿命",但实际测试下来,在很多设备上后台定位的精度直接从 GPS 级别掉到了网络级别,误差从几米变成几百米。对于需要记录运动轨迹的跑步 App 或者需要实时派单的即时配送系统,这种精度衰减是不可接受的。
有些开发者尝试用 BroadcastReceiver 监听 BOOT_COMPLETED、CONNECTIVITY_CHANGE、ACTION_USER_PRESENT 来重新拉起服务。Android 14 对这些隐式广播的限制也加强了,很多接收器在 targetSdk 34 下根本收不到。你能用的只剩下 JobScheduler 和 AlarmManager 的精确闹钟(SCHEDULE_EXACT_ALARM),而后者在 Android 14 里又要求 USE_EXACT_ALARM 权限,Google Play 审核对这个权限的把控严到变态,非闹钟类应用基本申请不下来。
被忽视的真实业务场景
Google 在做这些限制的时候,总喜欢拿"恶意应用后台跟踪用户"来说事。但问题是,这套铁拳砸下来,死得最惨的是那些正当业务。
物流行业的司机端 App 需要在后台持续上报位置,这样调度中心才能知道货车到哪了。以前一个前台服务就能解决的事情,现在要在 Android 14 上稳定运行,需要申请前台服务权限、后台位置权限、通知权限、电池优化白名单,还要针对小米、华为、OV 分别写保活策略。一个定位功能的工作量可能占到整个项目开发的三分之一。
户外运动类 App 比如 Strava、Keep、悦跑圈,用户明明开启了跑步记录,锁屏放口袋里,期望应用持续记录轨迹。Android 14 的某些行为变更导致前台服务的通知被用户误划后,服务停止,后半段轨迹直接消失。用户不会骂 Google,用户只会骂开发者:"你们 App 又飘了,跑十公里只记了三公里。"
共享单车的运维人员需要找车、挪车,他们的工作流就是骑一辆三轮,后台开着定位,系统按轨迹算工作量。这种蓝领场景下,手机型号杂、系统版本旧、用户对权限设置一无所知,每多一个弹窗就多一层流失。Google 在 I/O 大会上展示的"用户隐私仪表盘"看起来很酷,但那些在太阳底下干活的人只想知道为什么今天 App 又闪退了。
现在的 workaround 有多难看
为了绕过这些限制,业界的 workaround 已经发展到了一种荒诞的地步。我见过最离谱的方案是:应用启动一个真正的前台服务播放一段无声的 MP3,利用 FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK 的高优先级来维持进程存活,同时在另一个线程里偷偷跑定位。这种方案在 Android 14 上风险极高,因为 Google Play 的审核算法会扫描 APK 里的音频资源和前台服务类型的匹配关系,一旦发现你声明了媒体播放却没有实际的媒体播放 UI,直接下架。
另一个方向是走无障碍服务(AccessibilityService)。这个权限的级别极高,系统基本不会杀。有些应用把定位逻辑绑定在无障碍服务里,用户需要手动去系统设置里开启。这不仅用户体验极差,而且 Google Play 对无障碍服务的审核已经收紧到近乎苛刻,非真正辅助功能类应用基本不可能过审。
还有些企业级应用直接走 MDM(移动设备管理)路线,要求用户把设备纳入公司管辖,通过 DevicePolicyManager 设置应用为豁免状态。这本质上已经放弃了面向普通消费者的 C 端市场。
对于中小开发者,最现实的方案可能是:放弃原生前台服务,转而接入高德、百度或 Google 的定位 SDK,让这些大厂去和 OEM 厂商做适配。但这又带来了新的问题——这些 SDK 为了兼容性,往往采用保守的采样策略,而且把位置数据先过一遍自家的服务器,对数据隐私敏感的用户和企业根本无法接受。
那后台定位到底还能不能跑
坦白说,如果你做的是一个需要持续后台定位的产品,并且面向普通消费者,现在的 Android 生态已经不是"能不能跑"的问题,而是"能跑多久"和"开发维护成本有多高"的问题。在 Pixel 这种原生系统上,严格按照 Android 14 的规范写,该申请的权限一个不落,前台服务类型声明准确,通知渠道配置正确,它确实能跑。但一旦到了国内厂商定制的 Rom 上,稳定性就成了一门玄学。
更讽刺的是,Google 一边用 ForegroundServiceType 和后台位置权限卡普通开发者,一边在自己的生态里保留特权通道。Android 的权限模型越来越像 iOS,但又没有 iOS 那种统一的管控力度和明确的审核边界。在 iOS 上,你申请 UIBackgroundModes 的 location,只要理由充分、审核通过,系统就会给你一个相对稳定的运行环境。而在 Android 上,你过了 Google Play 的审核,还要过小米、华为、OPPO、vivo 各自的应用商店审核,每一家的审核标准对定位权限的解释都不完全一样。
对于那些已经上线的应用,升级 targetSdkVersion 到 34 不再是一个可选的技术债,Google Play 从去年开始就在强制推动。这意味着大量旧代码里的定位服务需要重写,测试矩阵要覆盖 Android 10 到 14 的至少五个大版本,以及国内主流厂商的定制系统。一个定位模块的回归测试成本,可能抵得上半个新功能的开发。
我在想,当 Google 下一次 I/O 大会再吹"隐私保护"的时候,有没有人敢站起来问问:你们用来演示 PPT 的那台 Pixel 手机上,GMS 自己的定位服务到底声明了哪些前台服务类型?它的电池优化白名单是谁加的?如果普通开发者按照同样的逻辑写代码,能不能在 Google Play 过审?这个问题大概不会有答案。而我们能做的,就是在 AndroidManifest.xml 里再补一行权限,在 onStartCommand 里把 startForeground() 再提前几毫秒调用,然后对着 logcat 祈祷 OEM