欢迎参加我们将于 6 月 3 日举行的 #Android11:Beta 版发布会

位置策略

注意:本指南中描述的策略适用于 android.location 中的 Platform Location API。Google Location Services API 是 Google Play 服务的一部分,它提供了一种更强大的高级框架,可自动处理位置信息提供程序、用户移动和位置信息准确性。它还可以根据您提供的耗电量参数处理位置信息更新安排。在大多数情况下,通过使用 Location Services API,您将获得更好的电池性能以及更适当的准确性。

要详细了解 Location Services API,请参阅适用于 Android 的 Google 位置信息服务

了解用户的位置可以让应用更加智能,并为用户提供更好的信息。在针对 Android 开发具备位置感知能力的应用时,您可以利用 GPS 和 Android 网络位置信息提供程序获取用户的位置信息。虽然 GPS 最为准确,但它只能在户外工作,并会快速消耗电池电量,而且无法像用户期望的那样快速返回位置信息。Android 网络位置信息提供程序根据手机信号塔和 WLAN 信号确定用户位置信息,在室内外均可工作,响应速度更快,并且电池耗电量更少。要在应用中获取用户位置信息,您既可以同时使用 GPS 和网络位置信息提供程序,也可以只使用其中的一种。

确定用户位置信息存在的挑战

在移动设备上获取用户位置信息可能是件很复杂的任务。位置信息读数(无论来源是什么)可能因为多种原因而包含错误并且不准确。用户位置信息的一些错误来源包括:

  • 位置信息来源众多

    GPS、Cell-ID 和 WLAN 均可提供用户位置信息线索。确定究竟使用和信任哪种方式,需要在准确性、速度和电池能效方面做出权衡。

  • 用户移动

    由于用户位置信息会发生变化,因此您必须不时地重新估算用户位置信息,从而将移动情况考虑在内。

  • 准确性不同

    每个位置信息来源产生的位置信息估算数据准确性不一致。10 秒前从一个来源获取的位置信息可能比从另一个或同一个来源获取的最新位置信息更准确。

这些问题导致我们难以获取可靠的用户位置信息读数。本文档提供的信息可以帮助您应对这些挑战,以获取可靠的位置信息读数。本文还提供了一些供您在应用中采用的建议,从而为用户提供准确且灵敏的地理位置信息体验。

请求位置信息更新

在解决上述某些位置信息错误之前,我们先来介绍一下如何在 Android 上获取用户位置信息。

您可以通过回调在 Android 中获取用户位置信息。您可以通过调用 requestLocationUpdates() 并传入 LocationListener 以从 LocationManager 接收位置信息更新。在用户位置信息或服务状态发生变化时,您的 LocationListener 必须实现 LocationManager 调用的多个回调方法。

注意:在搭载 Android 8.0(API 级别 26)及更高版本的设备上,如果应用正在后台运行并请求当前位置信息,则设备每小时仅计算几次位置信息。要了解如何让应用适应这些计算限制,请参阅后台位置信息限制

以下代码展示了如何定义 LocationListener 和请求位置信息更新:

Kotlin

    // Acquire a reference to the system Location Manager
    val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager

    // Define a listener that responds to location updates
    val locationListener = object : LocationListener {

        override fun onLocationChanged(location: Location) {
            // Called when a new location is found by the network location provider.
            makeUseOfNewLocation(location)
        }

        override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {
        }

        override fun onProviderEnabled(provider: String) {
        }

        override fun onProviderDisabled(provider: String) {
        }
    }

    // Register the listener with the Location Manager to receive location updates
    locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0f,locationListener)
    

Java

    // Acquire a reference to the system Location Manager
    LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);

    // Define a listener that responds to location updates
    LocationListener locationListener = new LocationListener() {
        public void onLocationChanged(Location location) {
          // Called when a new location is found by the network location provider.
          makeUseOfNewLocation(location);
        }

        public void onStatusChanged(String provider, int status, Bundle extras) {}

        public void onProviderEnabled(String provider) {}

        public void onProviderDisabled(String provider) {}
      };

    // Register the listener with the Location Manager to receive location updates
    locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListener);
    

requestLocationUpdates() 中的第一个参数是要使用的位置信息提供程序的类型(在本例中,是获取基于手机信号塔和 WLAN 的位置信息的网络位置信息提供程序)。您可以通过第二个和第三个参数控制监听器接收更新的频率(第二个参数是通知之间的时间间隔下限,第三个参数是通知之间的距离变化下限);如需尽可能频繁地请求位置信息通知,只需将这两个参数都设为 0 即可。最后一个参数是 LocationListener,用于接收位置信息更新的回调。

