深入探究Android定位(一)

原文地址:http://android-developers.blogspot.com/2011/06/deep-dive-into-location.html

本文原作者为Reto Meier,Google Android技术开发推广部主管。

我是一名基于定位功能的应用程序的粉丝,但是定位功能的启动延迟却令我有点不爽。

不管是寻找一个吃饭的地方还是搜索周围最近的自行车租车行,我发现等待GPS定位然后再列出所有结果的列表的过程冗长缓慢。当我在某个场所等待提示,准备入住或者寻找饭馆的时候,我经常被缺少数据连接的情况弄得很无奈。

不怨天尤人,我写了一个开源应用项目整合了我知道的所有技巧、方法和窍门来减少开启应用程序和获得一个附近场所实时列表之间的时间间隔,同时提供了合理级别的离线支持,让电量的消耗降到最低。

让我看看代码吧!

你可以在这里https://github.com/svn2github/android-protips-location获取到这个开源工程,别忘了阅读Readme.txt中的步骤以便你可以成功编译运行它。

它到底做了些什么?

它使用 Google Places API实现了通过定位提供周围兴趣点的应用程序的核心功能,你同时还能查看兴趣点详情,然后实际体验、评级、评论它们。

这些代码实现了我在2011年Google开发者大会上发言,Android Protips: Advanced Topics for Expert Android Developers (video),中提出的很多最佳实践,包括使用Intents去接收位置更新信息,使用Passive Location Provider(被动的位置提供器),监测并使用设备状态改变刷新频率,运行时动态开关你的广播接收器以及使用Cursor Loader。

本应用基于HoneyComb(API 11),但是支持所有Android 1.6以上。

没有什么比你们剪切、复制、借阅、“剽窃"这些代码去构造更好的基于定位功能的应用程序能让我更加高兴。如果你这么做了,我非常乐意你告诉我这件事。

现在你有代码了,让我们好好看看它们!

我最优先考虑的是"新鲜度":减小启动应用程序到获得所需位置信息之间的延迟,同时减小应用程序对电量的消耗。

相关的需求如下:

  • 尽可能快地确定当前位置。
  • 当位置改变的时候,相关场所的列表需要更新。
  • 周边场所的位置和详情在离线状态下依然可用。
  • 处于离线状态下地理签到依然可用。
  • 位置数据和其他的用户数据必须被合理使用(参见 prior blog post on best practices)。

新鲜度意味着从来不需要等待

通过在每次应用程序重新与用户交互的时候从Location Manager中获取最后一次已知的位置,可以显著降低首次获取位置时的延迟。

在以下一小段从GingerbreadLastLocationFinder中摘录的代码中,我们遍历当前设备上的每一个Location Provider(包括当前不可用的那些),来查找最及时的和最精确的最后一次已知位置。

  /**
   * Returns the most accurate and timely previously detected location.
   * Where the last result is beyond the specified maximum distance or 
   * latency a one-off location update is returned via the {@link LocationListener}
   * specified in {@link setChangedLocationListener}.
   * @param minDistance Minimum distance before we require a location update.
   * @param minTime Minimum time required between location updates.
   * @return The most accurate and / or timely previously detected location.
   */
  public Location getLastBestLocation(int minDistance, long minTime) {
    Location bestResult = null;
    float bestAccuracy = Float.MAX_VALUE;
    long bestTime = Long.MIN_VALUE;
    
    // Iterate through all the providers on the system, keeping
    // note of the most accurate result within the acceptable time limit.
    // If no result is found within maxTime, return the newest Location.
    List<String> matchingProviders = locationManager.getAllProviders();
    for (String provider: matchingProviders) {
      Location location = locationManager.getLastKnownLocation(provider);
      if (location != null) {
        float accuracy = location.getAccuracy();
        long time = location.getTime();
        
        if ((time > minTime && accuracy < bestAccuracy)) {
          bestResult = location;
          bestAccuracy = accuracy;
          bestTime = time;
        }
        else if (time < minTime && bestAccuracy == Float.MAX_VALUE && time > bestTime) {
          bestResult = location;
          bestTime = time;
        }
      }
    }
    
    // If the best result is beyond the allowed time limit, or the accuracy of the
    // best result is wider than the acceptable maximum distance, request a single update.
    // This check simply implements the same conditions we set when requesting regular
    // location updates every [minTime] and [minDistance]. 
    if (locationListener != null && (bestTime < minTime || bestAccuracy > minDistance)) { 
      IntentFilter locIntentFilter = new IntentFilter(SINGLE_LOCATION_UPDATE_ACTION);
      context.registerReceiver(singleUpdateReceiver, locIntentFilter);      
      locationManager.requestSingleUpdate(criteria, singleUpatePI);
    }
    
    return bestResult;
  }

