地理围栏是一种基于位置的服务,它允许应用在用户进入、离开或停留在某个地理区域(称为“围栏”)时,触发特定的操作,当用户进入公司范围时自动打卡,或当用户靠近家时自动打开空调。

在 Android 中,实现地理围栏主要有两种方式:
- Google Play 服务 API (推荐): 这是目前最主流、最推荐的方式,它由 Google Play 服务提供,具有更好的性能、更低的功耗和更精确的触发。
- Android 框架 API (已弃用): 这是早期 Android 版本中内置的方式。自 Android 8.0 (API 26) 起,该 API 已被弃用,强烈建议迁移到 Play Services API。
下面我们将重点介绍 Google Play Services API,因为它代表了当前的最佳实践。
核心概念:Google Play Services Geofencing API
Geofencing API 主要由以下几个关键部分组成:
Geofence (地理围栏)
这是定义围栏的核心对象,你需要创建一个 Geofence 对象,并指定以下关键参数:

requestId(唯一标识符): 一个字符串,用于唯一标识这个围栏,在后续的回调中,你会用它来识别是哪个围栏被触发了。transitionTypes(触发类型): 一个整数,定义了在什么情况下触发回调,可以使用Geofence类中的常量进行组合:Geofence.GEOFENCE_TRANSITION_ENTER: 用户进入围栏。Geofence.GEOFENCE_TRANSITION_EXIT: 用户离开围栏。Geofence.GEOFENCE_TRANSITION_DWELL: 用户在围栏内停留了指定的时间。
latitude,longitude,radius(围栏信息):latitude,longitude: 围栏的中心点坐标。radius: 围栏的半径,单位是米,这定义了一个圆形的围栏,Google Geofencing API 目前只支持圆形围栏。
expirationDuration(有效期): 围栏的持续时间,单位是毫秒,当围栏过期后,系统将不再监控它。Geofence.NEVER_EXPIRE: 表示围栏永不过期。
GeofencingClient (地理围栏客户端)
这是与系统交互的主要入口点,通过它,你可以添加、移除和监听围栏状态的变化,你需要通过 GoogleApiClient 或直接使用 getGeofencingClient(Context) 来获取其实例。
PendingIntent (待定意图)
这是 Geofencing API 的核心机制,它是一个“包装”好的 Intent,可以在后台(即使你的应用进程被杀死)被系统安全地执行。
- 工作原理: 当围栏被触发时,Google Play 服务会通过这个
PendingIntent发送一个广播或启动一个服务。 - 为什么重要: 使用
PendingIntent可以确保即使应用没有运行,地理围栏的回调也能被接收和处理,这是实现低功耗后台任务的关键。
GeofencingEvent (地理围栏事件)
当你的 PendingIntent 被触发时,你会在接收到的广播或服务中获取到一个 GeofencingEvent 对象,这个对象包含了触发事件的详细信息,
- 触发类型 (
getGeofenceTransition())。 - 被触发的围栏的
requestId列表 (getTriggeringGeofences())。 - 触发事件的时间和地点。
实现步骤 (代码示例)
下面是一个完整的实现流程,包括请求位置权限、添加围栏和处理回调。

