原文地址: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以上。
没有什么比你们剪切、复制、借阅、“剽窃"这些代码去构造更好的基于定位功能的应用程序能让我更加高兴。如果你这么做了,我非常乐意你告诉我这件事。
相关的需求如下:
在以下一小段从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); } };
// 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;
public void requestLocationUpdates(long minTime, long minDistance, Criteria criteria, PendingIntent pendingIntent) { locationManager.requestLocationUpdates(minTime, minDistance, criteria, pendingIntent); }
<receiver android:name=".receivers.LocationChangedReceiver"/>
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); }
// 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); }
<receiver android:name=".receivers.PassiveLocationChangedReceiver"/>
这样当应用程序因为系统需要释放资源而被强行终止的时候,我们依然接收到那些后台位置信息。
这种允许系统回收应用程序所用资源的做法有非常明显的好处,同时也能够为应用启动时零延迟获取位置信息提供便利。
如果应用程序意识到了退出的意图(比如用户在应用程序主页点击后退按钮时),此时关闭被动位置更新是一种很好的最好,这也包括停用Manifest中的被动位置接收器。
在某种特定的情况下,我们同样需要预读位置详情信息,以下来自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. }
<span style="font-weight: normal;">NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting();</span>
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);
<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>
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);