如果在允许的延迟时间之内有一个或更多位置信息可用,我们返回其中最精确的那个结果。如果没有,那我们只简单地返回其中时间上距现在最近的那个结果。

译者注:

首先看以上代码中的else if分支:

由于初始时

float bestAccuracy = Float.MAX_VALUE;
long bestTime = Long.MIN_VALUE;

此时只要任何一种Location Provider的LastKnownLocation存在,其定位时间一定大于bestTime,于是将定位结果bestResult赋值为该位置数据。如果之后查询其他Location Provider,没有发现更精确的位置数据,就返回该结果作为bestResult。如果在允许时间之内,其他的Location Provider可以提供更精确的位置数据,那就使用更为精确的位置数据,也就是以上代码中以下分支:

 if ((time > minTime && accuracy < bestAccuracy)) {
          bestResult = location;
          bestAccuracy = accuracy;
          bestTime = time;
       }

注意精度信息accuracy是一个float型数值,精度越高,该数值越小。

在后一种情况下(也就是没有其他Location Provider能提供更精确的位置数据的情况下,此时最后一次更新的位置信息并不够及时),这个“最新的结果”依然被返回了,但是我们还要使用目前可用的最快速的Location Provider进行单次位置更新。

if (locationListener != null &&
   (bestTime < maxTime || bestAccuracy > maxDistance)) { 
  IntentFilter locIntentFilter = new IntentFilter(SINGLE_LOCATION_UPDATE_ACTION);
  context.registerReceiver(singleUpdateReceiver, locIntentFilter);      
  locationManager.requestSingleUpdate(criteria, singleUpatePI);
}

很不幸的是,当我们选择一个Location Provider的时候,我们不能指定“最快”作为预设条件,但是事实上我们知道粗略一些的位置提供者,特别是基于网络的,一般说来相比那些更为精确的定位方式能更快返回结果。既然如此,当网络定位可用的时候,我会使用它获得粗略的定位结果,并且这样消耗的电量也更少。

注意,以下来自于GingerbreadLastLocationFinder的代码片段使用了requestSingleUpdate方法来获取一次性的位置更新。这种做法在GingerBread之前的版本是不可用的,请查阅LegacyLastLocationFinder 去了解我如何在运行更早版本系统的设备上实现了与之具有相同功能的方法。

singleUpdateReceiver通过注册一个Location Listener将接收到的位置更新信息返回给调用者。

protected BroadcastReceiver singleUpdateReceiver = new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    context.unregisterReceiver(singleUpdateReceiver);
      
    String key = LocationManager.KEY_LOCATION_CHANGED;
    Location location = (Location)intent.getExtras().get(key);
      
    if (locationListener != null && location != null)
      locationListener.onLocationChanged(location);
      
    locationManager.removeUpdates(singleUpatePI);
  }
};

使用Intent接收位置更新

获得了最为精确/及时的对当前位置的估算之后,我们依然需要接收位置更新信息。
PlacesConstants类中包含了一系列决定当前位置更新频率的值(与服务器轮询频率相关)。调整他们来确保位置信息更新的频率和你的需求一致。
// The default search radius when searching for places nearby.
public static int DEFAULT_RADIUS = 150;
// The maximum distance the user should travel between location updates. 
public static int MAX_DISTANCE = DEFAULT_RADIUS/2;
// The maximum time that should pass before the user gets a location update.
public static long MAX_TIME = AlarmManager.INTERVAL_FIFTEEN_MINUTES; 

下一步就是从Location Manager中请求位置更新。在以下这些来自GingerbreadLocationUpdateRequester的代码中,我们可以把决定采用哪个Location Provider来提供位置更新信息的限制条件传递到requestLocationUpdates的调用中。

public void requestLocationUpdates(long minTime, long minDistance, 
  Criteria criteria, PendingIntent pendingIntent) {

  locationManager.requestLocationUpdates(minTime, minDistance, 
    criteria, pendingIntent);
}

注意,我们将传递一个PendingIntent而不是一个Location Listener。

一般来说,相比于是使用Location Listener,我更喜欢这种方式,因为在不同的Activity或者Service中注册甚至直接在Manifest中注册广播接收器的方式更加灵活。在本应用中,一个新的位置意味着需要更新周边场所列表。这是通过一个不断查询发布地点列表的Content Provider的后台服务来实现的。由于位置改变并不会直接更新UI界面,所以在Manifest中注册关联LocationChangedReceiver相比于直接在main Activity中动态注册来说也是有实际意义的。

