位置服务
源:Android Location | iOS CoreLocation
位置服务是地图、导航、LBS 应用的核心功能。Android 和 iOS 的定位 API 差异显著,本文将展示如何抽象统一的位置获取接口。
平台差异对比
| 平台 | 原生 API | 定位方式 | 权限要求 | 精度控制 |
|---|---|---|---|---|
| Android | FusedLocationProviderClient | GPS、网络、基站 | ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION | 高精度/低精度/平衡 |
| iOS | CLLocationManager | GPS、Wi-Fi、蜂窝网 | NSLocationWhenInUseUsageDescription / NSLocationAlwaysUsageDescription | kCLLocationAccuracyBest 等多级 |
| Desktop | 无原生支持 | IP 定位(第三方) | 无需权限 | 城市级 |
expect/actual 实现方案
API 核心签名说明
data class Locationenum class LocationAccuracyexpect class LocationManagersuspend fun LocationManager.getCurrentLocation(accuracy: LocationAccuracy): Location?fun LocationManager.startLocationUpdates(callback: (Location) -> Unit)fun LocationManager.stopLocationUpdates()
标准代码块
kotlin
data class Location(
val latitude: Double,
val longitude: Double,
val altitude: Double? = null,
val accuracy: Float? = null,
val timestamp: Long = System.currentTimeMillis()
)
enum class LocationAccuracy {
HIGH, // 高精度(GPS)
BALANCED, // 平衡(GPS + 网络)
LOW // 低精度(仅网络)
}
expect class LocationManager {
suspend fun requestPermission(): Boolean
suspend fun getCurrentLocation(accuracy: LocationAccuracy = LocationAccuracy.HIGH): Location?
fun startLocationUpdates(
accuracy: LocationAccuracy = LocationAccuracy.BALANCED,
interval: Long = 10000L,
callback: (Location) -> Unit
)
fun stopLocationUpdates()
}
// 业务层使用
class MapViewModel(private val locationManager: LocationManager) {
private var isTracking = false
suspend fun getCurrentPosition(): Location? {
val hasPermission = locationManager.requestPermission()
if (!hasPermission) {
return null
}
return locationManager.getCurrentLocation(LocationAccuracy.HIGH)
}
fun startTracking(onLocationUpdate: (Location) -> Unit) {
if (isTracking) return
locationManager.startLocationUpdates(
accuracy = LocationAccuracy.BALANCED,
interval = 5000L
) { location ->
onLocationUpdate(location)
}
isTracking = true
}
fun stopTracking() {
locationManager.stopLocationUpdates()
isTracking = false
}
}kotlin
import android.Manifest
import android.content.Context
import android.location.Location as AndroidLocation
import android.os.Looper
import com.google.android.gms.location.*
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.tasks.await
import kotlin.coroutines.resume
actual class LocationManager(private val context: Context) {
private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
private var locationCallback: LocationCallback? = null
actual suspend fun requestPermission(): Boolean {
// 权限检查应该在外部通过 PermissionManager 处理
val fineLocation = context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
val coarseLocation = context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
return fineLocation == android.content.pm.PackageManager.PERMISSION_GRANTED ||
coarseLocation == android.content.pm.PackageManager.PERMISSION_GRANTED
}
actual suspend fun getCurrentLocation(accuracy: LocationAccuracy): Location? {
if (!requestPermission()) return null
return try {
val priority = when (accuracy) {
LocationAccuracy.HIGH -> Priority.PRIORITY_HIGH_ACCURACY
LocationAccuracy.BALANCED -> Priority.PRIORITY_BALANCED_POWER_ACCURACY
LocationAccuracy.LOW -> Priority.PRIORITY_LOW_POWER
}
val currentLocation = fusedLocationClient.getCurrentLocation(
priority,
null
).await()
currentLocation?.toCommonLocation()
} catch (e: Exception) {
null
}
}
actual fun startLocationUpdates(
accuracy: LocationAccuracy,
interval: Long,
callback: (Location) -> Unit
) {
if (!runBlocking { requestPermission() }) return
val priority = when (accuracy) {
LocationAccuracy.HIGH -> Priority.PRIORITY_HIGH_ACCURACY
LocationAccuracy.BALANCED -> Priority.PRIORITY_BALANCED_POWER_ACCURACY
LocationAccuracy.LOW -> Priority.PRIORITY_LOW_POWER
}
val locationRequest = LocationRequest.Builder(priority, interval)
.setMinUpdateIntervalMillis(interval / 2)
.build()
locationCallback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.lastLocation?.let { androidLocation ->
callback(androidLocation.toCommonLocation())
}
}
}
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback!!,
Looper.getMainLooper()
)
}
actual fun stopLocationUpdates() {
locationCallback?.let {
fusedLocationClient.removeLocationUpdates(it)
}
locationCallback = null
}
private fun AndroidLocation.toCommonLocation() = Location(
latitude = latitude,
longitude = longitude,
altitude = if (hasAltitude()) altitude else null,
accuracy = if (hasAccuracy()) accuracy else null,
timestamp = time
)
}kotlin
import platform.CoreLocation.*
import platform.Foundation.*
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
actual class LocationManager {
private val manager = CLLocationManager()
private var locationUpdateCallback: ((Location) -> Unit)? = null
private var delegate: LocationDelegate? = null
actual suspend fun requestPermission(): Boolean = suspendCancellableCoroutine { continuation ->
val status = CLLocationManager.authorizationStatus()
when (status) {
kCLAuthorizationStatusAuthorizedWhenInUse,
kCLAuthorizationStatusAuthorizedAlways -> {
continuation.resume(true)
}
kCLAuthorizationStatusNotDetermined -> {
delegate = LocationDelegate(
onAuthorizationChanged = { newStatus ->
val granted = newStatus == kCLAuthorizationStatusAuthorizedWhenInUse ||
newStatus == kCLAuthorizationStatusAuthorizedAlways
continuation.resume(granted)
}
)
manager.delegate = delegate
manager.requestWhenInUseAuthorization()
}
else -> {
continuation.resume(false)
}
}
}
actual suspend fun getCurrentLocation(accuracy: LocationAccuracy): Location? =
suspendCancellableCoroutine { continuation ->
if (CLLocationManager.authorizationStatus() !in listOf(
kCLAuthorizationStatusAuthorizedWhenInUse,
kCLAuthorizationStatusAuthorizedAlways
)) {
continuation.resume(null)
return@suspendCancellableCoroutine
}
manager.desiredAccuracy = when (accuracy) {
LocationAccuracy.HIGH -> kCLLocationAccuracyBest
LocationAccuracy.BALANCED -> kCLLocationAccuracyNearestTenMeters
LocationAccuracy.LOW -> kCLLocationAccuracyKilometer
}
delegate = LocationDelegate(
onLocationUpdate = { location ->
continuation.resume(location)
manager.stopUpdatingLocation()
}
)
manager.delegate = delegate
manager.startUpdatingLocation()
}
actual fun startLocationUpdates(
accuracy: LocationAccuracy,
interval: Long,
callback: (Location) -> Unit
) {
manager.desiredAccuracy = when (accuracy) {
LocationAccuracy.HIGH -> kCLLocationAccuracyBest
LocationAccuracy.BALANCED -> kCLLocationAccuracyNearestTenMeters
LocationAccuracy.LOW -> kCLLocationAccuracyKilometer
}
locationUpdateCallback = callback
delegate = LocationDelegate(
onLocationUpdate = { location ->
callback(location)
}
)
manager.delegate = delegate
manager.startUpdatingLocation()
}
actual fun stopLocationUpdates() {
manager.stopUpdatingLocation()
locationUpdateCallback = null
}
}
// iOS LocationManager Delegate
private class LocationDelegate(
private val onLocationUpdate: ((Location) -> Unit)? = null,
private val onAuthorizationChanged: ((CLAuthorizationStatus) -> Unit)? = null
) : NSObject(), CLLocationManagerDelegateProtocol {
override fun locationManager(
manager: CLLocationManager,
didUpdateLocations: List<*>
) {
val clLocation = didUpdateLocations.lastOrNull() as? CLLocation ?: return
val location = Location(
latitude = clLocation.coordinate.useContents { latitude },
longitude = clLocation.coordinate.useContents { longitude },
altitude = clLocation.altitude,
accuracy = clLocation.horizontalAccuracy.toFloat(),
timestamp = (clLocation.timestamp.timeIntervalSince1970 * 1000).toLong()
)
onLocationUpdate?.invoke(location)
}
override fun locationManagerDidChangeAuthorization(manager: CLLocationManager) {
onAuthorizationChanged?.invoke(manager.authorizationStatus)
}
}kotlin
actual class LocationManager {
actual suspend fun requestPermission(): Boolean {
return true // Desktop 无需权限
}
actual suspend fun getCurrentLocation(accuracy: LocationAccuracy): Location? {
// Desktop 可以通过 IP 定位 API(如 ipapi.co)
println("⚠️ Desktop location not implemented")
return null
}
actual fun startLocationUpdates(
accuracy: LocationAccuracy,
interval: Long,
callback: (Location) -> Unit
) {
println("⚠️ Desktop location updates not implemented")
}
actual fun stopLocationUpdates() {
println("⚠️ Desktop location updates not implemented")
}
}代码封装示例
以下是带缓存和降级策略的完整封装:
kotlin
// commonMain
class LocationService(
private val manager: LocationManager,
private val cacheTimeout: Long = 60_000L // 1 分钟缓存
) {
private var cachedLocation: Location? = null
private var cacheTime: Long = 0
suspend fun getLocation(forceRefresh: Boolean = false): Location? {
// 优先使用缓存
if (!forceRefresh && isCacheValid()) {
return cachedLocation
}
// 尝试高精度定位
val location = manager.getCurrentLocation(LocationAccuracy.HIGH)
?: manager.getCurrentLocation(LocationAccuracy.BALANCED) // 降级
?: manager.getCurrentLocation(LocationAccuracy.LOW) // 再降级
// 更新缓存
if (location != null) {
cachedLocation = location
cacheTime = System.currentTimeMillis()
}
return location
}
private fun isCacheValid(): Boolean {
val cachedLoc = cachedLocation ?: return false
return (System.currentTimeMillis() - cacheTime) < cacheTimeout
}
fun clearCache() {
cachedLocation = null
cacheTime = 0
}
}依赖补充
Android Google Play Services
kotlin
kotlin {
sourceSets {
androidMain.dependencies {
implementation("com.google.android.gms:play-services-location:21.1.0")
}
}
}groovy
kotlin {
sourceSets {
androidMain {
dependencies {
implementation "com.google.android.gms:play-services-location:21.1.0"
}
}
}
}toml
[versions]
play-services-location = "21.1.0"
[libraries]
gms-location = { module = "com.google.android.gms:play-services-location", version.ref = "play-services-location" }最新版本查看链接:https://developers.google.com/android/guides/releases
AndroidManifest.xml 配置
xml
<!-- 精确位置权限(GPS) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 粗略位置权限(网络) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- 后台位置权限(Android 10+) -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />iOS Info.plist 配置
xml
<!-- 使用期间定位权限 -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要获取您的位置以提供导航服务</string>
<!-- 始终定位权限(后台) -->
<key>NSLocationAlwaysUsageDescription</key>
<string>需要在后台获取位置以记录运动轨迹</string>
<!-- iOS 11+ 必需 -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>需要在后台获取位置</string>实战坑点
Android 后台位置限制
Android 10+ 后台定位
Android 10+ 后台定位需要 ACCESS_BACKGROUND_LOCATION 权限,且必须单独请求。
正确请求流程:
kotlin
// 先请求前台权限
requestPermission(ACCESS_FINE_LOCATION)
// 在已授予前台权限后,再请求后台权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
requestPermission(ACCESS_BACKGROUND_LOCATION)
}iOS 精确位置权限
iOS 14+ 模糊位置
iOS 14+ 用户可选择提供模糊位置,精度降至几公里级别。
检查精度:
swift
if #available(iOS 14.0, *) {
let accuracy = manager.accuracyAuthorization
if accuracy == .reducedAccuracy {
// 提示用户开启精确位置
}
}iOS Delegate 内存管理
Delegate 被释放
CLLocationManager 的 delegate 必须保持强引用,否则回调不会触发。
解决方案:
kotlin
// iosMain
actual class LocationManager {
private val manager = CLLocationManager()
private var delegate: LocationDelegate? = null // 强引用
init {
delegate = LocationDelegate()
manager.delegate = delegate
}
}Android Google Play Services 未安装
::: caution 设备无 GMS 中国大陆设备、华为新设备可能无 Google Play Services。 :::
降级方案:
kotlin
fun isGooglePlayServicesAvailable(context: Context): Boolean {
val result = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(context)
return result == ConnectionResult.SUCCESS
}
// 降级使用 LocationManager
if (!isGooglePlayServicesAvailable(context)) {
// 使用 android.location.LocationManager
}定位超时处理
定位无响应
GPS 冷启动可能需要几十秒,室内可能无法定位。
超时机制:
kotlin
suspend fun getLocationWithTimeout(timeout: Long = 30000L): Location? {
return withTimeoutOrNull(timeout) {
getCurrentLocation(LocationAccuracy.HIGH)
}
}