如何设置一个半径为 100 米的地理围栏并监听进入/离开事件?

解读

国内面试中,这道题考察的是“系统服务 + 权限 + 省电 + 兼容国内 ROM”的综合能力。面试官希望听到:

  1. 权限声明与动态申请(Android 10/11/12 后台定位差异)
  2. 使用 GeofencingClient 而非已废弃的 LocationManager
  3. 合理选择触发响应策略(INITIAL_TRIGGER、LOITERING_DELAY)
  4. 对国产 ROM 后台限制、杀进程、省电模式的应对手段
  5. 7.0 以上 Doze、App Standby 对广播的限制及 JobScheduler 兜底
  6. 围栏数量上限(100 个)与精度误差(GPS 漂移 + Wi-Fi 基站混合定位)的补偿思路

知识点

  • Google Play 服务 Location API:GeofencingClient、Geofence、GeofencingRequest、PendingIntent
  • 权限:ACCESS_FINE_LOCATION(精确 100 米必须)、Android 10+ ACCESS_BACKGROUND_LOCATION
  • 国内无 GMS 场景:高德 Android SDK 地理围栏接口(com.amap.api.location.GeofenceClient)
  • 省电策略:JobScheduler 轮询兜底 + 静止时降低定位频率
  • 精度:setNotificationResponsiveness 与 setLoiteringDelay 平衡电量与实时性
  • 杀进程保活:系统级白名单引导 + WorkManager 周期任务重新注册围栏
  • 合规:隐私政策、双清单(权限与信息收集清单)、工信部 164 号文

答案

  1. 权限与合规 在 AndroidManifest.xml 中声明 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> Android 10+ 如需后台触发,再声明 <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/> 启动页使用 ActivityResultLauncher 动态申请,拒绝时弹窗跳转系统设置。

  2. 依赖与初始化 国内带 GMS 机型: implementation 'com.google.android.gms:play-services-location:21.0.1' 创建 LocationServices.getGeofencingClient(context)

    国内无 GMS 机型(华为、OPPO 等): implementation 'com.amap.api:location:6.4.3' 创建 GeofenceClient(高德)

    以下代码以 GMS 为例,高德只需替换对应类名。

  3. 构建围栏 val geofence = Geofence.Builder() .setRequestId("store_100m") .setCircularRegion(lat, lng, 100f) .setExpirationDuration(Geofence.NEVER_EXPIRE) .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT) .setLoiteringDelay(5000) // 在圈内停留 5 秒再触发,减少漂移误报 .setNotificationResponsiveness(30000) // 30 秒上报一次,平衡电量 .build()

    val request = GeofencingRequest.Builder() .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER) .addGeofence(geofence) .build()

  4. PendingIntent 与接收 采用 IntentService 或 BroadcastReceiver 均可,但 Android 8.0 后台限制后推荐使用 BroadcastReceiver + 前台服务: val intent = Intent(context, GeofenceReceiver::class.java) val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE else PendingIntent.FLAG_UPDATE_CURRENT val pi = PendingIntent.getBroadcast(context, 0, intent, flags)

    GeofencingClient.addGeofences(request, pi) .addOnSuccessListener { Log.d("Geo", "围栏注册成功") } .addOnFailureListener { e -> // 错误码 1000 代表权限缺失,1002 代表围栏数超限 if ((e as ApiException).statusCode == GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE) { // 国内 ROM 常返回此错误,提示用户关闭省电或加入白名单 } }

  5. 接收并处理事件 class GeofenceReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val geofencingEvent = GeofencingEvent.fromIntent(intent) if (geofencingEvent.hasError()) return val transition = geofencingEvent.geofenceTransition val list = geofencingEvent.triggeringGeofences list.forEach { when (transition) { Geofence.GEOFENCE_TRANSITION_ENTER -> handleEnter(it.requestId) Geofence.GEOFENCE_TRANSITION_EXIT -> handleExit(it.requestId) } } } }

  6. 电量与保活

    • 在 Application.onCreate 中通过 WorkManager 周期任务(15 分钟)重新注册围栏,防止被系统回收。
    • 引导用户将应用加入“电池优化”白名单,代码: val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, Uri.parse("package:${context.packageName}")) context.startActivity(intent)
  7. 测试与验证 使用 Android Studio 模拟器 “Location” 面板输入坐标,观察 logcat 输出;真机测试需关闭 Wi-Fi 仅开 GPS,验证 100 米边界触发精度。

拓展思考

  1. 围栏重叠与分级:当业务需要 100 米、500 米、1 公里三级预警时,可一次注册多个同心圆围栏,通过 requestId 区分,避免在客户端做距离计算。
  2. 室内场景无 GPS:可结合蓝牙 iBeacon 或高德室内地图围栏,做“室外 GPS + 室内蓝牙”双通道,保证商场、地铁内也能触发。
  3. 服务端同步:进入围栏后上传事件到后端,后端结合用户画像做实时营销;需做幂等(UUID + 去重表),防止网络重试导致重复发券。
  4. 精度漂移补偿:利用 Kalman 滤波或高德 “逆地理编码 + 路网吸附” 接口,把定位点纠正到马路对面店铺,减少 30 米误差带来的误报。
  5. 隐私合规升级:工信部要求 2024 年起定位权限必须“单次授权”选项,需动态判断 shouldShowRequestPermissionRationale(),并在隐私弹窗中明确“100 米围栏营销”场景,否则应用市场审核会被驳回。