<receiver android:name=".receivers.LocationChangedReceiver"/>

Location Changed Receiver从每次更新中获取位置信息,然后启动PlaceUpdateService来刷新周边位置数据库。

if (intent.hasExtra(locationKey)) {
  Location location = (Location)intent.getExtras().get(locationKey);
  Log.d(TAG, "Actively Updating place list");
  Intent updateServiceIntent = 
    new Intent(context, PlacesConstants.SUPPORTS_ECLAIR ? EclairPlacesUpdateService.class : PlacesUpdateService.class);
  updateServiceIntent.putExtra(PlacesConstants.EXTRA_KEY_LOCATION, location);
  updateServiceIntent.putExtra(PlacesConstants.EXTRA_KEY_RADIUS, PlacesConstants.DEFAULT_RADIUS);
  updateServiceIntent.putExtra(PlacesConstants.EXTRA_KEY_FORCEREFRESH, true);

  context.startService(updateServiceIntent);
}

监测非活动状态的Provider获取更好的选项

下面这段PlacesActivity中的代码展示了如何监测两个重要的条件:
  • 当前正在使用的Location Provider变成了不可用状态
  • 一个更好的Location Provider变成了可用状态
在其他情况下,我们重新执行判断哪个可用Provider为最佳的过程,然后进行位置更新。

// Register a receiver that listens for when the provider I'm using has been disabled. 
IntentFilter intentFilter = new IntentFilter(PlacesConstants.ACTIVE_LOCATION_UPDATE_PROVIDER_DISABLED);
registerReceiver(locProviderDisabledReceiver, intentFilter);

// Listen for a better provider becoming available.
String bestProvider = locationManager.getBestProvider(criteria, false);
String bestAvailableProvider = locationManager.getBestProvider(criteria, true);
if (bestProvider != null && !bestProvider.equals(bestAvailableProvider))
  locationManager.requestLocationUpdates(bestProvider, 0, 0, 
    bestInactiveLocationProviderListener, getMainLooper());

新鲜度意味着永远即时更新,我们如何将延迟降低为0呢?

当你的应用程序在后台运行的时候,你可以在后台启动PlacesUpdateService更新周边地点信息。

如果做法正确,一个相关的场所列表可以在打开应用程序时立刻被获取到。

如果做法不佳,你的用户可能永远不会获得这种体验,并且他们的手机电量也会被快速消耗。

当你的应用程序不在前台运行的时候去请求位置更新(特别是使用GPS的情况下)是非常糟糕的做法,因为那样会显著加快电量消耗。取而代之的是,你可以使用Passive Location Provider在其他应用已经请求位置更新的同时接收这些信息。

这些是来自FroyoLocationUpdateRequester中的针对Froyo以上版本的实现被动位置更新的代码

public void requestPassiveLocationUpdates(long minTime, long minDistance, PendingIntent pendingIntent) {
  locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER,
    PlacesConstants.MAX_TIME, PlacesConstants.MAX_DISTANCE, pendingIntent);    
}

在后台获取位置更新信息在效率上是非常高效的,但是必须要考虑数据下载时的电量消耗,因此你还要是非常小心地权衡被动位置更新和电量消耗之间的取舍。

你可以在Froyo之前版本的设备上使用非精确重复的非唤醒闹钟来实现相同的效果,这些代码在LegacyLocationUpdateRequester

public void requestPassiveLocationUpdates(long minTime, long minDistance, 
  PendingIntent pendingIntent) {

  alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,   
    System.currentTimeMillis()+PlacesConstants.MAX_TIME, 
    PlacesConstants.MAX_TIME, pendingIntent);    
}
这种技术根据由最大位置更新延迟所确定的频率来不断查询最后一次已知的位置信息,而不是直接从Location Manager中获取位置更新信息。

这种遗留下来的技术效率很低,所以你可以简单地选择在版本低于Froyo的设备上禁用后台位置更新。

我们在PassiveLocationChangedReceiver中处理位置更新,确定当前位置并且启动PlaceUpdateService