要从 GPS 提供程序请求位置信息更新,请使用 GPS_PROVIDER,而不是 NETWORK_PROVIDER。您还可以同时从 GPS 和网络位置信息提供程序请求位置信息更新,只需调用 requestLocationUpdates() 两次即可(一次针对 NETWORK_PROVIDER,另一次针对 GPS_PROVIDER)。

请求用户权限

要从 NETWORK_PROVIDERGPS_PROVIDER 接收位置信息更新,您必须通过在 Android 清单文件中分别声明 ACCESS_COARSE_LOCATIONACCESS_FINE_LOCATION 权限来请求用户权限。如果没有这些权限,应用将在请求位置信息更新时在运行时失败。

如果您同时使用 NETWORK_PROVIDERGPS_PROVIDER,则只需请求 ACCESS_FINE_LOCATION 权限,因为它包含针对这两个提供程序的权限。针对 ACCESS_COARSE_LOCATION 的权限只允许访问 NETWORK_PROVIDER

注意:如果您的应用以 Android 5.0(API 级别 21)或更高版本为目标平台,则必须在清单文件中声明应用使用的是 android.hardware.location.networkandroid.hardware.location.gps 硬件功能,具体取决于应用是从 NETWORK_PROVIDER 还是 GPS_PROVIDER 接收位置信息更新。如果应用从这两个位置信息提供来源之一接收位置信息,则需要在应用清单中声明应用使用了相应的硬件功能。在搭载 Android 5.0 (API 21) 之前版本的设备中,请求 ACCESS_FINE_LOCATIONACCESS_COARSE_LOCATION 权限包含对位置信息硬件功能的隐含请求。不过,在 Android 5.0(API 级别 21)及更高版本上,请求这些权限并不会自动请求位置信息硬件功能。

以下代码示例演示了如何在应用的清单文件中声明用于从设备的 GPS 读取数据的权限和硬件功能:

    <manifest ... >
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
        ...
        <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
        <uses-feature android:name="android.hardware.location.gps" />
        ...
    </manifest>
    

定义模型以实现最佳性能

基于位置信息的应用现在已经司空见惯了,但由于准确性不理想、用户会移动、获取位置信息的方法众多以及节省电池电量的需求,获取用户位置信息就变得十分复杂。要在节省电池电量的同时克服那些妨碍您获取优质用户位置信息的障碍,您必须定义一个一致的模型以指定应用获取用户位置信息的方式。此模型包含您开始和停止监听更新的时间,以及使用缓存的位置信息数据的时间。

获取用户位置信息的流程

以下是获取用户位置信息的一般流程:

  1. 启动应用。
  2. 稍后,开始监听来自所需位置信息提供程序的更新。
  3. 过滤掉不太准确的新修正项,使位置信息保持“当前的最佳估算值”。
  4. 停止监听位置信息更新。
  5. 利用最近一次最佳位置信息估算值。

图 1 以时间轴的形式演示了一个这样的模型,直观呈现了应用监听位置信息更新的时间段,以及在这段时间内发生的事件。

图 1. 表示应用监听位置信息更新的窗口的时间轴。

当您将基于位置信息的服务添加到应用中时,会根据此窗口模型(系统在此期间接收位置信息更新)做出许多决策。

决定何时开始监听更新

您可能需要在应用启动后立即开始监听位置信息更新,或仅在用户激活特定功能后才开始监听。请注意,长时间监听位置信息修正可能会消耗大量的电池电量,但短时间监听可能无法达到足够的准确性。

如上所示,您可以通过调用 requestLocationUpdates() 开始监听更新:

Kotlin

    val locationProvider: String = LocationManager.NETWORK_PROVIDER
    // Or, use GPS location data:
    // val locationProvider: String = LocationManager.GPS_PROVIDER;

    locationManager.requestLocationUpdates(locationProvider, 0, 0, locationListener)
    

Java

    String locationProvider = LocationManager.NETWORK_PROVIDER;
    // Or, use GPS location data:
    // String locationProvider = LocationManager.GPS_PROVIDER;

    locationManager.requestLocationUpdates(locationProvider, 0, 0, locationListener);
    

根据最近一次的已知位置信息进行快速修正

位置信息监听器往往需要很长时间才能接收到第一个位置信息修正项,导致用户等得不耐烦。在为位置信息监听器提供更准确的位置之前,您应该通过调用 getLastKnownLocation(String) 利用缓存的位置信息:

Kotlin

    val locationProvider: String = LocationManager.NETWORK_PROVIDER
    // Or use LocationManager.GPS_PROVIDER

    val lastKnownLocation: Location = locationManager.getLastKnownLocation(locationProvider)
    

