3. 使用 FusedLocationProviderClient 获取最后已知位置

使用 FusedLocationProviderClient 获取最后已知位置 学习卡片

为什么 getLastLocation 是“先问缓存,再决定要不要动”

FusedLocationProviderClient 的 getLastLocation() 返回的是系统位置缓存中最近一次被任意应用写入的位置,它不触发新的定位硬件工作,因此通常毫秒级返回,但可能很旧、精度很差,甚至为 null。正确用法不是“拿到就用”,而是先判断时效性与精度,再决定是否发起新的定位请求。

时效性判断的三个维度

  • 是否为空:返回 null 表示缓存中没有任何可用位置,必须发起新请求。
  • 时间戳 age:System.currentTimeMillis() - location.getTime(),超过阈值(如 2 分钟)视为过期。
  • 精度 accuracy:location.getAccuracy() 单位为米,数值越大越差,超过阈值(如 100 m)视为不可用。
缓存位置适合“先渲染再刷新”的体验:立即把旧位置画到界面上,同时后台发起一次新请求,新结果到达后覆盖。这样用户不会看到空白等待。

完整实现:缓存优先 + 过期回退

class LastKnownLocationActivity : AppCompatActivity() {

    private lateinit var fusedClient: FusedLocationProviderClient
    private val maxAgeMs = 2 * 60 * 1000L   // 缓存最大可接受年龄:2 分钟
    private val maxAccuracyM = 100f          // 缓存最大可接受精度:100 米

    private val locationPermissionRequest =
        registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grants ->
            if (grants[Manifest.permission.ACCESS_FINE_LOCATION] == true ||
                grants[Manifest.permission.ACCESS_COARSE_LOCATION] == true) {
                readLastKnownLocation()
            } else {
                showMessage("未授予定位权限,无法读取缓存位置")
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        fusedClient = LocationServices.getFusedLocationProviderClient(this)
        requestPermissionThenRead()
    }

    private fun requestPermissionThenRead() {
        val fine = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
        val coarse = ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
        if (fine == PackageManager.PERMISSION_GRANTED || coarse == PackageManager.PERMISSION_GRANTED) {
            readLastKnownLocation()
        } else {
            locationPermissionRequest.launch(arrayOf(
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION))
        }
    }

    @SuppressLint("MissingPermission")
    private fun readLastKnownLocation() {
        fusedClient.lastLocation
            .addOnSuccessListener { location -> handleLastLocation(location) }
            .addOnFailureListener { e -> showMessage("读取缓存位置失败:${e.message}") }
    }

    private fun handleLastLocation(location: Location?) {
        if (location == null) {
            showMessage("缓存为空,需要发起新定位请求")
            requestFreshLocation()
            return
        }
        val ageMs = System.currentTimeMillis() - location.time
        val accuracy = location.accuracy
        val fresh = ageMs <= maxAgeMs && accuracy <= maxAccuracyM

        renderLocation(location, ageMs, accuracy, fresh)

        if (!fresh) {
            showMessage("缓存已过期或精度不足,后台刷新中…")
            requestFreshLocation()
        }
    }

    @SuppressLint("MissingPermission")
    private fun requestFreshLocation() {
        val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000L)
            .setMaxUpdates(1)
            .build()
        fusedClient.requestLocationUpdates(request, object : LocationCallback() {
            override fun onLocationResult(result: LocationResult) {
                result.lastLocation?.let { fresh ->
                    renderLocation(fresh, 0L, fresh.accuracy, true)
                }
                fusedClient.removeLocationUpdates(this)
            }
        }, Looper.getMainLooper())
    }

    private fun renderLocation(loc: Location, ageMs: Long, accuracy: Float, fresh: Boolean) {
        val tag = if (fresh) "可用" else "过期"
        showMessage("[$tag] lat=${loc.latitude}, lon=${loc.longitude}, " +
                "age=${ageMs / 1000}s, acc=${accuracy}m")
    }

    private fun showMessage(msg: String) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
    }
}

判断逻辑可视化

getLastLocation() location == null ? 是 发起新请求 requestLocationUpdates 否 age ≤ 2 min ? now - location.time 否 发起新请求 后台刷新 是 accuracy ≤ 100 m ? location.accuracy 否 发起新请求 后台刷新 是 直接使用缓存 立即渲染

关键参数与判读

参数含义示例阈值超限处理
location == null缓存中无任何位置—直接发起新请求
ageMs位置距今毫秒数≤ 120000视为过期,后台刷新
accuracy水平精度(米)≤ 100视为不可用,后台刷新
getLastLocation 返回的 Location 可能来自其他应用写入的缓存,其精度与时间戳不一定满足你的业务需求。不要跳过时效性判断直接使用,否则可能把几分钟甚至几小时前的位置当作当前位置展示。

验证方法

  1. 首次安装后立即调用:预期返回 null,触发新请求路径。
  2. 打开地图应用定位一次后回到本页:预期返回非 null,age 较小,直接使用缓存。
  3. 关闭定位服务后调用:预期返回 null 或旧位置,观察是否走新请求分支并收到失败回调。
  4. 用 Toast 输出 age 与 accuracy,确认阈值判断与预期一致。