在移动应用开发中,定位功能是实现 LBS(基于位置服务)的核心基础,广泛应用于地图导航、本地生活服务、社交签到等场景。Android 平台提供了多种定位方案,从传统的 GPS 到融合定位服务,开发者需要在精度、功耗和响应速度之间找到平衡。本文将系统讲解 Android 定位的技术原理、核心 API 使用、权限适配及优化策略,帮助你构建稳定、高效的定位功能。
一、定位技术原理与方案对比
Android 设备获取位置信息主要依赖四种技术手段,各有其适用场景和局限性:
定位方式 |
技术原理 |
精度范围 |
功耗 |
适用场景 |
GPS |
卫星信号定位 |
1-10 米 |
高 |
户外开阔场景 |
网络定位 |
Wi-Fi 热点 + 基站信号 |
10-1000 米 |
中 |
城市室内 / 半室内 |
基站定位 |
移动基站三角定位 |
500-3000 米 |
低 |
网络环境差的区域 |
传感器辅助 |
加速度计 + 陀螺仪 + 磁力计 |
辅助提升 |
中 |
短距离移动跟踪 |
现代 Android 系统通过融合定位服务(Fused Location Provider) 智能整合上述技术,根据场景自动选择最优定位方式。例如:
- 户外导航时优先使用 GPS 保证精度
- 室内场景自动切换到 Wi-Fi 定位
- 后台低频率定位时采用基站定位降低功耗
二、权限配置与适配策略
位置权限是 Android 权限体系中最敏感的权限之一,随着系统版本迭代,权限管控日益严格。正确处理权限是实现定位功能的前提。
2.1 权限声明
根据定位精度需求在AndroidManifest.xml中声明相应权限:
<!-- 粗略定位权限(网络定位) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- 精细定位权限(GPS) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- Android 10+后台定位权限 -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- 可选:网络状态和Wi-Fi权限(辅助定位) -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
2.2 动态权限申请
Android 6.0(API 23)以上需要动态申请危险权限,定位权限处理流程如下:
// 所需定位权限
private val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
// 后台定位额外权限(Android 10+)
private val BACKGROUND_PERMISSION = Manifest.permission.ACCESS_BACKGROUND_LOCATION
// 检查权限是否已授予
private fun hasLocationPermissions(): Boolean {
return REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
}
}
// 检查后台定位权限(Android 10+)
private fun hasBackgroundLocationPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContextCompat.checkSelfPermission(this, BACKGROUND_PERMISSION) == PackageManager.PERMISSION_GRANTED
} else {
true // 低版本默认拥有后台定位能力
}
}
// 申请权限
private fun requestLocationPermissions() {
val permissionsToRequest = mutableListOf<String>()
REQUIRED_PERMISSIONS.forEach {
if (ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(it)
}
}
// Android 10+需要单独申请后台权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
ContextCompat.checkSelfPermission(this, BACKGROUND_PERMISSION) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(BACKGROUND_PERMISSION)
}
if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(
this,
permissionsToRequest.toTypedArray(),
LOCATION_PERMISSION_REQUEST_CODE
)
}
}
// 处理权限申请结果
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) {
if (grantResults.any { it == PackageManager.PERMISSION_DENIED }) {
// 权限被拒绝,提示用户
showPermissionDeniedDialog()
} else {
// 权限授予,初始化定位
initLocationProvider()
}
}
}
2.3 权限适配要点
1.权限分级处理:
- 基础定位功能:申请ACCESS_COARSE_LOCATION或ACCESS_FINE_LOCATION
- 后台持续定位(如运动追踪):额外申请ACCESS_BACKGROUND_LOCATION(Android 10+)
- 仅前台定位:无需后台权限,但应用退到后台后定位会停止
2.权限解释策略:
- 在申请权限前,通过弹窗说明定位用途(如 "需要定位以显示附近的餐厅")
- 对于拒绝权限的用户,提供跳转设置页面的引导
3.Android 12 + 精确位置控制:
Android 12 引入 "精确位置" 开关,用户可单独控制是否授予高精度定位:
// 检查是否获得精确位置权限
fun isPreciseLocationGranted(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
LocationManagerCompat.isLocationEnabledForUsageScenario(
context,
LocationManagerCompat.USAGE_SCENARIO_FINE_LOCATION
)
} else {
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
}
三、Fused Location Provider 实战
Google 推荐使用融合定位服务(Fused Location Provider)实现定位功能,它封装了复杂的定位逻辑,提供更稳定、高效的定位体验。
3.1 集成 Google Play 服务
在build.gradle中添加依赖:
dependencies {
// 融合定位服务
implementation 'com.google.android.gms:play-services-location:21.0.1'
}
3.2 获取最后已知位置
获取设备最后记录的位置,适合快速获取用户大致位置:
class LocationManager(private val context: Context) {
// 融合定位客户端
private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
// 获取最后已知位置
fun getLastKnownLocation(callback: (Location?) -> Unit) {
// 检查权限
if (!hasLocationPermissions()) {
callback(null)
return
}
fusedLocationClient.lastLocation
.addOnSuccessListener { location ->
// 位置可能为null(如设备从未定位过)
callback(location)
}
.addOnFailureListener { e ->
Log.e(TAG, "获取最后位置失败", e)
callback(null)
}
}
}
注意:lastLocation可能返回 null,原因包括:
- 设备首次启动未完成定位
- 定位服务被禁用
- 应用从未获得定位权限
3.3 请求实时位置更新
通过设置定位请求参数,获取持续的位置更新:
// 配置定位请求
private fun createLocationRequest(): LocationRequest {
return LocationRequest.create().apply {
// 定位优先级(精度与功耗平衡)
priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY
// 位置更新间隔(毫秒)
interval = 10000 // 10秒
// 最快更新间隔(不能小于interval)
fastestInterval = 5000 // 5秒
// 最小位移(米),超过此距离才更新
smallestDisplacement = 10f // 10米
// 最长等待时间(毫秒)
maxWaitTime = 15000 // 15秒
}
}
// 位置更新回调
private val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult.locations.forEach { location ->
// 处理新位置
handleNewLocation(location)
}
}
override fun onLocationAvailability(availability: LocationAvailability) {
if (!availability.isLocationAvailable) {
// 定位不可用,提示用户
showLocationUnavailableMessage()
}
}
}
// 开始位置更新
fun startLocationUpdates() {
if (!hasLocationPermissions()) {
return
}
val locationRequest = createLocationRequest()
try {
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper() // 回调在主线程执行
)
} catch (e: SecurityException) {
Log.e(TAG, "权限不足", e)
}
}
// 停止位置更新
fun stopLocationUpdates() {
fusedLocationClient.removeLocationUpdates(locationCallback)
}
定位优先级选择:
- PRIORITY_HIGH_ACCURACY:最高精度(GPS 优先),功耗高
- PRIORITY_BALANCED_POWER_ACCURACY:平衡精度与功耗(默认)
- PRIORITY_LOW_POWER:低功耗,精度低
- PRIORITY_NO_POWER:仅使用被动定位(如其他应用触发的定位)
3.4 前台服务定位(Android 10+)
Android 10 及以上,应用退到后台后无法获取位置更新,需使用前台服务:
// 前台服务中启动定位
class LocationForegroundService : Service() {
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var locationCallback: LocationCallback
override fun onCreate() {
super.onCreate()
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
// 处理位置更新
}
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// 显示前台服务通知
val notification = createNotification()
startForeground(LOCATION_SERVICE_ID, notification)
// 开始定位更新
val locationRequest = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY
interval = 30000 // 30秒
}
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
return START_STICKY
}
// 创建前台服务通知
private fun createNotification(): Notification {
val channelId = "location_channel"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"位置服务",
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
return NotificationCompat.Builder(this, channelId)
.setContentTitle("正在获取位置")
.setSmallIcon(R.drawable.ic_location)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
}
override fun onDestroy() {
super.onDestroy()
fusedLocationClient.removeLocationUpdates(locationCallback)
}
override fun onBind(intent: Intent): IBinder? = null
}
在AndroidManifest.xml中声明服务:
<service
android:name=".LocationForegroundService"
android:foregroundServiceType="location"
android:exported="false" />
四、地理编码与逆地理编码
将经纬度转换为具体地址(逆地理编码)或反之(地理编码),是定位功能的常见需求。
4.1 使用 Geocoder 实现
Android 内置的Geocoder提供基础的地理编码功能:
// 逆地理编码(经纬度 -> 地址)
suspend fun getAddressFromLocation(
latitude: Double,
longitude: Double
): List<Address>? = withContext(Dispatchers.IO) {
try {
val geocoder = Geocoder(context, Locale.getDefault())
// 获取最多5个地址候选
geocoder.getFromLocation(latitude, longitude, 5)
} catch (e: Exception) {
Log.e(TAG, "逆地理编码失败", e)
null
}
}
// 地理编码(地址 -> 经纬度)
suspend fun getLocationFromAddress(addressName: String): List<Address>? = withContext(Dispatchers.IO) {
try {
val geocoder = Geocoder(context, Locale.getDefault())
geocoder.getFromLocationName(addressName, 5)
} catch (e: Exception) {
Log.e(TAG, "地理编码失败", e)
null
}
}
使用注意:
- Geocoder依赖网络,可能返回 null 或空列表
- 结果准确性有限,建议用于非关键场景
- 调用需在后台线程执行,避免阻塞 UI
4.2 谷歌地理编码 API(推荐)
对于更高精度的地理编码需求,可使用 Google Maps Geocoding API:
// 谷歌地理编码API请求
suspend fun getAddressByGoogleApi(
latitude: Double,
longitude: Double,
apiKey: String
): String? = withContext(Dispatchers.IO) {
val url = "https://maps.googleapis.com/maps/api/geocode/json?" +
"latlng=$latitude,$longitude&key=$apiKey"
return@withContext try {
val response = OkHttpClient().newCall(Request.Builder()
.url(url)
.build()).execute()
if (response.isSuccessful) {
val json = JsonParser.parseString(response.body?.string()).asJsonObject
val results = json.getAsJsonArray("results")
if (results.size() > 0) {
results.get(0).asJsonObject.get("formatted_address").asString
} else {
null
}
} else {
null
}
} catch (e: Exception) {
Log.e(TAG, "谷歌地理编码API请求失败", e)
null
}
}
优势:
- 地址解析更准确,支持多种语言
- 提供结构化地址信息(街道、城市、国家等)
- 支持批量请求和复杂地址解析
五、定位优化策略
定位功能是应用功耗的主要来源之一,合理的优化策略能显著提升用户体验。
5.1 动态调整定位参数
根据应用场景动态修改定位参数,平衡精度和功耗:
// 根据应用状态调整定位策略
fun adjustLocationStrategy(isForeground: Boolean) {
val locationRequest = if (isForeground) {
// 前台状态:高精度,短间隔
LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
interval = 5000
}
} else {
// 后台状态:低精度,长间隔
LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_LOW_POWER
interval = 60000 // 1分钟
}
}
// 更新定位请求
fusedLocationClient.removeLocationUpdates(locationCallback)
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
}
5.2 批量处理位置更新
减少频繁 UI 更新和网络请求,批量处理位置数据:
class BatchLocationProcessor {
private val locationBuffer = mutableListOf<Location>()
private val BATCH_SIZE = 5 // 批量大小
private val BATCH_TIMEOUT = 30000L // 超时时间(毫秒)
private var lastFlushTime = 0L
// 添加位置到缓冲区
fun addLocation(location: Location) {
locationBuffer.add(location)
// 满足批量大小或超时则处理
if (locationBuffer.size >= BATCH_SIZE ||
System.currentTimeMillis() - lastFlushTime > BATCH_TIMEOUT) {
flushLocations()
}
}
// 处理缓冲区数据
private fun flushLocations() {
if (locationBuffer.isEmpty()) return
// 批量处理(如网络上传)
processBatch(locationBuffer)
// 清空缓冲区
locationBuffer.clear()
lastFlushTime = System.currentTimeMillis()
}
private fun processBatch(locations: List<Location>) {
// 实现批量处理逻辑
}
}
5.3 地理围栏替代持续定位
对于特定区域监控(如到达某个地点提醒),使用地理围栏更节能:
// 创建地理围栏
fun createGeofence(latitude: Double, longitude: Double, radius: Float) {
val geofence = Geofence.Builder()
.setRequestId("home_fence")
.setCircularRegion(latitude, longitude, radius) // 圆心和半径(米)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
.setExpirationDuration(Geofence.NEVER_EXPIRE) // 永不过期
.build()
val geofencingRequest = GeofencingRequest.Builder()
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.addGeofence(geofence)
.build()
// 地理围栏触发 Intent
val intent = Intent(context, GeofenceBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// 添加地理围栏
val geofencingClient = LocationServices.getGeofencingClient(context)
geofencingClient.addGeofences(geofencingRequest, pendingIntent)
.addOnSuccessListener {
Log.d(TAG, "地理围栏添加成功")
}
.addOnFailureListener { e ->
Log.e(TAG, "地理围栏添加失败", e)
}
}
// 接收地理围栏事件
class GeofenceBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val geofencingEvent = GeofencingEvent.fromIntent(intent)
if (geofencingEvent.hasError()) {
return
}
// 处理围栏触发事件
val transitionType = geofencingEvent.geofenceTransition
if (transitionType == Geofence.GEOFENCE_TRANSITION_ENTER) {
// 进入围栏区域
context.sendBroadcast(Intent("GEOFENCE_ENTER"))
} else if (transitionType == Geofence.GEOFENCE_TRANSITION_EXIT) {
// 离开围栏区域
}
}
}
六、异常处理与用户引导
定位功能受设备状态、网络环境等因素影响较大,完善的异常处理能提升应用稳定性。
6.1 定位服务状态检查
// 检查定位服务是否开启
fun isLocationEnabled(context: Context): Boolean {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
locationManager.isLocationEnabled
} else {
// 低版本检查GPS和网络定位是否开启
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
}
}
// 引导用户开启定位服务
fun promptEnableLocationService(activity: Activity) {
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
activity.startActivityForResult(intent, LOCATION_SETTINGS_REQUEST_CODE)
}
6.2 定位精度低的处理
// 检查定位精度是否满足需求
fun isLocationAccurateEnough(location: Location, minAccuracy: Float): Boolean {
// 定位精度(accuracy)数值越小越精确
return location.accuracy <= minAccuracy
}
// 处理低精度定位
fun handleLowAccuracyLocation(location: Location) {
// 1. 提示用户移到开阔区域
// 2. 切换到更高优先级的定位请求
// 3. 结合网络定位辅助
}
6.3 定位超时处理
// 定位超时监控
class LocationTimeoutMonitor {
private var timeoutJob: Job? = null
// 开始监控
fun startMonitoring(timeoutMillis: Long, onTimeout: () -> Unit) {
timeoutJob?.cancel()
timeoutJob = GlobalScope.launch(Dispatchers.Main) {
delay(timeoutMillis)
onTimeout()
}
}
// 收到位置更新时重置监控
fun onLocationReceived() {
timeoutJob?.cancel()
}
// 停止监控
fun stopMonitoring() {
timeoutJob?.cancel()
}
}
// 使用示例
val timeoutMonitor = LocationTimeoutMonitor()
timeoutMonitor.startMonitoring(30000) { // 30秒超时
// 定位超时,提示用户检查网络和定位服务
showLocationTimeoutMessage()
}
七、最佳实践与总结
7.1 定位功能最佳实践
- 权限申请时机:在用户需要使用定位功能时再申请,而非应用启动时
- 参数动态调整:根据应用状态(前台 / 后台)和用户行为调整定位参数
- 优先使用缓存:合理利用lastLocation减少定位请求
- 清理资源:页面销毁或功能关闭时,及时停止定位更新
- 错误日志:记录定位失败详情,便于问题排查
- 用户教育:通过友好提示引导用户开启定位服务和权限
7.2 常见场景解决方案
场景 |
推荐方案 |
关键参数 |
地图导航 |
GPS 优先,高精度 |
PRIORITY_HIGH_ACCURACY,interval=1-5 秒 |
签到打卡 |
高精度 + 位置验证 |
结合 WiFi 和基站信息,accuracy<50 米 |
运动追踪 |
前台服务 + 中等精度 |
PRIORITY_BALANCED_POWER_ACCURACY,interval=10-30 秒 |
后台围栏监控 |
地理围栏 + 低功耗 |
围栏半径 100-500 米,仅监控进入 / 离开事件 |
本地推荐 |
低精度 + 批量更新 |
PRIORITY_LOW_POWER,interval=5-10 分钟 |
Android 定位技术的核心是在精度、响应速度和功耗之间找到最佳平衡点。通过融合定位服务,开发者可以快速实现稳定的定位功能,而针对不同场景的参数优化和异常处理,则能显著提升用户体验。随着 Android 系统对隐私保护的加强,合理申请和使用定位权限,透明地向用户说明定位用途,也是构建可信应用的关键。