Java

    String locationProvider = LocationManager.NETWORK_PROVIDER;
    // Or use LocationManager.GPS_PROVIDER

    Location lastKnownLocation = locationManager.getLastKnownLocation(locationProvider);
    

决定何时停止监听更新

决定何时不再需要新修正的逻辑可能非常简单,也可能非常复杂,具体取决于您的应用。如果从获取到使用位置信息的时间间隔较短,则可以提高估算值的准确性。始终要注意,长时间监听会消耗大量的电池电量,因此只要获取了所需的信息,就应立即调用 removeUpdates(PendingIntent) 以停止监听更新:

Kotlin

    // Remove the listener you previously added
    locationManager.removeUpdates(locationListener)
    

Java

    // Remove the listener you previously added
    locationManager.removeUpdates(locationListener);
    

保持当前的最佳估算值

您可能会认为最新的位置信息修正项是最准确的信息。不过,由于位置信息修正项的准确性不尽相同,所以最新的修正项并不一定是最佳的。您应该包含根据多个条件选择位置信息修正项的逻辑。这些条件也因应用和现场测试的用例而有所不同。

您可以执行以下几个步骤以验证位置信息修正项的准确性:

  • 检查检索到的位置信息是否明显比之前的估算值更新。
  • 检查位置信息所声明的准确性优于还是劣于之前的估算值。
  • 检查新的位置信息来自哪个提供程序,并确定您是否更信任此提供程序。

此逻辑的详细示例大致如下:

Kotlin

    private const val TWO_MINUTES: Long = 1000 * 60 * 2

    /** Determines whether one Location reading is better than the current Location fix
     * @param location The new Location that you want to evaluate
     * @param currentBestLocation The current Location fix, to which you want to compare the new one
     */
    fun isBetterLocation(location: Location, currentBestLocation: Location?): Boolean {
        if (currentBestLocation == null) {
            // A new location is always better than no location
            return true
        }

        // Check whether the new location fix is newer or older
        val timeDelta: Long = location.time - currentBestLocation.time
        val isSignificantlyNewer: Boolean = timeDelta > TWO_MINUTES
        val isSignificantlyOlder:Boolean = timeDelta < -TWO_MINUTES

        when {
            // If it's been more than two minutes since the current location, use the new location
            // because the user has likely moved
            isSignificantlyNewer -> return true
            // If the new location is more than two minutes older, it must be worse
            isSignificantlyOlder -> return false
        }

        // Check whether the new location fix is more or less accurate
        val isNewer: Boolean = timeDelta > 0L
        val accuracyDelta: Float = location.accuracy - currentBestLocation.accuracy
        val isLessAccurate: Boolean = accuracyDelta > 0f
        val isMoreAccurate: Boolean = accuracyDelta < 0f
        val isSignificantlyLessAccurate: Boolean = accuracyDelta > 200f

        // Check if the old and new location are from the same provider
        val isFromSameProvider: Boolean = location.provider == currentBestLocation.provider

        // Determine location quality using a combination of timeliness and accuracy
        return when {
            isMoreAccurate -> true
            isNewer && !isLessAccurate -> true
            isNewer && !isSignificantlyLessAccurate && isFromSameProvider -> true
            else -> false
        }
    }
    

Java

    private static final int TWO_MINUTES = 1000 * 60 * 2;

    /** Determines whether one Location reading is better than the current Location fix
      * @param location  The new Location that you want to evaluate
      * @param currentBestLocation  The current Location fix, to which you want to compare the new one
      */
    protected boolean isBetterLocation(Location location, Location currentBestLocation) {
        if (currentBestLocation == null) {
            // A new location is always better than no location
            return true;
        }

        // Check whether the new location fix is newer or older
        long timeDelta = location.getTime() - currentBestLocation.getTime();
        boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
        boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
        boolean isNewer = timeDelta > 0;

        // If it's been more than two minutes since the current location, use the new location
        // because the user has likely moved
        if (isSignificantlyNewer) {
            return true;
        // If the new location is more than two minutes older, it must be worse
        } else if (isSignificantlyOlder) {
            return false;
        }

        // Check whether the new location fix is more or less accurate
        int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
        boolean isLessAccurate = accuracyDelta > 0;
        boolean isMoreAccurate = accuracyDelta < 0;
        boolean isSignificantlyLessAccurate = accuracyDelta > 200;

        // Check if the old and new location are from the same provider
        boolean isFromSameProvider = isSameProvider(location.getProvider(),
                currentBestLocation.getProvider());

        // Determine location quality using a combination of timeliness and accuracy
        if (isMoreAccurate) {
            return true;
        } else if (isNewer && !isLessAccurate) {
            return true;
        } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
            return true;
        }
        return false;
    }

    /** Checks whether two providers are the same */
    private boolean isSameProvider(String provider1, String provider2) {
        if (provider1 == null) {
          return provider2 == null;
        }
        return provider1.equals(provider2);
    }
    