第 1 步:添加依赖和权限
在 app/build.gradle 文件中添加 Play Services 依赖:
dependencies {
implementation 'com.google.android.gms:play-services-location:21.0.1' // 建议使用最新版本
}
在 AndroidManifest.xml 中声明必要的权限:
<!-- 用于访问精确位置 --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- 用于访问大致位置 (ACCESS_FINE_LOCATION 已包含此权限) --> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- 用于在后台接收位置更新 --> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <!-- Android 9 (API 28) 及以上需要此权限才能在后台启动前台服务 --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
第 2 步:请求运行时权限
在运行时,你需要向用户请求位置权限,对于 Android 10 (API 29) 及以上,ACCESS_BACKGROUND_LOCATION 需要单独请求,并且用户可能会看到一个解释为什么需要后台位置的对话框。
// 在 Activity 或 Fragment 中
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// 请求权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION},
LOCATION_PERMISSION_REQUEST_CODE);
}
第 3 步:创建 Geofence 和 PendingIntent
创建一个 Geofence 对象和一个 PendingIntent。
private Geofence createGeofence() {
// 围栏中心点 (北京天安门)
double latitude = 39.9042;
double longitude = 116.4074;
float radius = 500; // 半径500米
return new Geofence.Builder()
.setRequestId("tiananmen_square") // 唯一ID
.setCircularRegion(latitude, longitude, radius)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT)
.setLoiteringDelay(30000) // 停留30秒后触发 DWELL 事件 (可选)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.build();
}
private PendingIntent getGeofencePendingIntent() {
// 创建一个 Intent,用于接收 Geofence 事件
Intent intent = new Intent(this, GeofenceBroadcastReceiver.class);
// 使用 FLAG_UPDATE_CURRENT 来确保 PendingIntent 是最新的
// FLAG_IMMUTABLE 是 Android 12 (API 31) 及以上的要求
return PendingIntent.getBroadcast(this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}
第 4 步:添加地理围栏
当用户授权后,调用 GeofencingClient 的 addGeofences() 方法。
private void addGeofence() {
if (checkPermissions()) {
GeofencingClient geofencingClient = LocationServices.getGeofencingClient(this);
Geofence geofence = createGeofence();
PendingIntent pendingIntent = getGeofencePendingIntent();
// GeofencingRequest 包含了要添加的围栏列表
GeofencingRequest geofencingRequest = new GeofencingRequest.Builder()
.addGeofence(geofence)
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER) // 可选:设置初始触发条件
.build();
geofencingClient.addGeofences(geofencingRequest, pendingIntent)
.addOnSuccessListener(aVoid -> {
// 添加成功
Log.d("Geofence", "Geofence added successfully.");
})
.addOnFailureListener(e -> {
// 添加失败
Log.e("Geofence", "Failed to add geofence: " + e.getMessage());
});
}
}
第 5 步:创建广播接收器处理回调
创建一个 BroadcastReceiver 来接收围栏触发事件。
public class GeofenceBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
if (geofencingEvent == null) {
return;
}
int geofenceTransition = geofencingEvent.getGeofenceTransition();
if (geofenceTransition == GeofencingEvent.GEOFENCE_TRANSITION_ENTER ||
geofenceTransition == GeofencingEvent.GEOFENCE_TRANSITION_EXIT ||
geofenceTransition == GeofencingEvent.GEOFENCE_TRANSITION_DWELL) {
List<String> triggeringGeofenceIds = geofencingEvent.getTriggeringGeofences()
.stream()
.map(Geofence::getRequestId)
.collect(Collectors.toList());
// 处理逻辑
String transitionType;
switch (geofenceTransition) {
case GeofencingEvent.GEOFENCE_TRANSITION_ENTER:
transitionType = "进入";
break;
case GeofencingEvent.GEOFENCE_TRANSITION_EXIT:
transitionType = "离开";
break;
case GeofencingEvent.GEOFENCE_TRANSITION_DWELL:
transitionType = "停留";
break;
default:
transitionType = "未知";
break;
}
Log.d("GeofenceReceiver", "触发事件: " + transitionType + ", 围栏ID: " + triggeringGeofenceIds);
// 在这里执行你的业务逻辑,例如发送通知、启动服务等
// 为了确保通知的可靠性,建议使用前台服务
} else {
// 无效的转换类型
Log.e("GeofenceReceiver", "无效的转换类型: " + geofenceTransition);
}
}
}
别忘了在 AndroidManifest.xml 中注册这个广播接收器:
<receiver android:name=".GeofenceBroadcastReceiver"
android:enabled="true"
android:exported="false" />
重要注意事项和最佳实践
-
功耗优化:
- 合并围栏: 尽量在一次
addGeofences()调用中添加多个围栏,而不是频繁地添加和移除单个围栏,这能显著降低功耗。 - 合理的半径: 设置过小的半径可能会导致设备频繁触发围栏,增加功耗。
- 使用
Dwell事件: 对于某些场景(如到达目的地后自动执行操作),使用Dwell事件比Enter事件更合理,可以避免用户只是路过就触发操作。
- 合并围栏: 尽量在一次
-
精度和可靠性:
- 位置模式: Geofencing API 依赖于设备的定位能力,确保设备的位置模式设置为“高精度”。
- 室内环境: 在 GPS 信号弱的室内,定位精度会下降,可能导致围栏触发不准确,可以结合 Wi-Fi 或蓝牙信标来辅助定位,但这超出了 Geofencing API 的范畴。
- Android 10 的后台限制: 从 Android 10 开始,后台应用访问位置受到严格限制,使用
PendingIntent和ACCESS_BACKGROUND_LOCATION权限是绕过此限制的正确方式。
-
用户体验:
- 清晰的权限说明: 在请求位置权限时,向用户清晰地解释为什么需要这些权限,以及它们将如何被使用。
- 提供设置选项: 允许用户在应用设置中开启或关闭地理围栏功能。
- 前台服务: 如果围栏触发后需要执行长时间或显眼的操作(如播放声音、显示持续的通知),强烈建议使用前台服务,这可以防止系统在内存不足时杀死你的服务,并向用户提供一个持续的通知,告知他们应用正在后台执行任务。
替代方案
如果你的应用主要面向中国市场,且无法依赖 Google Play 服务,可以考虑以下替代方案:
- 第三方 SDK: 一些国内地图服务提供商(如高德地图、百度地图)提供了自己的地理围栏 SDK,这些 SDK 通常与它们的地图服务深度集成,并针对国内网络环境进行了优化。
- 自研方案: 基于设备的 GPS 或网络位置,自己实现一个定时检查位置并与围栏列表进行比对的服务,这种方式功耗高、精度差、耗电严重,不推荐在生产环境中使用,除非有非常特殊的需求。
| 特性 | Google Play Services Geofencing API (推荐) | Android Framework API (已弃用) |
|---|---|---|
| 状态 | 活跃,官方推荐 | 已弃用 (API 26+) |
| 功耗 | 低,系统级优化 | 高,依赖应用自身轮询 |
| 可靠性 | 高,即使在后台或应用关闭时也能触发 | 低,易受系统限制和 Doze 模式影响 |
| 实现复杂度 | 中等,需要理解 PendingIntent |
简单,但功能有限 |
| 适用场景 | 所有需要地理围栏的 Android 应用 | 仅适用于维护旧版应用 |
对于任何新的 Android 项目,请务必使用 Google Play Services 的 Geofencing API,它功能强大、稳定且功耗低,是实现地理围栏功能的最佳选择。
