Skip to content

位置服务

源:Android Location | iOS CoreLocation

位置服务是地图、导航、LBS 应用的核心功能。Android 和 iOS 的定位 API 差异显著,本文将展示如何抽象统一的位置获取接口。

平台差异对比

平台原生 API定位方式权限要求精度控制
AndroidFusedLocationProviderClientGPS、网络、基站ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION高精度/低精度/平衡
iOSCLLocationManagerGPS、Wi-Fi、蜂窝网NSLocationWhenInUseUsageDescription / NSLocationAlwaysUsageDescriptionkCLLocationAccuracyBest 等多级
Desktop无原生支持IP 定位(第三方)无需权限城市级

expect/actual 实现方案

API 核心签名说明

  • data class Location
  • enum class LocationAccuracy
  • expect class LocationManager
  • suspend 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)
    }
}