调整模型以节省电池电量和数据交换

在测试应用时,您可能会发现用于提供优质位置信息和良好性能的模型需要进行一些调整。您可以采取以下措施,从而在二者之间达到很好的平衡。

减小窗口的大小

监听位置信息更新的窗口越小,意味着与 GPS 和网络位置信息服务的交互就越少,因而可以延长电池续航时间。但这样一来,从中选择最佳估算值的位置信息也就越少。

将位置信息提供程序设为以较低的频率返回更新

降低新更新在窗口期间出现的速度也能提高电池能效,但准确性会降低。权衡值取决于应用的使用方式。您可以增大 requestLocationUpdates() 中用于指定间隔时间和距离变化下限的参数,从而降低更新速度。

限制一组提供程序

根据应用的使用环境和所需的准确性级别,您可以选择仅使用网络位置信息提供程序或仅使用 GPS,而不是同时使用这两种服务。仅与其中一种服务进行交互可以减少电池用量,但可能会降低准确性。

常见的应用案例

许多原因都可能会促使您想要在应用中获取用户位置信息。在以下几种场景中,您可以使用用户位置信息改进您的应用。每个场景还介绍了与应该开始和停止监听位置信息的时间有关的最佳做法,以便获得优质读数并延长电池续航时间。

使用位置信息标记用户创建的内容

您可能正在创建一款应用,其中用户创建的内容将使用位置信息进行标记。假设用户会分享其当地体验、发布餐厅评论,或记录一些能够使用当前位置信息进行增强的内容。图 2 中的模型直观呈现了在使用位置信息服务时,这种互动可能会如何发生。

图 2. 表示窗口的时间轴,在此窗口中,系统会获取用户位置信息,并在用户使用当前位置信息后停止监听。

这与之前有关如何在代码中获取用户位置信息的模型(图 1)是一致的。为了达到最高的位置信息准确性,您可以选择在用户开始创建内容时,甚至是在应用启动时开始监听位置信息更新,然后在内容随时可供发布或记录时停止监听更新。您可能需要考虑典型的内容创建任务需要多长时间,并判断是否能够在此时长内有效地收集位置信息估算值。

帮助用户决定去哪里

您可能正在创建一款应用,它会针对“去哪里”这一问题为用户提供一系列选项。例如,您想要提供附近的餐厅、商店和娱乐场所列表,并且推荐顺序会根据用户位置信息而发生变化。

为了适应这样的流程,您可以选择:

  • 在获取新的最佳估算值后重新排列建议
  • 如果推荐顺序已经稳定,则停止监听更新

图 3 直观展示了这种模型。

显示位置信息数据不断改善的事件的时间轴

图 3. 表示窗口的时间轴,在此窗口中,每当用户位置信息更新时,系统就会更新一组动态数据。

提供模拟位置信息数据

在开发应用时,您必须要测试模型在获取用户位置信息方面的性能。使用实际 Android 设备即可轻松地实现这一点。不过,如果没有设备,您仍然可以测试基于位置信息的功能,只需在 Android 模拟器中模拟位置信息数据即可。您可以使用设备的开发者选项中提供的模拟位置信息选项,或在模拟器控制台中使用 geo 命令,将模拟位置信息数据发送到您的应用。

注意:提供的模拟位置信息数据将作为 GPS 位置信息注入,因此您必须从 GPS_PROVIDER 请求位置信息更新,才能让模拟位置信息数据正常工作。

使用开发者选项

在设备上启用开发者选项和 USB 调试,然后按照与使用选择模拟位置信息应用选项有关的说明操作。

在模拟器控制台中使用 geo 命令

要从命令行发送模拟位置信息数据,请执行以下操作:

  1. 在 Android 模拟器中启动您的应用,然后在 SDK 的 /tools 目录中打开终端/控制台。
  2. 连接到模拟器控制台:
    telnet localhost <console-port>
  3. 发送位置信息数据:
    • geo fix 用于发送固定的地理位置信息。

      此命令接受十进制的经度和纬度,还选择接受海拔高度(以米为单位)。例如:

      geo fix -121.45356 46.51119 4392
    • geo nmea 用于发送 NMEA 0183 语句。

      此命令接受“$GPGGA”(固定数据)或“$GPRMC”(瞬时数据)类型的单个 NMEA 语句。 例如:

      geo nmea $GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62

要了解如何连接到模拟器控制台,请参阅使用模拟器控制台