深入 Android Geofence:从注册链路到 Doze 触发失灵的全链路解析
做位置提醒功能时踩过一个坑:测试机放在桌面上,模拟走出围栏,App 却迟迟收不到回调,有时几分钟后才触发。排查下来问题不在代码,而在围栏的触发链路和 Doze 省电策略纠缠在了一起。这篇文章把围栏从注册到触发的全链路捋一遍。
Geofence 的本质:订阅事件,不是轮询
Geofence(地理围栏)解决的是「当设备进入、离开或停留在一片圆形区域时通知 App」。它和「持续定位(Location Updates)」有本质区别:持续定位是 App 主动向系统要位置,围栏是 App 订阅事件,由系统在合适的时机回报。围栏省电的关键正在于此——大部分时间它不做定位,只在跨过边界那一刻通知你。
Android 端通过 GeofencingClient 使用,它是 Google Play Services 提供的高层封装:
val geofencingClient = LocationServices.getGeofencingClient(context)
val geofence = Geofence.Builder()
.setRequestId("office")
.setCircularRegion(31.23, 121.47, 200f)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(
Geofence.GEOFENCE_TRANSITION_ENTER or
Geofence.GEOFENCE_TRANSITION_EXIT
)
.setNotificationResponsiveness(5 * 60 * 1000)
.build()
围栏注册后,系统可以杀死 App 进程,由 Play Services 进程负责监听。这是围栏相比自行轮询最大的优势:省电且可靠。
注册链路:PendingIntent 是回调通道
围栏回调的通道只有一条:PendingIntent。区别在于你把这个 Intent 指向哪里——BroadcastReceiver、Activity 还是 Service。我习惯用 getBroadcast 接一个 Receiver:Receiver 没有 UI、生命周期短,适合做事件入口,App 进程被杀后系统也能拉起它;Activity 和 Service 则会受后台启动限制,不划算。
val intent = Intent(context, GeofenceReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
geofencingClient.addGeofences(
GeofencingRequest.Builder()
.addGeofence(geofence)
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.build(),
pendingIntent
).addOnSuccessListener { /* 注册成功 */ }
setInitialTrigger 决定注册时若设备已在围栏内,是否立即触发 ENTER,默认不触发。setNotificationResponsiveness 设置的是围栏事件通知的响应延迟阈值(毫秒):它告诉系统「事件发生后我能接受多长的通知延迟」,值越大,系统越倾向批量处理位置更新、延后上报,从而省电;值设为 0 表示尽快响应上报,但这只是缩短了「已检测到穿越」到「通知 App」之间的延迟,不等于让 GPS 常开或让系统进入持续定位模式——底层用什么定位源采样,仍由系统按围栏半径、设备能力等因素综合决定。
触发机制:融合定位在背后干活
围栏底层靠融合定位(Fused Location Provider)实现。Play Services 融合了 GPS、Wi-Fi、基站、传感器多个来源,按场景动态切换。
围栏判定分两类路径:
- 硬件围栏(Hardware Geofence):部分芯片(高通、三星等)支持,位置计算在基带或协处理器里完成,AP 可以睡眠,功耗最低。
- 软件围栏(Software Geofence):Play Services 定期获取定位,在进程内计算是否穿越边界,功耗较高。
开发者没法指定走哪条路径,系统自动选择,且这个选择不是一次性锁定、也不是「固定半径对应固定定位源」的静态映射——融合定位会结合围栏半径、期望延迟、设备当前状态动态调整定位源,同一个围栏在不同时刻可能用不同的定位方式来判定。大体上,围栏半径越小(意味着判定精度要求越高),系统越可能倾向使用 GPS;setNotificationResponsiveness 设的响应阈值越松,系统越有空间用低功耗定位源;设备是否支持硬件围栏也会影响最终路径。
一个容易误解的点:注册围栏不等于 GPS 常开。系统会结合围栏大小和期望延迟动态权衡,优先尝试低功耗的 Wi-Fi/基站定位,只在判定精度不够时才会唤醒 GPS。
Doze 模式:围栏为什么「失灵」
Android 6.0 引入 Doze(打盹)模式,设备静止、灭屏一段时间后进入,系统会推迟网络访问、JobScheduler、Alarm 等。这直接影响围栏触发。
围栏事件的投递(PendingIntent 拉起 App)在 Doze 下有一定豁免,但豁免的是「通知」,不是「计算」。 真正容易受影响的是触发前的定位环节,这一点需要审慎表述——以下是基于 Doze 对网络访问、位置更新总体限流策略的合理推断,而非针对围栏场景的官方逐条说明:
- Doze 下系统会节流 Wi-Fi 扫描和网络定位等后台位置更新,融合定位可观测的位置更新频率可能降低。
- 设备长时间静止时,系统对是否已发生位移的判断依据变少,围栏判定可能依赖相对陈旧的位置数据。
- App 进入 App Standby(应用待机)后,后台能力进一步受限,也可能间接影响围栏回调的时延。
所以「走出围栏迟迟不触发」的一个常见诱因是:Doze 降低了位置更新频率,系统短时间内拿不到能明确判定 EXIT 的新位置。我当时的验证方法很直接:给测试机插上充电线(退出 Doze),回调恢复正常,可以作为省电策略影响触发时延的一个佐证,但这不代表 Doze 是所有围栏延迟问题的唯一根因。
平衡功耗与可靠性的实践
围栏是位置服务闭环里的后台兜底,但过度依赖会带来功耗问题。几条实践建议:
1. 半径尽量放大。 围栏半径是系统选择定位源的依据。200 米以上的半径,Wi-Fi/基站就能判定;50 米以内基本要上 GPS。业务上能用 300 米就别用 50 米。
2. 把 setNotificationResponsiveness 调到能接受的上限。 业务允许 10 分钟延迟就传 10 * 60 * 1000,系统在满足这个响应阈值的前提下有更大空间降低采样频率、批量处理;调成 0 意味着要求尽快通知,会压缩系统的调度空间,间接推高功耗,但具体影响幅度因设备和场景而异,没有固定倍数。
3. 少用 DWELL。 DWELL(停留)需要持续判断设备是否在区域内保持不动,采样压力比 ENTER/EXIT 大很多。能用 ENTER+EXIT 解决的场景就别加 DWELL。
4. 给围栏设过期时间。 一次性提醒场景用 setExpirationDuration,到期自动移除,避免留下永远耗电的孤儿围栏。
5. 结合前台状态降级。 App 在前台时自己用 requestLocationUpdates 做精确判断,围栏只做后台兜底,前台体验和后台功耗都照顾到。
实际项目里我见过的典型翻车配置:提醒半径设成 30 米,setNotificationResponsiveness 又传 0——两者叠加会明显压缩系统的调度和省电空间,实际功耗影响因设备、系统版本和使用场景差异很大,没有统一的量化数据,这里不给出具体掉电数字。围栏省电的前提,是给系统留出足够的权衡空间。
围栏的价值在于「系统帮你盯着位置,必要时叫醒你」,它不是实时定位的替代品。理解 Doze 对定位采样的影响,比死记 API 参数更能解决问题。