28、性能优化:定位频率控制、缓存策略、内存泄漏防范

定位功能做完了,能跑起来,这不算完。

真正考验功力的,是让它跑得稳、跑得省、跑得久。我见过太多App,定位一开,电量哗哗掉,内存蹭蹭涨,最后被用户骂成“耗电怪兽”。

这一章,咱们就聊聊怎么把定位模块打磨得既听话又省心。

28.1 定位频率控制:别让CPU白干活

定位频率控制,说白了就是决定“多久问一次GPS要位置”。

你想想看,如果每秒钟都请求一次位置,大部分时候位置根本没变,CPU却在疯狂计算。这不是浪费是什么?

28.1.1 根据场景动态调整

我个人习惯把定位场景分成三类:

  • 前台高频场景:比如导航、跑步轨迹。需要1-2秒一次,精度要求高。
  • 前台低频场景:比如查看附近店铺。5-10秒一次就够了。
  • 后台场景:比如签到打卡、地理围栏。几分钟甚至十几分钟一次。

代码里怎么控制?我一般用LocationRequestsetInterval()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();
}
核心原则: 能复用就复用。定位缓存的有效期,取决于你的业务场景。导航类App可能只接受5秒内的缓存,而天气类App接受10分钟都没问题。

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,定位回调自动移除。再也不用担心忘记取消注册了。

我曾经踩过的坑: 有一次在Fragment里注册了定位,但Fragment的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 知识体系总览

这一章的内容,我用一张图帮你理清思路:

定位性能优化三大支柱 定位频率控制 动态调整间隔 setInterval / setFastestInterval DeferredLocationUpdates 缓存策略 getLastLocation() 复用 时效性 + 精度判断 自定义轻量缓存 内存泄漏防范 Lifecycle 绑定 避免匿名内部类 弱引用处理回调 目标:省电 + 省内存 + 响应快

28.5 避坑指南

最后,分享几个我亲身踩过的坑:

  • 不要在主线程做定位计算:定位回调默认在主线程,如果你在里面做大量计算,UI会卡顿。我习惯用Executors.newSingleThreadExecutor()处理数据。
  • 注意Android 12+的定位权限变化:后台定位现在需要单独申请ACCESS_BACKGROUND_LOCATION权限,而且审核更严了。
  • 测试时别只盯着模拟器:模拟器的定位行为跟真机差很多。我建议用真机测试,特别是低端机,更容易暴露性能问题。
一句话总结: 定位性能优化,核心就是“少请求、多用缓存、及时清理”。做到这三点,你的App在定位这块就不会出大问题。

公众号:蓝海资料掘金营,微信deep3321