28、性能优化:定位频率控制、缓存策略、内存泄漏防范
定位功能做完了,能跑起来,这不算完。
真正考验功力的,是让它跑得稳、跑得省、跑得久。我见过太多App,定位一开,电量哗哗掉,内存蹭蹭涨,最后被用户骂成“耗电怪兽”。
这一章,咱们就聊聊怎么把定位模块打磨得既听话又省心。
28.1 定位频率控制:别让CPU白干活
定位频率控制,说白了就是决定“多久问一次GPS要位置”。
你想想看,如果每秒钟都请求一次位置,大部分时候位置根本没变,CPU却在疯狂计算。这不是浪费是什么?
28.1.1 根据场景动态调整
我个人习惯把定位场景分成三类:
- 前台高频场景:比如导航、跑步轨迹。需要1-2秒一次,精度要求高。
- 前台低频场景:比如查看附近店铺。5-10秒一次就够了。
- 后台场景:比如签到打卡、地理围栏。几分钟甚至十几分钟一次。
代码里怎么控制?我一般用LocationRequest的setInterval()和setFastestInterval():
// 高频模式:导航场景
LocationRequest highFreqRequest = new LocationRequest.Builder(
Priority.PRIORITY_HIGH_ACCURACY, 1000) // 1秒一次
.setWaitForAccurateLocation(true)
.setMinUpdateIntervalMillis(500) // 最快500ms
.build();
// 低频模式:后台签到
LocationRequest lowFreqRequest = new LocationRequest.Builder(
Priority.PRIORITY_BALANCED_POWER_ACCURACY, 60000) // 60秒一次
.setMinUpdateIntervalMillis(30000) // 最快30秒
.build();
setFastestInterval设得太激进。我曾经有个项目,设了100ms,结果定位回调像机关枪一样狂喷,UI线程直接卡死。后来改成500ms,世界清净了。
28.1.2 使用DeferredLocationUpdates
如果你不需要实时更新,可以用DeferredLocationUpdates。它会把多个定位结果攒起来,一次性回调给你。
// 延迟更新:每10秒批量回调一次
locationClient.flushLocations();
// 或者设置延迟阈值
locationClient.setDeferredUpdates(10000, 5, // 10秒内最多5次
Executors.newSingleThreadExecutor());
嗯,这里要注意:延迟更新适合“我不在乎具体时间点,只在乎大概位置”的场景。比如统计用户一天的活动范围,完全可以用这个。
28.2 缓存策略:别每次都重新定位
定位是个耗电活。如果能用缓存,就别重新请求。
我遇到过最夸张的情况:有个App每次打开都重新定位,哪怕用户刚关掉App10秒钟。这不是傻是什么?
28.2.1 利用Location的时效性
Android的Location对象自带getTime()和getAccuracy()。我们可以根据这两个字段判断缓存是否可用:
private boolean isLocationCacheValid(Location location, long maxAgeMs, float maxAccuracyMeters) {
if (location == null) return false;
long age = System.currentTimeMillis() - location.getTime();
// 缓存超过30秒,或者精度大于100米,视为无效
if (age > maxAgeMs || location.getAccuracy() > maxAccuracyMeters) {
return false;
}
return true;
}
调用的时候:
Location lastLocation = fusedLocationClient.getLastLocation().getResult();
if (isLocationCacheValid(lastLocation, 30000, 100f)) {
// 直接用缓存,不重新定位
updateUI(lastLocation);
} else {
// 缓存太旧,重新请求
requestNewLocation();
}
28.2.2 自己维护一个轻量缓存
有时候getLastLocation()返回null,或者系统清掉了缓存。这时候我习惯自己维护一个:
public class LocationCache {
private Location cachedLocation;
private long cacheTime;
public void put(Location location) {
this.cachedLocation = location;
this.cacheTime = System.currentTimeMillis();
}
public Location get(long maxAgeMs) {
if (cachedLocation == null) return null;
if (System.currentTimeMillis() - cacheTime > maxAgeMs) return null;
return cachedLocation;
}
}
这个类很简单,但很实用。我在一个外卖配送项目里用了它,定位请求次数直接减少了60%。
28.3 内存泄漏防范:别让定位成为内存杀手
内存泄漏,是定位模块最容易踩的坑。
为什么?因为FusedLocationProviderClient是系统服务,它的生命周期比Activity长。如果你在Activity里注册了定位回调,Activity销毁时没取消注册,那Activity就泄漏了。
28.3.1 生命周期绑定
我建议用Lifecycle组件来管理定位回调。这样不用手动记着取消注册:
public class LocationLifecycleObserver implements DefaultLifecycleObserver {
private final FusedLocationProviderClient client;
private final LocationCallback callback;
public LocationLifecycleObserver(FusedLocationProviderClient client, LocationCallback callback) {
this.client = client;
this.callback = callback;
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
client.removeLocationUpdates(callback);
}
}
// 在Activity中使用
getLifecycle().addObserver(new LocationLifecycleObserver(client, callback));
这样,Activity一进入onStop,定位回调自动移除。再也不用担心忘记取消注册了。
onDestroyView里没取消。结果Fragment的View被销毁了,但定位回调还在,导致View泄漏。后来我改用Lifecycle组件,这个问题再也没出现过。
28.3.2 避免匿名内部类
匿名内部类会隐式持有外部类的引用。如果你在Activity里这样写:
// 危险写法!匿名内部类持有Activity引用
fusedLocationClient.requestLocationUpdates(request, new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
// 处理定位结果
}
}, Looper.getMainLooper());
如果Activity销毁时没取消注册,这个LocationCallback会一直持有Activity引用,导致泄漏。
正确的做法是:把LocationCallback声明为成员变量,或者用弱引用:
private LocationCallback callback = new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
// 处理定位结果
}
};
// 注册
fusedLocationClient.requestLocationUpdates(request, callback, Looper.getMainLooper());
// 取消注册
fusedLocationClient.removeLocationUpdates(callback);
28.3.3 使用弱引用处理回调
如果你实在需要在匿名内部类里访问外部类,可以用弱引用:
public class LocationManager {
private WeakReference<Activity> activityRef;
public void startLocation(Activity activity) {
this.activityRef = new WeakReference<>(activity);
fusedLocationClient.requestLocationUpdates(request, new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
Activity act = activityRef.get();
if (act != null) {
// 安全地使用Activity
}
}
}, Looper.getMainLooper());
}
}
嗯,这里要注意:弱引用不是万能的。如果Activity已经被回收,回调里拿到的就是null,需要做好判空处理。
28.4 知识体系总览
这一章的内容,我用一张图帮你理清思路:
28.5 避坑指南
最后,分享几个我亲身踩过的坑:
- 不要在主线程做定位计算:定位回调默认在主线程,如果你在里面做大量计算,UI会卡顿。我习惯用
Executors.newSingleThreadExecutor()处理数据。 - 注意Android 12+的定位权限变化:后台定位现在需要单独申请
ACCESS_BACKGROUND_LOCATION权限,而且审核更严了。 - 测试时别只盯着模拟器:模拟器的定位行为跟真机差很多。我建议用真机测试,特别是低端机,更容易暴露性能问题。
公众号:蓝海资料掘金营,微信deep3321