if (location != null) {
  Intent updateServiceIntent = 
    new Intent(context, PlacesConstants.SUPPORTS_ECLAIR ? EclairPlacesUpdateService.class : PlacesUpdateService.class);
  
  updateServiceIntent.putExtra(PlacesConstants.EXTRA_KEY_LOCATION, location);
  updateServiceIntent.putExtra(PlacesConstants.EXTRA_KEY_RADIUS, 
    PlacesConstants.DEFAULT_RADIUS);
  updateServiceIntent.putExtra(PlacesConstants.EXTRA_KEY_FORCEREFRESH, false);
  context.startService(updateServiceIntent);   
}

当你的应用程序处于非活动状态时,使用Intent来被动接收位置更新

注意,我们在应用程序的Manifest中注册Passive Location Changed Receiver

<receiver android:name=".receivers.PassiveLocationChangedReceiver"/>


这样当应用程序因为系统需要释放资源而被强行终止的时候,我们依然接收到那些后台位置信息。

这种允许系统回收应用程序所用资源的做法有非常明显的好处,同时也能够为应用启动时零延迟获取位置信息提供便利。

如果应用程序意识到了退出的意图(比如用户在应用程序主页点击后退按钮时),此时关闭被动位置更新是一种很好的最好,这也包括停用Manifest中的被动位置接收器。

实时更新意味着离线工作

为了添加离线支持,我们首先需要缓存所需信息到PlacesContentProviderPlaceDetailsContentProvider中。

在某种特定的情况下,我们同样需要预读位置详情信息,以下来自PlacesUpdateService展示了对有限数量的位置如何启用预读处理。

注意,预读处理在处于移动数据网络或者电量较低的时候可能会无法使用。

if ((prefetchCount < PlacesConstants.PREFETCH_LIMIT) &&
    (!PlacesConstants.PREFETCH_ON_WIFI_ONLY || !mobileData) &&
    (!PlacesConstants.DISABLE_PREFETCH_ON_LOW_BATTERY || !lowBattery)) {
  prefetchCount++;
      
  // Start the PlaceDetailsUpdateService to prefetch the details for this place.
}


我们使用类似的基础对离线地理签到提供支持,PlaceCheckinService将失败的地理签到请求和离线状态时的地理签到请求添加到队列中,当ConnectivityChangedReceiver确定我们重新连接到网络的时候在对这些请求进行重试(按照请求在队列中的顺序)

优化电池续航:  更智能化的服务和根据设备状态开关广播接收器

没有任何理由在离线状态下开启更新服务,因此PlaceUpdateService在尝试更新之前将首先判断当前的网络连接状态

<span style="font-weight: normal;">NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
boolean isConnected = activeNetwork != null &&
                      activeNetwork.isConnectedOrConnecting();</span>

如果当前网络连接不可用,Passive和Active Location Change Receiver都将被禁用,而ConnectivityChangedReceiver将被启用。
ComponentName connectivityReceiver = 
  new ComponentName(this, ConnectivityChangedReceiver.class);
ComponentName locationReceiver = 
  new ComponentName(this, LocationChangedReceiver.class);
ComponentName passiveLocationReceiver = 
  new ComponentName(this, PassiveLocationChangedReceiver.class);

pm.setComponentEnabledSetting(connectivityReceiver,
  PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 
  PackageManager.DONT_KILL_APP);
            
pm.setComponentEnabledSetting(locationReceiver,
  PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 
  PackageManager.DONT_KILL_APP);
      
pm.setComponentEnabledSetting(passiveLocationReceiver,
  PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 
  PackageManager.DONT_KILL_APP);

ConnectivityChangedReceiver监听网络状态改变,新的连接一旦被建立,它将停用自身并且重新启用位置监听。

监控电池状态来减少功能节约电量消耗

当手机电量低于15%的时候,大部分的应用程序都会尽维护仅剩的电量。我们可以在Manifest中注册广播接收器,以便在设备进入或者离开低电量状态的时候发出警示。

<receiver android:name=".receivers.PowerStateChangedReceiver">
  <intent-filter>
    <action android:name="android.intent.action.ACTION_BATTERY_LOW"/>
    <action android:name="android.intent.action.ACTION_BATTERY_OKAY"/>
  </intent-filter>
</receiver>


这些来自PowerStateChangedReceiver中的代码在设备进入低电量状态的时候将停用PassiveLocationChangedReceiver,并在电量回到合适状态的时候重新启用它。

boolean batteryLow = intent.getAction().equals(Intent.ACTION_BATTERY_LOW);
 
pm.setComponentEnabledSetting(passiveLocationReceiver,
  batteryLow ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED :
               PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,  
  PackageManager.DONT_KILL_APP);


你可以把以上逻辑拓展,以便在电量过低的条件下禁用预读功能或者降低位置更新的频率。





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值